image utils | cache | components

This commit is contained in:
babayaga 2025-12-25 02:08:14 +01:00
parent ecde90f89d
commit c00cae6fde
10 changed files with 235 additions and 165 deletions

View File

@ -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]

View File

@ -26,8 +26,23 @@ export default async function getSrcset(
.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");

View File

@ -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);
---
<Fragment set:html={link + style + picture} />

View File

@ -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,

View File

@ -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,

View File

@ -13,7 +13,7 @@ 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"

View File

@ -9,33 +9,38 @@ interface Props {
const {
currentPath,
collection = '',
title = '',
separator = '/',
showHome = true
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) {
@ -44,8 +49,8 @@ function generateBreadcrumbs(path: string, collection: string, pageTitle?: strin
breadcrumbs.push({
label,
href: isLast ? undefined : currentHref + '/',
isLast
href: isLast ? undefined : currentHref + "/",
isLast,
});
});
@ -57,7 +62,8 @@ const breadcrumbs = generateBreadcrumbs(currentPath, collection, title);
<nav class="breadcrumb" aria-label="Breadcrumb navigation">
<ol class="breadcrumb-list">
{breadcrumbs.map((crumb, index) => (
{
breadcrumbs.map((crumb, index) => (
<li class="breadcrumb-item">
{crumb.href ? (
<a
@ -78,7 +84,8 @@ const breadcrumbs = generateBreadcrumbs(currentPath, collection, title);
</span>
)}
</li>
))}
))
}
</ol>
</nav>
@ -103,10 +110,10 @@ const breadcrumbs = generateBreadcrumbs(currentPath, collection, title);
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;
@ -126,7 +133,6 @@ const breadcrumbs = generateBreadcrumbs(currentPath, collection, title);
}
.breadcrumb-current {
font-weight: 500;
font-size: 0.875rem;
padding: 0.25rem 0.5rem;

View File

@ -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)
---
<div
@ -138,6 +140,8 @@ console.log(`LGallery Images`, images)
format="avif"
placeholder="blurred"
sizes={mergedGallerySettings.SIZES_REGULAR}
sizes={mergedGallerySettings.SIZES_REGULAR}
s={s || image.hash}
attributes={{
img: { class: "main-image p-4 rounded-lg max-h-[60vh] aspect-square" }
}}
@ -180,6 +184,7 @@ console.log(`LGallery Images`, images)
}
}}
loading="lazy"
s={s || image.hash}
/>
</button>
))}
@ -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" }
}}

View File

@ -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,31 +29,30 @@ 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) => ({
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);
---
<Wrapper variant="standard" class="py-12">
<footer class="py-2">
<div class="p-4 xl:pb-0 bg-white dark:bg-gray-800 overflow-hidden rounded-xl">
<div class="grid md:grid-cols-3 gap-6">
<div
class="flex flex-col h-full justify-between xl:pb-2 order-last md:order-none"
class="p-4 xl:pb-0 bg-white dark:bg-gray-800 overflow-hidden rounded-xl"
>
<div class="grid grid-cols-2 md:grid-cols-3 gap-6">
<div class="flex flex-col gap-4 h-full justify-between xl:pb-2">
<nav role="navigation">
<ul class="text-xs space-y-1 uppercase dark:text-gray-400">
{
@ -69,20 +65,24 @@ const footerRight = await footer_right(locale);
))
}
</ul>
<div class="mt-2">
<div
class="flex flex-col space-y-1 text-xs uppercase dark:text-gray-400"
>
{
languages.map((link) => (
<span>
<a class=" hover:text-orange-500 p-2 dark:text-gray-400" href={link.url}>
<a class="hover:text-orange-500 block" href={link.url}>
{link.lang}
</a>
</span><br/>
))
}
</div>
</nav>
<div class="flex gap-4 mt-12 items-center">
<img src="/logos/transparent.svg" alt="logo" class="size-4" />
<div class="flex gap-4 mt-8 md:mt-12 items-center">
<img
src="/logos/transparent.svg"
alt="logo"
class="size-4 hidden md:block"
/>
<p
class="text-xs leading-5 text-neutral-400 dark:text-gray-500 text-pretty uppercase"
>
@ -93,11 +93,10 @@ const footerRight = await footer_right(locale);
<img
src="/logos/transparent.svg"
alt="logo"
class="size-12 md:mx-auto fill-orange-600"
class="size-12 md:mx-auto fill-orange-600 hidden md:block"
/>
<div class="flex flex-col h-full justify-between md:text-right xl:pb-2">
<div class="flex flex-col h-full justify-between text-right xl:pb-2">
<nav role="navigation">
<ul class="text-xs space-y-1 uppercase dark:text-gray-400">
{

View File

@ -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;
}
---
<div class="bg-white rounded-xl specs specs-table">
<SpecsComponent />
</div>