# 0046. Photographs are JPEG with a WebP twin, rendered through Picture::tag()

- **Status:** Accepted
- **Date:** 2026-09-24

## Context

Versions were encoded at quality 100 in the original's format, no WebP, no density variants; the home page hero
was served as the original PNGs (300 KB and 1 MB), and card images upscaled on phones.

## Decision

Molino's pipeline (its ADRs 0044, 0048, 0050, 0056, 0058): `Picture::DEFAULT_QUALITY` 82; a version's
`format => 'jpg'` stores it as JPEG (flattened on white) and also writes a WebP twin; `retina` derives a
double-size key (`…2`). Here the markup is one call, `Picture::tag( $version, $options )`: `<picture>` with the
WebP source, `srcset` (densities, or widths with `sizes`), the real output `width`/`height` of the version
(`getVersionDimensions()`, since a `contain` version keeps the original's ratio), lazy unless `priority`.
`picture { display: contents }`. The hero is a single `<picture>` switching crops by media query. The location
card has its own 240² version with `sizes`. `Wysiwyg::render()` writes the size of editor images that lack one.
`./yii files/regenerate-pictures [--warm]` refreshes what is on disk.

## Alternatives

- **`Accept`-based content negotiation.** No markup change, and `Vary: Accept` on every image.
- **AVIF.** Smaller, and an order of magnitude slower to encode on a shared host.

## Consequences

- Every photographic version exists twice on disk. Deploying a change to a version needs
  `files/regenerate-pictures`.
- A photo rendered without `tag()` silently stays JPEG-only.
- The card's `sizes` mirrors `location-widget.scss`; a breakpoint change there has to follow.
