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`}},