All files / src/utils cdnImage.ts

96.96% Statements 32/33
96.87% Branches 31/32
100% Functions 6/6
100% Lines 22/22

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96                                                        13x       13x     219x   373x                               226x 223x     221x 221x   2x     219x 219x 205x   203x 203x 203x   203x                         31x   29x   60x 60x   60x   29x    
/**
 * CDN image helpers
 *
 * The BATbern content CDN (cdn.*.batbern.ch) is fronted by an image-resize
 * Lambda@Edge (`infrastructure/lib/lambda/image-resize/index.ts`) that, when a
 * `w` and/or `h` query param is present, resizes the origin object with `sharp`,
 * converts it to WebP (quality 80), and returns it with
 * `Cache-Control: public, max-age=31536000, immutable`. Without `w`/`h` the
 * Lambda passes the request through unchanged.
 *
 * These helpers append the resize params to CDN raster URLs so callers stop
 * shipping full-resolution PNG/JPEG originals to the browser. Non-CDN URLs,
 * relative paths, and vector (SVG) assets are returned untouched.
 */
 
export type CdnFit = 'cover' | 'contain' | 'fill' | 'inside' | 'outside';
 
export interface CdnImageOptions {
  /** Target width in CSS pixels (request 2× the display size for retina). */
  w?: number;
  /** Target height in CSS pixels. */
  h?: number;
  /** Resize fit mode. Defaults to the Lambda's own default (`cover`) when omitted. */
  fit?: CdnFit;
}
 
// Mirror of MAX_DIM in the Lambda — requesting larger is clamped server-side
// anyway, but clamping here keeps generated URLs honest and cache-friendly.
const MAX_DIM = 2000;
 
// Raster formats sharp can decode and re-encode to WebP. SVGs are intentionally
// excluded: logos should stay vector (crisp at any size, already tiny).
const RASTER_RE = /\.(png|jpe?g|webp|avif|tiff?|gif)$/i;
 
/** The resize Lambda only lives on the content CDN host (cdn.*.batbern.ch). */
const isResizableCdnHost = (hostname: string): boolean => /(^|\.)cdn\./i.test(hostname);
 
const clampDim = (value: number): number => Math.min(Math.max(Math.round(value), 1), MAX_DIM);
 
/**
 * Append `?w=&h=&fit=` to a CDN raster URL so it is served as a resized WebP.
 *
 * Returns the input unchanged when:
 * - it is empty / not a string,
 * - neither `w` nor `h` is provided (the Lambda would pass through anyway),
 * - it is a relative path or non-http(s) URL,
 * - its host is not the content CDN, or
 * - it does not point at a raster image (e.g. an SVG).
 */
export function buildCdnImageUrl(
  url: string | null | undefined,
  { w, h, fit }: CdnImageOptions = {}
): string | null | undefined {
  if (!url) return url;
  if (!w && !h) return url;
 
  let parsed: URL;
  try {
    parsed = new URL(url);
  } catch {
    return url; // relative path (e.g. /logo.svg) or malformed — leave it alone
  }
 
  Iif (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return url;
  if (!isResizableCdnHost(parsed.hostname)) return url;
  if (!RASTER_RE.test(parsed.pathname)) return url;
 
  if (w) parsed.searchParams.set('w', String(clampDim(w)));
  if (h) parsed.searchParams.set('h', String(clampDim(h)));
  if (fit) parsed.searchParams.set('fit', fit);
 
  return parsed.toString();
}
 
/**
 * Build a `srcset` of WebP variants at the given widths for responsive images
 * (e.g. a full-bleed hero). Returns `undefined` when the URL is not a
 * resizable CDN image, so callers can fall back to a plain `src`.
 */
export function buildCdnImageSrcSet(
  url: string | null | undefined,
  widths: number[],
  { fit }: Pick<CdnImageOptions, 'fit'> = {}
): string | undefined {
  if (!url || widths.length === 0) return undefined;
 
  const entries = widths
    .map((w) => {
      const resized = buildCdnImageUrl(url, { w, fit });
      return resized && resized !== url ? `${resized} ${clampDim(w)}w` : null;
    })
    .filter((entry): entry is string => entry !== null);
 
  return entries.length > 0 ? entries.join(', ') : undefined;
}