tell the browser how wide a picture will be

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.
This commit is contained in:
Claude Opus 5
2026-08-01 23:18:59 +06:00
committed by bdeshi
parent 7136f6e2d9
commit ccce537ae4
6 changed files with 36 additions and 4 deletions
+17
View File
@@ -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.
+1 -1
View File
@@ -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 |
+1 -1
View File
@@ -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
+7
View File
@@ -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` |
+6 -2
View File
@@ -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" -}}
<figure>
{{- range .Pictures}}
<img src="{{.Src}}"{{if .Srcset}} srcset="{{.Srcset}}"{{end}}{{if .Width}} width="{{.Width}}" height="{{.Height}}"{{end}} alt="{{$.Args.alt}}">
<img src="{{.Src}}"{{if .Srcset}} srcset="{{.Srcset}}" sizes="(min-width: 36rem) 32rem, 100vw"{{end}}{{if .Width}} width="{{.Width}}" height="{{.Height}}"{{end}} alt="{{$.Args.alt}}">
{{- end}}
{{- if not .Pictures}}
<img src="{{.Args.src}}" alt="{{.Args.alt}}">
@@ -16,7 +20,7 @@
{{if .Pictures -}}
<div class="gallery">
{{- range .Pictures}}
<figure><img src="{{.Src}}"{{if .Srcset}} srcset="{{.Srcset}}"{{end}}{{if .Width}} width="{{.Width}}" height="{{.Height}}"{{end}} alt=""></figure>
<figure><img src="{{.Src}}"{{if .Srcset}} srcset="{{.Srcset}}" sizes="(min-width: 36rem) 16rem, 100vw"{{end}}{{if .Width}} width="{{.Width}}" height="{{.Height}}"{{end}} loading="lazy" decoding="async" alt=""></figure>
{{- end}}
</div>
{{- end}}
+4
View File
@@ -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`}},