From ecff7b0bbf54e8f0b4ab165884e6e824222069d6 Mon Sep 17 00:00:00 2001 From: Peter Meier Date: Sat, 25 Apr 2026 09:43:34 +0200 Subject: [PATCH] feat(images): cache-bust /cms-images URLs by post._updated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Browser caches /cms-images responses immutably for a year. When the editor changes a focal point or alt text, the underlying transform output changes too, but the URL stays the same — so the browser keeps serving the previous bytes. Manual hard-reloads and /api/cache/clear help once but not for visitors who already have the file. - RustyImageTransformParams gains an optional `version` that serializes as `_v` in the /cms-images URL. - RustyImage.svelte takes a matching `version` prop and forwards it through `baseParams`, so every source carries the tag. - The post thumbnail passes `post._updated` (truncated to seconds) as version. Each save bumps `_updated`, the URL changes, browsers re-fetch. `_v` is intentionally not consumed by `/cms-images` — its sole job is to break the browser cache key. The server-side image-cache and the RustyCMS transform cache key both ignore it, so disk usage doesn't balloon when the version tag changes. --- src/lib/components/RustyImage.svelte | 4 ++++ src/lib/rusty-image.ts | 8 ++++++++ src/routes/post/[slug]/+page.svelte | 1 + 3 files changed, 13 insertions(+) diff --git a/src/lib/components/RustyImage.svelte b/src/lib/components/RustyImage.svelte index f041eb1..bdeb5b0 100644 --- a/src/lib/components/RustyImage.svelte +++ b/src/lib/components/RustyImage.svelte @@ -37,6 +37,8 @@ /** JPEG/AVIF/WebP-Qualität. Default 75. */ quality?: number; fit?: "cover" | "contain" | "fill"; + /** Cache-Bust-Tag (z.B. post._updated) → `_v`-Param, bricht Browser-HTTP-Cache. */ + version?: string; } let { @@ -56,6 +58,7 @@ formats = ["webp", "jpeg"], quality = 75, fit = "cover", + version, }: Props = $props(); function parseAr(s: string | undefined): number | null { @@ -89,6 +92,7 @@ ...(arString && fit !== "contain" ? { height: Math.round(w / (arValue ?? 1)) } : {}), + ...(version ? { version } : {}), }; } diff --git a/src/lib/rusty-image.ts b/src/lib/rusty-image.ts index f374b8a..5e84e0b 100644 --- a/src/lib/rusty-image.ts +++ b/src/lib/rusty-image.ts @@ -68,6 +68,13 @@ export interface RustyImageTransformParams { focalX?: number; /** Focal-Y in [0,1] (Default 0.5). */ focalY?: number; + /** + * Cache-busting tag (e.g. post._updated). Serialized as `_v` and is the + * only param the cms-images endpoint deliberately ignores when forwarding + * to RustyCMS — its sole job is to invalidate the browser HTTP cache when + * the underlying entry (focal, alt, replacement) changes. + */ + version?: string; } function roundFocal(v: number): number { @@ -89,6 +96,7 @@ export function getCmsImageUrl(src: string, params: RustyImageTransformParams = if (params.quality != null) searchParams.set('q', String(params.quality)); if (params.focalX != null) searchParams.set('fx', String(roundFocal(params.focalX))); if (params.focalY != null) searchParams.set('fy', String(roundFocal(params.focalY))); + if (params.version) searchParams.set('_v', params.version); return `/cms-images?${searchParams.toString()}`; } diff --git a/src/routes/post/[slug]/+page.svelte b/src/routes/post/[slug]/+page.svelte index ee2e0f4..b13fbe1 100644 --- a/src/routes/post/[slug]/+page.svelte +++ b/src/routes/post/[slug]/+page.svelte @@ -45,6 +45,7 @@ class="block object-cover" loading="eager" fetchpriority="high" + version={(data.post as { _updated?: string })._updated?.slice(0, 19)} /> {:else if data.postImageUrl}