Files
khosra/internal/render/view.go
T
bdeshiandClaude Opus 5 ec6e9838f0 rename docs/ to harness/, and reserve docs/ for the reader
docs/content-model.md opens with "Engine specification". It is also where the
rule lives that a leading underscore makes a file unaddressable — and the human
who owns this site did not know that rule, because nothing in this repository is
addressed to an author. Twelve documents named docs/ while being exclusively
about building the parser is a signpost pointing at the wrong room.

Naming the directory for its audience makes the gap visible instead of hiding it.
docs/ is now reserved and deliberately absent: an empty docs/ is an honest
statement that end-user documentation does not exist, where docs/ full of parser
specs was a claim that it did.

HARNESS.md stays at the root. Root holds the three entry points — README.md for a
human, CLAUDE.md for an agent, HARNESS.md for whoever maintains the machine — and
harness/README.md is the map of the directory, so moving the guide inside would
have collided with it for nothing.

Mechanical and wide: 100 path references across 24 files. Every verify.sh gate
that names a doc by path, the directory lists the dangling-path and ADR-number
gates scan, surface.sh's output target, the Makefile, CLAUDE.md's read order, the
skill, four commands, and two Go package comments. A first pass with a shell loop
silently edited only four files and the rest still said docs/; the fix was to
write the file list out and check the remaining count was zero rather than trust
the loop's exit status.

No rule, threshold, gate or obligation moved — this is a rename, and the gates
demonstrated it twice: they stayed green on the new paths, and the ADR-number gate
caught ADR-0082 before the entry existed.

Deferred, both on the human's call: the end-user documentation site itself, which
wants its own decision about where it lives and whether its claims are gated; and
moving examples/ under docs/, since demo-site is a live site root that verify.sh,
the coverage test and make demo all point at, and moving it would couple a rename
to a design nobody has made.

31 files, +146/-106.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:11:06 +06:00

193 lines
7.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
// harness/theme-contract.md describes exactly this and nothing else.
package render
import (
"html/template"
"io/fs"
"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
// Assets is the theme's own markup for everything this page's shortcodes and `use:` asked for, already
// rendered and deduplicated. Emit it in `<head>`. Empty for a page that asked for nothing, which is
// nearly all of them (ADR-0079).
Assets template.HTML
// Styles and Scripts are URLs of this bundle's own CSS and JS, built by the engine because a theme must
// not construct an address. Empty unless frontmatter named files beside the body.
Styles, Scripts []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
}
// Partial renders a named fragment. A feature under internal/ext is handed one of these at wiring time,
// because markup belongs to the theme and a feature must not write any (ADR-0036).
type Partial func(name string, data Fragment) ([]byte, error)
// Fragment is what a fragment template receives (ADR-0037, widened by ADR-0042).
type Fragment struct {
// Args are the call's key="value" pairs, exactly as written. Escaping is the template's.
Args map[string]string
// Pictures are what the engine gathered rather than the author wrote: one for a figure, many for a
// gallery, none when the call names nothing a picture. Kept apart from Args so a supplied value can never
// be mistaken for an authored one.
Pictures []Picture
// Lang is the language being served, so a fragment can localise its own words through `t` (ADR-0067).
Lang string
// Body is a container call's content, already rendered. Empty for a leaf call (ADR-0064).
Body template.HTML
// Headings are the document's headings, for a call that builds a table of contents (ADR-0065).
Headings []Heading
}
// Heading is one heading in the document, with the id an anchor links to (ADR-0065).
type Heading struct {
Level int
Text, ID string
}
// Picture is one image a fragment can render (ADR-0042).
type Picture struct {
// Src is the author's own file, relative to the bundle. A browser that ignores Srcset still gets the
// picture that was put there.
Src string
// Srcset offers the derivatives, closed by the original at its own width; empty when the picture is
// already small enough that no derivative was worth making.
Srcset string
// Width and Height are the original's intrinsic size, so a page can reserve the box before the bytes
// arrive. Zero when the file could not be read.
Width, Height int
}
// Origin tells a feature which bundle is being rendered, so a path in a call can resolve relative to it.
//
// Features read it from the parser context with OriginFrom. It carries the site's fs.FS rather than a
// directory name alone, because every read goes through the rooted filesystem and never a joined path
// (ADR-0031).
type Origin struct {
// Dir is the bundle's directory, relative to the site root: "content/comics/the-long-monsoon".
Dir string
// Files is the site root. Nil when the renderer was built without one, in which case a feature that
// needs files degrades rather than guessing.
Files fs.FS
// Lang is the language of the variant being rendered, so a feature can hand it to a fragment (ADR-0067).
Lang string
}