diff --git a/src/lib/image-cache.server.ts b/src/lib/image-cache.server.ts index cf35d6b..450e05a 100644 --- a/src/lib/image-cache.server.ts +++ b/src/lib/image-cache.server.ts @@ -29,6 +29,7 @@ export interface ImageTransformParams { ar?: string; fx?: number; fy?: number; + dpr?: number; } const memoryCache = new Map(); @@ -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); } diff --git a/src/routes/cms-images/+server.ts b/src/routes/cms-images/+server.ts index 2a2f60f..f3752b2 100644 --- a/src/routes/cms-images/+server.ts +++ b/src/routes/cms-images/+server.ts @@ -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()}`;