serve Atom feeds for the site, a section and a tag

Membership is a publication date, not a type declaration (ADR-0043). The parked
feed shape said "every type declared primary", which would have made feeds wait on
declared types a third time — but the thing that distinguishes a feed item is
already on disk. Pages and section landings drop out because they have no date,
which is the right reason. The parked idea stays parked with a sharper trigger:
someone wanting a *dated* bundle kept out.

Built with encoding/xml from typed structs, never a template: XML in html/template
is escaping for the wrong grammar, and that is a correctness trap rather than a
matter of taste.

A bug the evidence found, older than feeds: content.URL("", lang) built "//", so a
whole-site feed's id and alternate link were https://khosra.example// — every
entry identity wrong in every reader. The root is "/" now, with a test, and the
hand-built "/" the resolver carried for the same reason can follow later.

Two counters re-scoped rather than incremented, the same way transforms was:

Views now counts *per-bundle selection* — the thing architecture.md means by the
View layer, still at zero consumers. Output formats are not it: HTML, sitemap XML
and Atom are three functions with nothing to share, so an interface over them
would have one member and no leverage.

Effects stays at 1. A feed is generated per request like the sitemap, so it is not
a second Effect and the runner is not yet due — the next thing that writes files
off the request path is.
This commit is contained in:
2026-08-01 02:23:36 +06:00
parent 7da2a58fd5
commit 62e31215ec
11 changed files with 372 additions and 12 deletions
+20
View File
@@ -575,3 +575,23 @@ core carries a generic "serve this directory of derived files" — which is the
feature wants output of its own.
Revisit if: startup time becomes noticeable on a large site — then the pass wants a manifest and a change
check rather than a stat per candidate.
## ADR-0043 — A feed carries dated bundles; membership needs no type declaration
Date: 2026-07-30 · Status: accepted (chooses a simpler rule than the parked feed shape in
`ideas/deferred-decisions.md`, which waited on declared types)
Decision: `/feed.xml` carries every **dated** bundle, newest first, capped at the most recent 20.
`/{section}/feed.xml` and `/tags/{term}/feed.xml` narrow it through the same Query, and a `/{lang}/` prefix
selects a language like anywhere else. The format is Atom, built with `encoding/xml` from typed structs rather
than a template. A feed needs the site's `base`, and answers 404 without one. Entries carry title, link,
identity and date — not the body.
Why: the parked direction was "every type declared `primary`", which would have made feeds wait for declared
types a third time. But the thing that actually distinguishes a feed item is already on disk: a publication
date. Pages, colophons and section landings have none and drop out for the right reason rather than by
declaration, and nothing new has to be invented or kept in sync. `encoding/xml` over a template because XML in
`html/template` is escaping for the wrong grammar — a correctness trap, not a style preference.
Consequence: cheap — one Query, one marshaller, and no new content concept; a `[spec]` idea stays parked
instead of being half-built. Expensive — "in the feed" and "has a date" cannot yet be separated, so a dated
bundle an author wants out of the feed has no way to say so; and entries without bodies mean a reader shows
titles only, until `summary` is parsed.
Revisit if: someone wants a dated bundle excluded, or one section kept out of the main feed. *That* is the
real trigger for declared types, and it is now a sharper one than "feeds exist".