generate sized derivatives ahead of the request

A pass over the content at startup writes three widths per picture into a cache
outside the site root, named by the source's content hash and the width (ADR-0042).
Idempotent by construction: a rerun stats and skips, an edited picture takes a new
name, and nothing stale can be served under an old one. Restarting the evidence
site made 0 derivatives the second time, as it should.

Ahead of the request rather than during it, because resampling is felt and there is
no page cache yet to hide it. Outside the site root, because the engine reads that
directory and must not leave generated files in somebody's content git — a lost
cache costs one startup pass and no correctness.

Markup now carries the original as src, the derivatives as srcset closed by the
original at its own width, and width/height from the original — which retires most
of the latent row about the output floor; only a gallery's alt is still empty, and
a filename cannot supply that.

Two things the work itself decided:

`Fragment.Items` became `Fragment.Pictures`, ADR-0037's own revisit trigger. Items
had one consumer, so widening it beat adding a second list beside it.

"A browser can show it" and "we can resample it" are different questions, and
conflating them nearly deleted content: an SVG has no decoder here, so a single
predicate would have dropped SVGs from galleries silently. Undecodable and
unsupported pictures are now rendered as they are, without a size or a srcset.
This commit is contained in:
2026-08-01 02:23:36 +06:00
parent 8dc6746684
commit 7da2a58fd5
18 changed files with 502 additions and 59 deletions
+25
View File
@@ -550,3 +550,28 @@ going there: if they land in core, `EXT_LOC` measures nothing and invariant 9 be
the whole reason there are two ceilings.
Revisit if: core approaches 2800. That is the question "what here is not core?" and the answer is a leaf, not
a third raise.
## ADR-0042 — Derivatives are generated ahead of the request, into a cache outside the site root
Date: 2026-07-30 · Status: accepted (replaces `Fragment.Items` with `Fragment.Pictures`, ADR-0037's own
revisit trigger)
Decision: sized image derivatives are produced by a pass over the content at startup, not during a request,
and written to a cache directory outside the site root (`-cache`, defaulting under `os.UserCacheDir`). Each is
named by the source's content hash and the target width, so the pass is idempotent and a changed source yields
a different name. The engine never writes into the site root and never touches an original. Core serves that
cache as a directory of opaque names; every image decision — widths, naming, dimensions, which files are
images — lives in `internal/ext/shortcodes`, the package whose shortcodes need it. A fragment now receives
`Pictures`, each carrying `Src`, `Srcset`, `Width` and `Height`, replacing the bare `Items` list.
Why: resampling on the request path would make the first view of a page take seconds, and there is no page
cache yet to hide it. Writing derivatives into the site root would put generated files in somebody's content
git — the engine reads that directory and must not litter it, and derived state is disposable by definition
(ADR-0010). Content-addressed names mean a rebuild rewrites nothing, and a lost cache costs one startup pass
rather than any correctness. `Items` had exactly one consumer, so widening it in place beat adding a second
list beside it.
Consequence: cheap — no request pays for resampling, the cache can be deleted at any time, and `width`/`height`
in the markup end the layout-shift problem the output floor named. Expensive — a new image needs a restart
until change detection lands (queue 21), the cache is a second directory to think about when deploying, and
AVIF passes through unresized since nothing can decode it. A feature still cannot serve a route of its own, so
core carries a generic "serve this directory of derived files" — which is the seam to revisit when a second
feature wants output of its own.
Revisit if: startup time becomes noticeable on a large site — then the pass wants a manifest and a change
check rather than a stat per candidate.