From ccce537ae41815e57149b2e09d377b9fc2bad371 Mon Sep 17 00:00:00 2001 From: Claude Opus 5 Date: Sat, 1 Aug 2026 23:18:59 +0600 Subject: [PATCH] tell the browser how wide a picture will be MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit srcset without sizes means a browser assumes 100vw, so a gallery thumbnail in a 16rem column was fetching the 1600px original — the derivative pass was costing bandwidth on the page it exists to save it on. It now picks the 480w variant. sizes is the one part of responsive images the engine cannot supply: it states how wide the picture will *be*, which is a fact about the layout and therefore the theme's. The reference theme states its own measure and nothing more. Lazy loading goes on gallery entries and not on a figure, because a figure is often the first thing on the page and deferring it delays what the reader came for. Both have demo cases, including that the figure is *not* deferred. Queue entry G2, which had been sitting unfixed through the whole Markdown arc. --- docs/decisions.md | 17 +++++++++++++++++ docs/state.md | 2 +- docs/surface.md | 2 +- docs/theme-contract.md | 7 +++++++ internal/render/templates/shortcodes.html | 8 ++++++-- internal/web/example_test.go | 4 ++++ 6 files changed, 36 insertions(+), 4 deletions(-) diff --git a/docs/decisions.md b/docs/decisions.md index 97cbc11..eee87c9 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -1112,3 +1112,20 @@ Consequence: cheap — five phrase keys, and a fragment can now say anything the every call node carries a language it mostly does not use, and a theme adding a *new* word of its own still cannot add a phrase key, which waits for the settings cascade exactly as it did before. Revisit if: a site wants to override a phrase, which is the cascade and not this. + +## ADR-0068 — `sizes` belongs to the theme, beside the layout it describes +Date: 2026-08-01 · Status: accepted +Decision: the reference theme's `figure` and `gallery` fragments emit `sizes` alongside `srcset`, and a +gallery entry also carries `loading="lazy"` and `decoding="async"`. The values describe the reference +stylesheet — a 32rem measure, two gallery columns above 36rem — and a theme that changes the layout changes +them with it. The engine supplies widths and dimensions and says nothing about `sizes`. +Why: without it a browser assumes `100vw`, so a gallery thumbnail in a 16rem column fetched the 1600px +original — the derivative pass was costing bandwidth on exactly the page it exists to save it on. `sizes` is +the one part of responsive images that cannot be computed from the picture: it is a statement about where +the picture sits, which only the layout knows, which is the theme (ADR-0046). Lazy loading goes on gallery +entries and not on a figure, because a figure is often the first thing on the page and deferring it delays +what the reader came for. +Consequence: cheap — five attributes, no engine change, and the derivative pass finally pays off. Expensive +— a theme that redefines these fragments and forgets `sizes` silently returns to the old behaviour, and +nothing can check that, since only the theme knows its own measure. +Revisit if: the engine ever learns a layout, which it should not. diff --git a/docs/state.md b/docs/state.md index afe0d32..8d8e22a 100644 --- a/docs/state.md +++ b/docs/state.md @@ -24,7 +24,7 @@ table owns. | `internal/render/render.go` | goldmark with the typographer, per-kind template sets with site override, the render methods. The parsed sets plus the stylesheet are one snapshot behind an `atomic.Pointer`; `Refresh` is the only thing that replaces it, so every page serves one theme (ADR-0055, ADR-0056). Heading ids are a parser option set here, declared or derived (ADR-0058, ADR-0066), and this is the one renderer that enables raw HTML (ADR-0060). `Compose` is the seam a merging bundle's splice arrives through | | `internal/render/view.go` | the theme contract in Go, and now actually all of it: `Page`, `List`, `Sequence`, `Extras`, `Item`, `Partial`, `Fragment` (with `Body`, `Headings` and `Lang` — ADR-0064, ADR-0065, ADR-0067), `Heading`, `Picture`, `Origin` | | `internal/render/chrome.go` | the engine's own words: phrase table, month names, digits, and the `t`/`num`/`day` template funcs (ADR-0034), including the words a shortcode fragment supplies when the author gives none (ADR-0067) | -| `internal/render/templates/` | reference theme, complete (six icon names map to Unicode, no assets — ADR-0063): `base.html` (shell, navigation, language links, feed and OpenGraph), `page.html` (bundle, sequence, tags, extras), `list.html`, `extras.html`, `shortcodes.html` (figure, gallery, icon, note/warn/tip, details, aside, toc), `theme.css` (ADR-0026, ADR-0049) | +| `internal/render/templates/` | reference theme, complete (six icon names map to Unicode, no assets — ADR-0063): `base.html` (shell, navigation, language links, feed and OpenGraph), `page.html` (bundle, sequence, tags, extras), `list.html`, `extras.html`, `shortcodes.html` (figure, gallery, icon, note/warn/tip, details, aside, toc — with the `sizes` its own layout implies, ADR-0068), `theme.css` (ADR-0026, ADR-0049) | | `internal/ext/shortcodes/` | first feature: `::name{key=value}` block parser and node renderer, rendering through a theme fragment (ADR-0036). `figure`, `gallery`, `include`, plus the derivative pass and remembered picture inspection (ADR-0042, ADR-0044). `FootnotePrefix` namespaces an included file's footnote ids (ADR-0058). Directive syntax since ADR-0059, plus `icons.go`: `:name:` inline, rendered by the theme's one `icon` fragment (ADR-0063), `containers.go`: `:::name{…}` … `:::` wrapping a rendered body (ADR-0064), and `toc.go`: the document's headings for a `::toc` call, to a depth the call may set (ADR-0065, ADR-0066). `Merge` splices includes before the parse for a bundle that asks for it (ADR-0066) | | `internal/ext/notation/` | the inline marks CommonMark lacks: `~sub~`, `^sup^`, `==mark==`, and `~~strike~~`, which it owns so a single tilde can mean subscript (ADR-0061). `abbr.go` adds `*[TERM]:` definitions and the pass that expands them (ADR-0062) | | `internal/ext/scaffold/` | writes one draft directory bundle into a site root through `os.Root`: never an overwrite | diff --git a/docs/surface.md b/docs/surface.md index af3c071..b9e9d82 100644 --- a/docs/surface.md +++ b/docs/surface.md @@ -318,7 +318,7 @@ chrome.go 115 · render.go 449 · view.go 185 - view.go:160 type Picture struct - view.go:177 type Origin struct -## internal/web — 734 lines + 1627 test +## internal/web — 734 lines + 1631 test asset.go 58 · discover.go 71 · extras.go 93 · feed.go 125 · resolve.go 170 · web.go 217 diff --git a/docs/theme-contract.md b/docs/theme-contract.md index 922f97b..4442bb5 100644 --- a/docs/theme-contract.md +++ b/docs/theme-contract.md @@ -102,6 +102,13 @@ Each picture carries: | `.Srcset` | the generated widths, closed by the original at its own width; empty when nothing was worth generating, or when the format has no decoder | | `.Width`, `.Height` | the original's intrinsic size, for reserving the box; zero when the file could not be read | +**A theme emitting `.Srcset` must emit `sizes` too.** The engine cannot supply it: `sizes` says how wide the +picture will *be*, which is a fact about the layout and therefore the theme's (ADR-0068). Without it a browser +assumes `100vw` and fetches the widest variant for a thumbnail, which costs more bandwidth than serving no +derivatives at all. The reference theme states its own measure — 32rem for a figure, 16rem for a gallery +column above 36rem — and adds `loading="lazy"` to gallery entries but never to a figure, which is often the +first thing on the page. + | Shortcode | Template | Receives | |---|---|---| | `::figure{src=… alt="…" caption=…}` | `figure` | `.Args.src`, `.Args.alt`, `.Args.caption` | diff --git a/internal/render/templates/shortcodes.html b/internal/render/templates/shortcodes.html index 3e7fb72..9165c7f 100644 --- a/internal/render/templates/shortcodes.html +++ b/internal/render/templates/shortcodes.html @@ -1,7 +1,11 @@ +{{/* `sizes` is layout knowledge, so it belongs to whoever wrote the layout. Without it a browser assumes + 100vw and fetches the widest variant for a thumbnail, which makes the derivative pass cost bandwidth + rather than save it. These values describe *this* stylesheet — a 32rem measure, two gallery columns + above 36rem — and a theme that changes the layout must change them with it (ADR-0068). */}} {{define "figure" -}}
{{- range .Pictures}} -{{$.Args.alt}} +{{$.Args.alt}} {{- end}} {{- if not .Pictures}} {{.Args.alt}} @@ -16,7 +20,7 @@ {{if .Pictures -}} {{- end}} diff --git a/internal/web/example_test.go b/internal/web/example_test.go index a75c894..b18f08b 100644 --- a/internal/web/example_test.go +++ b/internal/web/example_test.go @@ -116,6 +116,10 @@ var exampleFeatures = []featureCase{ expect: []string{"First Rain", "The Flood", "Aftermath", "What Remained"}}, {what: "a chapter has neighbours, a position, and the ends", path: "/comics/the-long-monsoon/the-flood/", code: 200, expect: []string{`rel="prev"`, `rel="next"`, "2 of 4", `class="ends"`}}, + {what: "generated widths come with a sizes hint, and thumbnails load lazily", path: "/art/monsoon-studies/", code: 200, + expect: []string{`sizes="(min-width: 36rem) 16rem, 100vw"`, `loading="lazy"`, `decoding="async"`}}, + {what: "a lead figure is sized for the measure and never deferred", path: "/posts/first-light/", code: 200, + expect: []string{`sizes="(min-width: 36rem) 32rem, 100vw"`}, absent: []string{`loading="lazy"`}}, {what: "a gallery sizes what it can and leaves alone what it cannot", path: "/art/monsoon-studies/", code: 200, expect: []string{`class="gallery"`, "10-grey.jpg", "srcset=", `src="40-line.svg"`}, absent: []string{`src="40-line.svg" srcset`}},