Files
khosra/internal/render/view.go
T
Claude Opus 5andbdeshi 96313e4eda 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.
2026-07-31 19:48:32 +06:00

131 lines
4.9 KiB
Go

// The types in this file are the theme contract in Go: what a template receives, nothing about how it is
// produced. Split from render.go when that file crossed the size warning — the seam was already here, since
// docs/theme-contract.md describes exactly this and nothing else.
package render
import (
"html/template"
"time"
"khosra/internal/content"
)
// head is what every kind of page shares: the document shell the base template needs. Absence is the
// zero value — a template reads what exists and never fails on a missing field (invariant 1).
type head struct {
// Title may be empty for a bundle; a listing always has one.
Title string
// Lang is the locale being served.
Lang string
// Canonical is the permalink of what was actually served, which differs from the URL requested when
// the fallback chain supplied another language (ADR-0009).
Canonical string
// Alternates lists every language this key exists in, for hreflang.
Alternates []Alternate
// Style is the reference theme's stylesheet, inlined so a bare site root needs no asset route.
Style template.CSS
// Site is what the site declared about itself in site.yaml (ADR-0039). Zero when it declared nothing.
Site content.Settings
// Sections are the site's sections, for navigation: only .Title and .URL are set. Empty until the engine is
// told where to find them, which cmd does at wiring time (ADR-0049).
Sections []Item
}
// Page is one bundle rendered.
type Page struct {
head
// Key is the bundle's identity, useful for building links.
Key string
// HTML is the rendered body.
HTML template.HTML
// Extra carries every frontmatter key the parser does not name (ADR-0002).
Extra map[string]any
// Sequence is the series this page sits in, nil when it sits in none.
Sequence *Sequence
// Tags are this bundle's own terms, each with the URL of its listing. Empty when it carries none.
Tags []Item
// ExtrasURL links this bundle's supporting files, empty when it has none — so a theme can offer them
// without guessing whether they exist (ADR-0047).
ExtrasURL string
}
// Sequence is a series as a page sees it: its members in reading order, and where this page is in them
// (ADR-0033).
type Sequence struct {
// Title is the series' title, empty when the landing page omits one; URL is its permalink.
Title, URL string
// Members are every entry in reading order — ascending, unlike a dated listing.
Members []Item
// Index is this page's 1-based position, zero when this page is the series landing itself. Count is
// how many members there are.
Index, Count int
// Prev and Next are the neighbours in reading order, nil at the ends and on the landing page. Prev is
// the *earlier* entry, the opposite sense of a listing's PrevURL.
Prev, Next *Item
// First and Last are the ends of the series, set whenever it has members.
First, Last *Item
}
// Extras is a bundle's supporting files, browsable (ADR-0047).
type Extras struct {
head
// Bundle is the page these files belong to, so a theme has its title and a way back.
Bundle Item
// Entries is the whole tree, sorted by path.
Entries []content.Entry
// Selected is the entry the URL named, nil on the bare listing.
Selected *Selected
}
// Selected is the entry a request picked out of the tree.
type Selected struct {
content.Entry
// HTML is the rendered file for a markdown or text entry, empty for anything else.
HTML template.HTML
// RawURL always works: it is how a theme embeds an image, links a PDF, or offers a download.
RawURL string
}
// List is a collection page: the result of a Query, one page of it.
type List struct {
head
// Items are the entries on this page, in the Query's order.
Items []Item
// Page is 1-based; Pages is the total, at least 1 even when empty.
Page, Pages int
// PrevURL and NextURL are empty at the ends. Newer is "prev" because the order is newest first.
PrevURL, NextURL string
// Groups is the same entries partitioned by section, set alongside Items for a tag listing. Both shapes
// are offered because which one to show is the theme's decision, not the engine's (ADR-0046).
Groups []Group
}
// Group is a named run of entries within a listing.
type Group struct {
Name string
Items []Item
}
// Item is one entry in a listing.
type Item struct {
Title string
Key string
URL string
Date time.Time
// Section is the entry's top-level section, so a flat listing can label where an entry came from without
// the engine deciding that it must be grouped (ADR-0046).
Section string
}
// Alternate is one language a bundle exists in.
type Alternate struct {
Lang string
// URL is absolute when the site declares a base, because hreflang is read by machines that resolve nothing
// against the page.
URL string
// Path is the same address, root-relative — what a *visible* language link wants. Found by looking at a
// rendered page: using URL sent a reader from a local server to the canonical host (ADR-0049).
Path string
}