A draft is now not served at all, and neither is a bundle whose date has not
arrived. The filter sits in `Site.Lookup` and `Site.Run`, which is every path to a
bundle — so the files inside an unpublished bundle inherit its status for free,
which is what ADR-0024 asks for and what the asset route was written to allow. A
test asserts the 404 for the bundle *and* its picture, and that nothing leaks into
a listing, a feed or a sitemap.
The clock is read per request rather than at startup, so a scheduled post appears
exactly when its date arrives with nothing to restart and nothing to invalidate.
That first clock read created internal/content/clock.go, which is the only place
`verify.sh` allows `time.Now` — a render that depends on the time is worth being
able to find.
`-dev on` reveals both and reparses the theme before each render. Deliberately not
a bare boolean flag: turning unpublished work into public work should not be one
fumbled argument away. A reload that fails to parse leaves the working template set
in place, so a typo shows an error rather than replacing a good set with a broken one.
The server never fails on a bad bundle — it works around it and logs, which is easy
to miss (ADR-0029). This is the command that looks on purpose, and it exits non-zero
on anything that makes the site wrong rather than merely untidy.
Fatal: content the engine had to drop (unparseable frontmatter, a key two files
claim, an ignored slug or alias) and links that will 404 for a reader. Warnings:
no title, a figure with no alt text, a series where some members declare order and
others do not — that last one because unordered members sort last, so adding order
to one chapter silently moves every chapter that lacks it.
Two things this needed rather than invented. `Scan` and `Site` now *return* what they
worked around instead of only logging it: `ScanReport` and `Site.Problems`, with
`Scan` staying the logging wrapper so nothing else changed. And required-fields-per-type
is deliberately absent — `title` is the only field the engine requires today, so
checking more would mean inventing the type declaration that is still parked. Its
trigger stays where it was.
The link checker asks the same questions the resolver asks — bundle, alias, section
listing, file inside a bundle — because a checker that guesses differently from the
server is worse than no checker. Engine-owned paths are skipped: they are generated,
not authored.
It lives in internal/ext/check, so cmd/ stays wiring and the feature stays deletable.
Verified against the evidence site: clean before, and five findings across five fault
classes after I introduced them on purpose.
The human chose the second option: a route sits beside the key rather than
replacing it. So `slug` renames what a bundle is served at, in every language, and
identity stays derived from the path — which is exactly what keeps ADR-0033 intact,
since series membership is the directory. A series landing page can now be renamed
without orphaning its chapters, and there is a test that says so.
`Site` resolves routes at index time, because only it can see whether every variant
agrees. Disagreement is dropped rather than resolved, as is a slug landing where
another bundle already answers — the same rule colliding keys and contested aliases
already follow. The key a slug moved away from stops answering, so the old address
does not quietly keep working.
Two bugs surfaced doing this, both older than this change:
An alias naming its own bundle's former key was rejected as "an alias that names a
real bundle" — which made rename-plus-alias, the entire point of ADR-0008's alias
mechanism, impossible. The check now asks what a request asks: is anything actually
served there.
Aliases were counted per declaring *file*, so a bundle whose two language variants
both listed the same alias looked like two rival claimants and lost the alias. It is
a set of keys now. This one only appears with translated content, which is why no
fixture had caught it since entry 4 — the real binary did, on the first multilingual
rename.
Two exact paths a crawler asks for by name, so they are mux entries rather than
resolver cases — no bundle can collide, since a key always sits under a section.
robots.txt at the site root is served verbatim, because a site that ships one has
said something deliberate; otherwise the engine emits the minimum that is true and
points at the sitemap. The sitemap lists every bundle in every language it exists
in, since each variant is separately reachable, with lastmod only where a bundle
has a date. Every URL comes from content.URL like every other path the engine
emits, so a sitemap cannot disagree with what is actually served.
Both need a declared base. Without one the sitemap answers 404 rather than listing
paths no crawler can resolve, and robots omits the Sitemap line rather than
writing a relative one.
write() was setting text/html for every caller, and headers only go out with the
first byte — so a handler setting its own type would have had it silently replaced,
which is how a sitemap gets served as a web page. It now splits into write and
writeAs, and the tests assert the content types rather than only the bodies.
A bundle nested under another bundle is a member of that series (ADR-0033), so
`Site.Sequence` walks up to the nearest bundle ancestor and back down to its
members: ordered by `order` where set, then by name. Members resolve through the
language fallback, so a chapter with no Bengali variant still holds its place in
Bengali reading order instead of breaking prev/next.
One `.Sequence` field carries both shapes a theme needs. A landing page renders
`.Members` as an archive; a chapter renders `.Prev`/`.Next`, which are pointers
into `.Members` so `{{with}}` yields nothing at the ends. `Index == 0` is what
tells the two apart.
`Query` was deliberately not extended. A series ascends where `Run` descends, and
an order knob on `Query` is the config knob rule 6 bans; instead `Site.keys()`
came out so both iterate the index one way, deleting `Run`'s own dedupe map.
`draft` is not honoured: no bundle carries the field and nothing else excludes
drafts, so entry 19 adds it in both places at once. Recorded in content-model.md
rather than left implied.
state.md also corrects six inventory rows that had drifted before this change —
three LOC figures, the test total, `go.mod`, and two lines that were flatly wrong
("Dependencies: none", "goldmark is not yet imported"). The coupling gate proves
state.md changed with the code; it cannot prove the numbers are right.
content.go had passed FILE_LOC_WARN, which conventions.md treats as the moment to
split rather than a number to ignore: flat until the figure, then split, never
pre-partitioned. content.go now parses bundles and builds permalinks; site.go holds
the indexed site — lookup with language fallback, aliases, Query and Run. The tests
follow the same seam.
No behaviour change, and the test-coupling gate was right to demand the tests move:
its exemption covers comments and whitespace, not code relocated between files,
where an edit could hide.