feat(cms-images): forward dpr + format=auto to RustyCMS
Deploy / verify (push) Successful in 40s
Deploy / deploy (push) Successful in 1m2s

- Accept dpr query param (clamped 1..4) and include it in cache key so
  each density gets its own cached file.
- Accept format=auto and negotiate locally from the client's Accept
  header (avif > webp > jpeg) before building the cache key — concrete
  formats in the cache avoid cross-client leaks.
This commit is contained in:
Peter Meier
2026-04-23 13:36:38 +02:00
parent 50bb0ef398
commit 724b75b6bf
2 changed files with 27 additions and 2 deletions
+2
View File
@@ -29,6 +29,7 @@ export interface ImageTransformParams {
ar?: string;
fx?: number;
fy?: number;
dpr?: number;
}
const memoryCache = new Map<string, CachedImage>();
@@ -60,6 +61,7 @@ export function buildCacheKey(src: string, params: ImageTransformParams): string
if (params.ar) parts.push(`ar=${params.ar}`);
if (params.fx != null) parts.push(`fx=${roundFocal(params.fx)}`);
if (params.fy != null) parts.push(`fy=${roundFocal(params.fy)}`);
if (params.dpr != null) parts.push(`dpr=${roundFocal(params.dpr)}`);
return createHash('sha256').update(parts.join('|')).digest('hex').slice(0, 16);
}
+25 -2
View File
@@ -31,7 +31,19 @@ function resolveSourceUrl(src: string): string {
return `${getCmsBaseUrl()}/api/assets/${encodeURIComponent(src)}`;
}
export const GET: RequestHandler = async ({ url }) => {
/**
* Pick output format based on client Accept header.
* Priority: avif > webp > jpeg. Used when `format=auto` is requested so the
* cache key binds to a concrete format (no cross-client leaks).
*/
function negotiateFormat(accept: string | null): 'avif' | 'webp' | 'jpeg' {
const a = (accept ?? '').toLowerCase();
if (a.includes('image/avif')) return 'avif';
if (a.includes('image/webp')) return 'webp';
return 'jpeg';
}
export const GET: RequestHandler = async ({ url, request }) => {
const src = url.searchParams.get('src');
if (!src) {
return new Response('Missing "src" parameter', { status: 400 });
@@ -46,6 +58,7 @@ export const GET: RequestHandler = async ({ url }) => {
const ar = url.searchParams.get('ar');
const fx = url.searchParams.get('fx');
const fy = url.searchParams.get('fy');
const dpr = url.searchParams.get('dpr');
if (w) {
const n = Number(w);
@@ -59,7 +72,12 @@ export const GET: RequestHandler = async ({ url }) => {
const n = Number(q);
if (Number.isFinite(n)) params.q = n;
}
if (format) params.format = format;
// format=auto is resolved locally so the cache key binds to a concrete format.
if (format === 'auto') {
params.format = negotiateFormat(request.headers.get('accept'));
} else if (format) {
params.format = format;
}
if (fit) params.fit = fit;
if (ar) params.ar = ar;
if (fx) {
@@ -70,6 +88,10 @@ export const GET: RequestHandler = async ({ url }) => {
const n = Number(fy);
if (Number.isFinite(n)) params.fy = n;
}
if (dpr) {
const n = Number(dpr);
if (Number.isFinite(n)) params.dpr = Math.min(4, Math.max(1, n));
}
const key = buildCacheKey(src, params);
@@ -92,6 +114,7 @@ export const GET: RequestHandler = async ({ url }) => {
if (params.ar) transformParams.set('ar', params.ar);
if (params.fx != null) transformParams.set('fx', String(params.fx));
if (params.fy != null) transformParams.set('fy', String(params.fy));
if (params.dpr != null) transformParams.set('dpr', String(params.dpr));
const transformUrl = `${cmsBase}/api/transform?${transformParams.toString()}`;