From c00cae6fdee69c2885c1912f94a92d627b13a0cb Mon Sep 17 00:00:00 2001 From: babayaga Date: Thu, 25 Dec 2025 02:08:14 +0100 Subject: [PATCH] image utils | cache | components --- .../api/utils/getFilteredProps.js | 27 +++- packages/imagetools_3/api/utils/getSrcset.js | 35 ++++-- .../imagetools_3/components/Picture.astro | 13 +- packages/imagetools_3/integration/index.js | 1 - packages/imagetools_3/plugin/hooks/load.js | 66 +++++----- packages/polymech/src/app/navigation.ts | 10 +- .../polymech/src/components/Breadcrumb.astro | 118 +++++++++--------- .../polymech/src/components/GalleryK.astro | 11 +- .../src/components/global/Footer.astro | 71 ++++++----- packages/polymech/src/components/specs.astro | 48 ++++--- 10 files changed, 235 insertions(+), 165 deletions(-) diff --git a/packages/imagetools_3/api/utils/getFilteredProps.js b/packages/imagetools_3/api/utils/getFilteredProps.js index 9c791ba..8b35a19 100644 --- a/packages/imagetools_3/api/utils/getFilteredProps.js +++ b/packages/imagetools_3/api/utils/getFilteredProps.js @@ -36,8 +36,8 @@ const NonProperties = { }; const ImgProperties = NonGlobalSupportedConfigs.filter( - (key) => !NonProperties.Img.includes(key) - ), + (key) => !NonProperties.Img.includes(key) +), PictureProperties = NonGlobalSupportedConfigs.filter( (key) => !NonProperties.Picture.includes(key) ), @@ -65,14 +65,33 @@ export default function getFilteredProps(type, props) { const { search, searchParams } = new URL(props.src, "file://"); + const paramOptions = Object.fromEntries(searchParams); + + // Separate supported config params from others (like cache busters) + const supportedKeys = SupportedProperties[type] || []; + const configParams = {}; + const otherParams = new URLSearchParams(); + + for (const [key, value] of searchParams) { + if (supportedKeys.includes(key) || GlobalConfigOptions[key]) { + configParams[key] = value; + } else { + otherParams.append(key, value); + } + } + + // Remove ALL params from src initially props.src = props.src.replace(search, ""); - const paramOptions = Object.fromEntries(searchParams); + // Re-append ONLY the non-config params to src + if (otherParams.toString()) { + props.src += `?${otherParams.toString()}`; + } const filteredLocalProps = filterConfigs( type, { - ...paramOptions, + ...configParams, ...props, }, SupportedProperties[type] diff --git a/packages/imagetools_3/api/utils/getSrcset.js b/packages/imagetools_3/api/utils/getSrcset.js index a9337ed..8aad65c 100644 --- a/packages/imagetools_3/api/utils/getSrcset.js +++ b/packages/imagetools_3/api/utils/getSrcset.js @@ -18,24 +18,39 @@ export default async function getSrcset( const params = keys.length ? keys - .map((key) => - Array.isArray(options[key]) - ? `&${key}=${options[key].join(";")}` - : `&${key}=${options[key]}` - ) - .join("") + .map((key) => + Array.isArray(options[key]) + ? `&${key}=${options[key].join(";")}` + : `&${key}=${options[key]}` + ) + .join("") : ""; - const [cleanSrc] = src.split("?"); - const id = `${cleanSrc}?${params.slice(1)}`; + // Extract existing search params from src + const [cleanSrc, search] = src.split("?"); + + // Combine existing params (like s=...) with options + // Existing params come first so they can be overridden by options if needed, + // though 's' should typically be unique. + const searchParams = new URLSearchParams(search); + + // Add options to searchParams + keys.forEach(key => { + const value = Array.isArray(options[key]) + ? options[key].join(";") + : options[key]; + searchParams.set(key, value); + }); + + const id = `${cleanSrc}?${searchParams.toString()}`; // @todo : remove this const fullPath = await getSrcPath(id); const { default: load } = await import("../../plugin/hooks/load.js"); // @ts-ignore let srcset = null - + try { - + const loaded = await load(fullPath, base); if (loaded) { srcset = loaded.slice(16, -1); diff --git a/packages/imagetools_3/components/Picture.astro b/packages/imagetools_3/components/Picture.astro index 737bd77..7b3c6d9 100644 --- a/packages/imagetools_3/components/Picture.astro +++ b/packages/imagetools_3/components/Picture.astro @@ -2,9 +2,18 @@ import renderPicture from "../api/renderPicture.js"; import type { PictureConfigOptions } from "../types.d"; -declare interface Props extends PictureConfigOptions {} +declare interface Props extends PictureConfigOptions { + s?: string; +} -const { link, style, picture } = await renderPicture(Astro.props as Props); +const { s, ...rest } = Astro.props as Props; + +if (s) { + const separator = rest.src.includes("?") ? "&" : "?"; + rest.src = `${rest.src}${separator}s=${s}`; +} + +const { link, style, picture } = await renderPicture(rest); --- diff --git a/packages/imagetools_3/integration/index.js b/packages/imagetools_3/integration/index.js index a20b4f2..d530735 100644 --- a/packages/imagetools_3/integration/index.js +++ b/packages/imagetools_3/integration/index.js @@ -88,7 +88,6 @@ export default { await pMap( [...allAssets.entries()], async ([assetPath, { hash, image, buffer }]) => { - logger.info(`[imagetools] Processing image ${assetPath}...`); try { await saveAndCopyAsset( hash, diff --git a/packages/imagetools_3/plugin/hooks/load.js b/packages/imagetools_3/plugin/hooks/load.js index 9bda1d4..73ac4b5 100644 --- a/packages/imagetools_3/plugin/hooks/load.js +++ b/packages/imagetools_3/plugin/hooks/load.js @@ -26,9 +26,9 @@ export default async function load(id) { const ext = path.extname(id).slice(1); - + if (!supportedImageTypes.includes(ext)) return null; - + const { default: astroViteConfigs } = await import( // @ts-ignore "../../astroViteConfigs.js" @@ -42,9 +42,10 @@ export default async function load(id) { path.relative("", src).split(path.sep).join(path.posix.sep) ); + // Include search params in the hash calculation to force re-processing on change const getHash = (width) => objectHash( - { width, options, rootRelativePosixSrc }, + { width, options, rootRelativePosixSrc, search }, // @ts-ignore { algorithm: "sha256" } ); @@ -56,8 +57,11 @@ export default async function load(id) { const config = Object.fromEntries(searchParams); + // Use the full ID (including search params) + src for the store key + // This ensures that the same file with different params is treated as different source + const storeKey = src + search; const { image: loadedImage, width: imageWidth } = - store.get(src) || store.set(src, await getLoadedImage(src, ext)).get(src); + store.get(storeKey) || store.set(storeKey, await getLoadedImage(src, ext)).get(storeKey); const { type, widths, options, extension, raw, inline } = getConfigOptions( config, @@ -97,16 +101,16 @@ export default async function load(id) { const { image, buffer } = raw ? { - image: sharp ? loadedImage : null, - buffer: !sharp ? loadedImage.data : null, - } + image: sharp ? loadedImage : null, + buffer: !sharp ? loadedImage.data : null, + } : await getTransformedImage({ - src, - image: loadedImage, - config, - type, - }) - + src, + image: loadedImage, + config, + type, + }) + const dataUri = `data:${type};base64,${( buffer || (await getCachedBuffer(hash, image)) ).toString("base64")}` @@ -126,18 +130,18 @@ export default async function load(id) { ); if (!store.has(assetPath)) { - const config = { width, ...options }; + const config = { width, ...options }; // Create cache key for this specific image transformation - const cacheKey = { - src: id, - width, - type, + const cacheKey = { + src: id, + width, + type, extension, - options: objectHash(options) + options: objectHash(options) }; - + let imageObject = null; - + // Only use cache in production builds if (environment === "production") { imageObject = await get_cached_object(cacheKey, 'imagetools-plugin'); @@ -150,15 +154,15 @@ export default async function load(id) { if (!imageObject) { const { image, buffer } = raw ? { - image: sharp && loadedImage, - buffer: !sharp && loadedImage.data, - } + image: sharp && loadedImage, + buffer: !sharp && loadedImage.data, + } : await getTransformedImage({ - src, - image: loadedImage, - config, - type, - }); + src, + image: loadedImage, + config, + type, + }); imageObject = { hash, type, image, buffer }; @@ -184,8 +188,8 @@ export default async function load(id) { const srcset = sources.length > 1 ? sources - .map(({ width, modulePath }) => `${modulePath} ${width}w`) - .join(", ") + .map(({ width, modulePath }) => `${modulePath} ${width}w`) + .join(", ") : sources[0].modulePath; return `export default "${srcset}"`; diff --git a/packages/polymech/src/app/navigation.ts b/packages/polymech/src/app/navigation.ts index 500f4a7..dc8fe1b 100644 --- a/packages/polymech/src/app/navigation.ts +++ b/packages/polymech/src/app/navigation.ts @@ -13,16 +13,16 @@ export const items = async (opts: { locale: string }) => { "class": "hover:text-orange-600" }, { - "href": `/resources/home`, + "href": `/${opts.locale}/resources/home`, "title": _T("Resources"), "ariaLabel": "Resources", "class": "hover:text-orange-600" } ] } -export const footer_left = async ( locale: string ) => { +export const footer_left = async (locale: string) => { const _T = async (text: string) => await translate(text, I18N_SOURCE_LANGUAGE, locale) - return await pMap(config.footer_left, async (item:any) => { + return await pMap(config.footer_left, async (item: any) => { return { "href": `${item.href}`, "title": await _T(item.text), @@ -31,9 +31,9 @@ export const footer_left = async ( locale: string ) => { } }); } -export const footer_right = async ( locale: string ) => { +export const footer_right = async (locale: string) => { const _T = async (text: string) => await translate(text, I18N_SOURCE_LANGUAGE, locale) - return await pMap(config.footer_right, async (item:any) => { + return await pMap(config.footer_right, async (item: any) => { return { "href": `/${item.href}`, "title": await _T(item.text), diff --git a/packages/polymech/src/components/Breadcrumb.astro b/packages/polymech/src/components/Breadcrumb.astro index c1bb8c4..fd131f2 100644 --- a/packages/polymech/src/components/Breadcrumb.astro +++ b/packages/polymech/src/components/Breadcrumb.astro @@ -7,48 +7,53 @@ interface Props { showHome?: boolean; } -const { - currentPath, - collection = '', - title = '', - separator = '/', - showHome = true +const { + currentPath, + collection = "", + title = "", + separator = "/", + showHome = true, } = Astro.props; // Parse the current path to generate breadcrumb items -function generateBreadcrumbs(path: string, collection: string, pageTitle?: string) { - const segments = path.split('/').filter(segment => segment !== ''); - const breadcrumbs: Array<{ label: string; href?: string; isLast?: boolean }> = []; - +function generateBreadcrumbs( + path: string, + collection: string, + pageTitle?: string, +) { + const segments = path.split("/").filter((segment) => segment !== ""); + const breadcrumbs: Array<{ label: string; href?: string; isLast?: boolean }> = + []; + // Add home if enabled if (showHome) { - breadcrumbs.push({ label: 'Home', href: '/' }); + breadcrumbs.push({ label: "Home", href: "/" }); } - + // Build path segments - let currentHref = ''; + let currentHref = ""; segments.forEach((segment, index) => { currentHref += `/${segment}`; const isLast = index === segments.length - 1; - + // Format segment label let label = segment - .split('-') - .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) - .join(' '); - + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(" "); + // Use page title for the last segment if provided if (isLast && pageTitle) { label = pageTitle; } - + breadcrumbs.push({ label, - href: isLast ? undefined : currentHref + '/', - isLast + href: isLast ? undefined : currentHref + "/", + isLast, }); }); - + return breadcrumbs; } @@ -57,28 +62,30 @@ const breadcrumbs = generateBreadcrumbs(currentPath, collection, title); @@ -88,7 +95,7 @@ const breadcrumbs = generateBreadcrumbs(currentPath, collection, title); padding: 0.75rem 0; border-bottom: 1px solid #e5e7eb; } - + .breadcrumb-list { display: flex; flex-wrap: wrap; @@ -98,58 +105,57 @@ const breadcrumbs = generateBreadcrumbs(currentPath, collection, title); padding: 0; list-style: none; } - + .breadcrumb-item { display: flex; align-items: center; gap: 0.25rem; + text-transform: uppercase; } - + .breadcrumb-link { - text-decoration: none; font-size: 0.875rem; padding: 0.25rem 0.5rem; border-radius: 0.375rem; transition: all 0.2s ease; } - + .breadcrumb-link:hover { color: #374151; background-color: #f3f4f6; text-decoration: underline; } - + .breadcrumb-link:focus { outline: 2px solid #3b82f6; outline-offset: 2px; } - + .breadcrumb-current { - font-weight: 500; font-size: 0.875rem; padding: 0.25rem 0.5rem; } - + .breadcrumb-separator { color: #9ca3af; font-size: 0.875rem; user-select: none; } - + /* Mobile responsiveness */ @media (max-width: 640px) { .breadcrumb { padding: 0.5rem 0; } - + .breadcrumb-link, .breadcrumb-current { font-size: 0.8125rem; padding: 0.125rem 0.25rem; } - + .breadcrumb-list { gap: 0.125rem; } diff --git a/packages/polymech/src/components/GalleryK.astro b/packages/polymech/src/components/GalleryK.astro index 166f55c..bfd1c25 100644 --- a/packages/polymech/src/components/GalleryK.astro +++ b/packages/polymech/src/components/GalleryK.astro @@ -10,6 +10,7 @@ interface Image { src: string title?: string description?: string + hash?: string } export interface Props { @@ -29,9 +30,10 @@ export interface Props { SHOW_TITLE?: boolean; SHOW_DESCRIPTION?: boolean; }; + s?: string; } -const { images, gallerySettings = {}, lightboxSettings = {} } = Astro.props; +const { images, gallerySettings = {}, lightboxSettings = {}, s } = Astro.props; const mergedGallerySettings = { SIZES_REGULAR: gallerySettings.SIZES_REGULAR || IMAGE_SETTINGS.GALLERY.SIZES_REGULAR, @@ -46,7 +48,7 @@ const mergedLightboxSettings = { SHOW_DESCRIPTION: lightboxSettings.SHOW_DESCRIPTION ?? IMAGE_SETTINGS.LIGHTBOX.SHOW_DESCRIPTION, }; const locale = Astro.currentLocale || "en"; -console.log(`LGallery Images`, images) + ---
))} @@ -208,6 +213,8 @@ console.log(`LGallery Images`, images) format="avif" objectFit="contain" sizes={IMAGE_SETTINGS.LIGHTBOX.SIZES_LARGE} + sizes={IMAGE_SETTINGS.LIGHTBOX.SIZES_LARGE} + s={s || image.hash} attributes={{ img: { class: "max-w-[90vw] max-h-[90vh] object-contain rounded-lg lightbox-main" } }} diff --git a/packages/polymech/src/components/global/Footer.astro b/packages/polymech/src/components/global/Footer.astro index 8ad237d..463e378 100644 --- a/packages/polymech/src/components/global/Footer.astro +++ b/packages/polymech/src/components/global/Footer.astro @@ -2,10 +2,7 @@ import Wrapper from "@/components/containers/Wrapper.astro"; import { footer_left, footer_right } from "@/app/navigation.js"; import { ISO_LANGUAGE_LABELS } from "@polymech/i18n"; -import { - LANGUAGES_PROD, - I18N_SOURCE_LANGUAGE, -} from "config/config.js"; +import { LANGUAGES_PROD, I18N_SOURCE_LANGUAGE } from "config/config.js"; const locale = Astro.currentLocale || I18N_SOURCE_LANGUAGE; const currentUrl = new URL(Astro.url); @@ -16,7 +13,7 @@ const currentUrl = new URL(Astro.url); * @returns {string[]} The URL path segments without the language code (if present). */ const getCleanPathSegments = (url) => { - const segments = url.pathname.split('/').filter(Boolean); + const segments = url.pathname.split("/").filter(Boolean); if (segments.length && LANGUAGES_PROD.includes(segments[0])) { segments.shift(); } @@ -32,33 +29,32 @@ const getCleanPathSegments = (url) => { const buildLocalizedUrl = (lang, segments) => { const newUrl = new URL(Astro.url); // Prepend the language code and join with existing segments, removing any trailing slash. - newUrl.pathname = `/${lang}/${segments.join('/')}`.replace(/\/+$/, ''); + newUrl.pathname = `/${lang}/${segments.join("/")}`.replace(/\/+$/, ""); return newUrl.toString(); }; const cleanSegments = getCleanPathSegments(currentUrl); -const languages = LANGUAGES_PROD.filter( - (lang) => lang !== locale, -).map((lang) => ({ - lang: ISO_LANGUAGE_LABELS[lang] || lang, - url: buildLocalizedUrl(lang, cleanSegments), -})); +const languages = LANGUAGES_PROD.filter((lang) => lang !== locale).map( + (lang) => ({ + lang: ISO_LANGUAGE_LABELS[lang] || lang, + url: buildLocalizedUrl(lang, cleanSegments), + }), +); const footerLeft = await footer_left(locale); const footerRight = await footer_right(locale); - ---
-
-
-
+
+
+
-
- logo +
+

@@ -93,25 +93,24 @@ const footerRight = await footer_right(locale); logo - -
+
-
+
diff --git a/packages/polymech/src/components/specs.astro b/packages/polymech/src/components/specs.astro index 0fbacf4..25457f2 100644 --- a/packages/polymech/src/components/specs.astro +++ b/packages/polymech/src/components/specs.astro @@ -1,20 +1,25 @@ --- -import * as path from "path" -import { sync as fileExists } from "@polymech/fs/exists" +import * as path from "path"; +import { sync as fileExists } from "@polymech/fs/exists"; -import { specs } from "@/base/specs.js" +import { specs } from "@/base/specs.js"; -import { render, logger } from "@/base/index.js" -import { translateSheets } from "@/base/i18n.js" -import { I18N_SOURCE_LANGUAGE, LANGUAGES, PRODUCT_SPECS } from "config/config.js" +import { createComponent } from "astro/runtime/server/astro-component.js"; +import { renderTemplate, unescapeHTML } from "astro/runtime/server/index.js"; -import DefaultComponent from "./Default.astro" +import { logger } from "@/base/index.js"; +import { translateSheets } from "@/base/i18n.js"; +import { + I18N_SOURCE_LANGUAGE, + LANGUAGES, + PRODUCT_SPECS, +} from "config/config.js"; -const { frontmatter: data } = Astro.props -const locale = Astro.currentLocale || I18N_SOURCE_LANGUAGE +import DefaultComponent from "./Default.astro"; +const { frontmatter: data } = Astro.props; +const locale = Astro.currentLocale || I18N_SOURCE_LANGUAGE; -const specs_table = async (relPath) => -{ +const specs_table = async (relPath) => { let specsPath = path.join(PRODUCT_SPECS(relPath)); if (!fileExists(specsPath)) { logger.debug(`No specs found for ${specsPath}`); @@ -25,19 +30,26 @@ const specs_table = async (relPath) => if (!i18n) { logger.debug(`No i18n found for ${relPath} : ${locale}`); } else { - specsPath = i18n + specsPath = i18n; } } - return specs(specsPath) -} -const tableHTML = await specs_table(data.rel) -let SpecsComponent + return specs(specsPath); +}; + +const render = async (string) => { + const html = `${unescapeHTML(string)}`; + return createComponent(() => renderTemplate(html as any, [])); +}; + +const tableHTML = await specs_table(data.rel); +let SpecsComponent; if (tableHTML) { - SpecsComponent = await render(tableHTML) + SpecsComponent = await render(tableHTML); } else { - SpecsComponent = DefaultComponent + SpecsComponent = DefaultComponent; } --- +