serve a declared slug as an address, leaving the key alone

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.
This commit is contained in:
2026-07-31 02:34:07 +06:00
parent 33d4547dda
commit ebf63d1d3b
10 changed files with 329 additions and 25 deletions
+7 -6
View File
@@ -9,9 +9,9 @@ If this file disagrees with the code, the code is right and this file is a bug.
|---|---|---|
| `go.mod` | module `khosra`; `goldmark`, `x/text`, `yaml.v3` direct | 10 |
| `internal/content/doc.go` | package comment | 5 |
| `internal/content/content.go` | bundles: `os.Root` open, walk, frontmatter split, key/lang derivation, NFC, tag slugs, partial files, permalink building | 368 |
| `internal/content/content.go` | bundles: `os.Root` open, walk, frontmatter split, key/lang derivation, NFC, tag slugs, partial files, permalink building | 381 |
| `internal/content/settings.go` | `site.yaml`: the site's own declarations (`base`, `title`) and absolute-URL building (ADR-0039) | 59 |
| `internal/content/site.go` | the indexed site: lookup with language fallback, aliases, `Query` and `Run`, sections, `Sequence`, `Everything` | 300 |
| `internal/content/site.go` | the indexed site: lookup with language fallback, aliases, `Query` and `Run`, sections, `Sequence`, `Everything`, slug routes | 395 |
| `internal/render/render.go` | goldmark with the typographer, per-kind template sets with site override, the `Partial`/`Origin` seams features render and resolve through, `Page`/`List`/`Sequence`/`head` | 409 |
| `internal/render/chrome.go` | the engine's own words: phrase table, month names, digits, and the `t`/`num`/`day` template funcs (ADR-0034) | 105 |
| `internal/render/templates/` | reference theme: `base.html`, `page.html`, `list.html`, `shortcodes.html`, `theme.css` (ADR-0026) | — |
@@ -22,9 +22,10 @@ If this file disagrees with the code, the code is right and this file is a bug.
| `internal/web/discover.go` | `/robots.txt` and `/sitemap.xml`, absolute and only with a declared base (ADR-0039) | 74 |
| `internal/web/web.go` | handler: resolve, look up with fallback, section and tag listings, sequence, `/static/` (misses and refusals alike answer 404), degrade on failure | 152 |
| `cmd/khosra/main.go` | flags (`-site`, `-addr`, `-base`), wiring, startup — the only place things are assembled | 60 |
| `*_test.go` | table-driven, one file per source file; symlink escape (content and static), canonical paths, language fallback, aliases, pagination, tags, sequences, chrome, typography, shortcode escaping, galleries, includes, partials, widows, site settings, absolute URLs, robots, sitemap, 404 | 1755 |
| `*_test.go` | table-driven, one file per source file; symlink escape (content and static), canonical paths, language fallback, aliases, pagination, tags, sequences, chrome, typography, shortcode escaping, galleries, includes, partials, widows, site settings, absolute URLs, robots, sitemap, slug routes, 404 | 1946 |
Serves a bundle at `/{section}/{slug}/`, a paginated listing per section, tag listings global and
Serves a bundle at `/{section}/{slug}/` — the slug derived, or declared in frontmatter without moving the
key (ADR-0035) — a paginated listing per section, tag listings global and
section-narrowed, sequence navigation and a series archive on any nested bundle, `static/` verbatim, plus `/robots.txt` and
`/sitemap.xml`.
Chrome text, dates and digits render in English or Bengali; authored text is untouched but for typographic
@@ -32,8 +33,8 @@ smoothing and widow prevention (ADR-0034). This repo holds engine source only
`-site` (ADR-0011). `site.yaml` declares `base` and `title`; with a base, canonical, hreflang and OpenGraph
URLs go absolute (ADR-0039).
Frontmatter the parser lifts today: `title`, `date`, `tags`, `aliases`, `order`. Every other key in
`content-model.md`'s table — including `slug`, `draft` and `type` — lands in `Extra` unread, so that table
Frontmatter the parser lifts today: `title`, `date`, `tags`, `aliases`, `order`, `slug`. Every other key in
`content-model.md`'s table — including `draft` and `type` — lands in `Extra` unread, so that table
is the accepted format, not a list of what runs.
Dependencies: three, all allowlisted — `goldmark`, `golang.org/x/text`, `gopkg.in/yaml.v3`.