Responsive Images Config

config/responsive_images.ts is the single source of truth for responsive images. It defines the width breakpoints, encoder quality settings, and screenshot-specific behaviour. Two consumers read this file so they never drift — change a value here and both the generated image variants and the <responsive-image> markup update together.

ConsumerRole
scripts/prepare_images.tsGenerates width-stepped WebP + JPEG variants at build time
lib/images.tssrcset() and webp()/jpeg() helpers for <picture> markup

Configuration File

export const responsive_widths = [300, 500, 800, 1440] as const;
export const responsive_quality = { webp: 80, jpeg: 80 } as const;
export const screenshot_quality = { jpeg: 85 } as const;

responsive_widths

export const responsive_widths = [300, 500, 800, 1440] as const;

Width breakpoints in pixels. For every image, the generator produces one variant at each width (plus a full-size recode). These same widths are fed to the srcset() helper to build the <source srcset="… 300w, … 500w, … 800w, … 1440w"> attributes that let the browser pick the best size.

Variants are never upscaled: a breakpoint larger than a given original's native width is clamped to that original's width. If an image is only 640px wide, the 800px and 1440px breakpoints are skipped for it — only the 300px, 500px, and full‑size variants are produced.

To adjust widths per project, edit the array here. CLI overrides (--widths 300,600,900,1200) override it at build time, but the srcset() helper always reads from this file, so a permanent width change should be made here to keep markup and files in sync.

responsive_quality

export const responsive_quality = { webp: 80, jpeg: 80 } as const;

Per-format encoder quality for photographic and WebP sources (JPEG, .jpg, .jpeg, .webp originals). Both values range from 1–100.

FieldDescription
webpQuality for WebP output — the primary format browsers download (~97% support).
jpegQuality for JPEG output — the <img src> universal fallback.

CLI overrides: --quality 75 sets both formats. --quality-webp 75 and --quality-jpeg 82 override individually. These flags only affect the generator (prepare_images.ts) — the srcset() helper uses the file's widths but not its quality.

PNG Sources — Screenshot Treatment

PNG originals receive different treatment because they're assumed to be screenshots: flat UI colour and sharp text that lossy DCT compression would smear.

When the source extension is .png and no --quality* CLI flag has been set:

  • WebP is encoded losslessly. The webp quality value is ignored — Bun.Image's { lossless: true } option is used instead. This preserves pixel‑perfect text and UI elements.
  • JPEG uses screenshot_quality.jpeg (85) instead of responsive_quality.jpeg (80). The <img src> fallback cannot be lossless, so it gets a higher‑quality JPEG to minimise smearing.

When a CLI quality flag is set (--quality, --quality-webp, or --quality-jpeg), the explicit override takes precedence and the PNG‑routing logic is bypassed — every source, including PNGs, uses the user‑supplied quality values.

screenshot_quality

export const screenshot_quality = { jpeg: 85 } as const;

Quality override applied to the JPEG fallback of .png sources (see PNG Sources — Screenshot Treatment above). Only jpeg is defined here because the WebP side of a PNG source is handled as lossless, not quality‑driven.

FieldDefaultDescription
jpeg85JPEG quality for the <img src> fallback of PNGs.

Output Layout

The generator writes variants into src/public/images/responsive/ with this structure:

src/public/images/responsive/
  hero.png               full‑size recode, JPEG bytes (source extension kept)
  hero.webp              full‑size recode, WebP
  300/
    hero.png             300px‑wide variant, JPEG bytes
    hero.webp            300px‑wide variant, WebP
  500/
    hero.png
    hero.webp
  800/
    hero.png
    hero.webp
  1440/
    hero.png
    hero.webp

Sub‑folders under assets/images/ are preserved in the output. The width sub‑directories are named after the breakpoints in this config — changing responsive_widths changes the directory names and the srcset descriptor values together.

JPEG Extension Quirk

JPEG fallback files keep the source extension: hero.png contains JPEG bytes, and hero.jpg stays as hero.jpg. Only .webp sources get a .jpg JPEG fallback. This preserves backwards compatibility with the webp()/jpeg() URL helpers and the <responsive-image> component's <img src> attribute, which points at the source‑extension path.

Dual‑Consumer Design

The shared config eliminates the drift that occurs when a width list is defined separately in the generator and the template layer:

config/responsive_images.ts        ← single source of truth
         │
         ├── scripts/prepare_images.ts   ← reads widths + quality at build time
         │
         └── lib/images.ts               ← reads widths at render time for srcset
                │
                └── <responsive-image>   ← renders <picture> with correct candidates

Change responsive_widths in one place, re‑run bun prepare:images, and the component automatically references the new breakpoints. No manual sync required.

CLI Overrides

The generator (bun prepare:images) accepts flags that temporarily override the config values without editing the file. These are useful for one‑off builds or experimenting with quality settings.

FlagDefault (from config)Description
--widths <list>responsive_widthsComma‑separated breakpoints, e.g. 300,600,900
--quality <n>responsive_qualitySets both WebP and JPEG quality
--quality-webp <n>responsive_quality.webpOverride WebP only
--quality-jpeg <n>responsive_quality.jpegOverride JPEG only
--forceoffRe‑encode even when outputs are up‑to‑date

When a --quality* flag is used, the PNG screenshot routing is bypassed and all sources use the explicit values — this is how the generator distinguishes "user explicitly set quality" from "no flag, use PNG‑aware defaults."