finish the reference theme, and the contract holes it found

The theme was supposed to need no engine work. It needed four things, which is
exactly what the audit said would happen if the contract had gaps rather than the
theme (ADR-0046) — a reader could not reach, from any page: another section, the
tags on the page they were reading, the extras beside it, or the same page in
another language.

So the contract grew three fields, all additive: `.Sections` for navigation,
`.Tags` with each term's listing URL, and `.ExtrasURL`, empty when a bundle has
none so a theme never links a 404. Sections arrive through `Renderer.Navigation`, a
callback, because sections change when content does and a copy would go stale — the
nav updates on a rebuild along with everything else.

One flaw only visible by looking at a rendered page: the language switcher pointed
at the canonical host, because `.Alternates` went absolute for hreflang. Those are
two needs, so an Alternate now carries `.URL` (absolute, for machines) and `.Path`
(relative, for a link a person clicks).

The theme now demonstrates every field it is given, including `.First`/`.Last`,
which existed and were never rendered. Still no JavaScript, still one stylesheet.

A test that asserted a page had no sequence nav was matching the inlined
stylesheet rather than the markup, and now matches the element. That is the third
time a loose assertion has passed for the wrong reason.
This commit is contained in:
Claude Opus 5
2026-07-31 19:48:32 +06:00
committed by bdeshi
parent 9100ce4876
commit 96313e4eda
12 changed files with 188 additions and 13 deletions
+27 -2
View File
@@ -40,6 +40,9 @@ type Renderer struct {
files fs.FS
// settings are the site's declarations, constant for the life of the process.
settings content.Settings
// sections reports the site's sections when asked. A callback, because sections change when content does and
// the renderer must not hold a stale copy (ADR-0049).
sections func() []string
// reload reparses the theme before each render, for `-dev`: editing a template should not need a restart.
// Off in a serving build, where parsing once is the point (conventions.md).
reload bool
@@ -155,13 +158,19 @@ func New(siteFS fs.FS, settings content.Settings, extend func(Partial) []goldmar
// canonical arrives as a path and leaves absolute when the site declared a base: a canonical link and an
// hreflang are read by machines that resolve neither against the page (ADR-0039).
func (r *Renderer) head(title, lang, canonical string) head {
return head{
h := head{
Title: title,
Lang: lang,
Canonical: r.absolute(canonical),
Style: r.style,
Site: r.settings,
}
if r.sections != nil {
for _, name := range r.sections() {
h.Sections = append(h.Sections, Item{Title: name, Key: name, URL: content.URL(name, lang)})
}
}
return h
}
// absolute is the site's own URL for a path the engine emitted, or the path itself when no base is declared.
@@ -169,6 +178,12 @@ func (r *Renderer) absolute(path string) string {
return content.Absolute(r.settings.Base, path)
}
// Navigation tells the renderer where to find the site's sections.
//
// Set once at wiring time, like Reload: a page needs to offer navigation, and only the index knows which
// sections exist. A callback rather than a slice, because content changes and a copy would go stale.
func (r *Renderer) Navigation(sections func() []string) { r.sections = sections }
// Reload makes every render reparse the theme first. For `-dev` only: it trades the parse-once rule for the
// ability to edit a template and refresh.
func (r *Renderer) Reload() { r.reload = true }
@@ -311,7 +326,17 @@ func (r *Renderer) Bundle(b content.Bundle, served string, variants []string, se
Sequence: r.sequence(seq, served),
}
for _, l := range variants {
p.Alternates = append(p.Alternates, Alternate{Lang: l, URL: r.absolute(content.URL(b.Route, l))})
path := content.URL(b.Route, l)
p.Alternates = append(p.Alternates, Alternate{Lang: l, URL: r.absolute(path), Path: path})
}
for _, tag := range b.Tags {
p.Tags = append(p.Tags, Item{Title: tag, Key: content.TagSlug(tag), URL: content.TagURL("", content.TagSlug(tag), served, 1)})
}
// One Stat rather than a walk: a page only needs to know whether there is anything to link to (ADR-0047).
if assets, hasAssets := b.Assets(); hasAssets && r.files != nil {
if _, err := fs.Stat(r.files, path.Join(assets, content.ExtrasDir)); err == nil {
p.ExtrasURL = content.ExtrasURL(b.Route, served, "")
}
}
return r.execute(r.page, p, b.Key)
}