Responsive Images

Introduction

ReeWeb generates responsive image variants during static site generation from committed originals, so you never commit the resized/re-encoded files. You drop a full-size image into assets/images/, and the SSG script produces width-stepped WebP + JPEG variants that the <responsive-image> component serves through a <picture> element. Only originals live in git; every variant is a generated artifact.

The pieces:

PieceRole
assets/images/Committed originals (outside src/public, never served)
scripts/prepare_images.tsImage generator for SSG (uses Bun.Image, zero deps)
src/public/images/responsive/Generated variants (git-ignored, served)
config/responsive_images.tsWidths + quality - the single source of truth
lib/images.tssrcset() / webp() / jpeg() URL helpers
<responsive-image>Renders a <picture> from those helpers

Adding an image

  1. Put the original in assets/images/ (e.g. assets/images/hero.png).
  2. Run SSG (or bun prepare:images). Variants land in src/public/images/responsive/:
src/public/images/responsive/
  hero.png  hero.webp                 full-size recode
  300/  hero.png  hero.webpwidth variants - resized by width,
  500/  …                             │ aspect preserved, never upscaled
  800/  …                             ┘
  1440/ …

Sub-folders under assets/images/ are preserved in the output.

The <responsive-image> component

Pass a src pointing at the original's served path; the component builds the <picture> srcset for you:

{{ props.src = "/images/responsive/hero.png"; props.alt = "Our team"; }}
<responsive-image
    loading="eager"
    sizes="(max-width: 1024px) 100vw, 640px"
    image_class="h-full w-full object-cover"
></responsive-image>

Props: src, alt, class (the <picture>), image_class (the <img>), sizes, and loading (defaults to lazy). The browser downloads the smallest variant that satisfies sizes.

Configuration

Widths and quality are defined once in config/responsive_images.ts:

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

Both the generator and the component's srcset read this file, so they never drift - change a width here and the generated files and the markup update together. Override per-SSG-run with --widths / --quality* flags.

Why no AVIF (yet)

Bun.Image uses the OS-native codec, which has no AV1 encoder on many platforms (Intel macOS, typical Linux CI), so AVIF is not generated. WebP (~97% browser support) plus a JPEG fallback cover it. The component keeps an AVIF <source> commented out - <picture> does not fall back when a chosen source 404s, so a live AVIF source with no AVIF files would show broken images. Re-enable both the component line and AVIF generation together if you adopt an AVIF-capable encoder.

SSG wiring

prepare:images runs before bun ssg (blocking) and concurrently in dev (so the server starts instantly). The script is incremental: warm runs are near-instant, and editing one original rebuilds only that image. See the Scripts Reference for the exact commands.