Files
bdeshiandClaude Opus 5 30c26bd1ac resolve relative links against the disk, serve them as addresses
Item 2 of the order of work. An author writes `../day-01.en.md` — the path an
editor preview resolves — and the engine emits `/posts/day-01/`.

The larger effect is durability. Resolution goes through key → route, and a slug
moves the route while never moving the key (ADR-0035), so a relative link survives
a rename that a hand-written /posts/a-better-name/ does not. The demo proves it:
`../renamed-thing.en.md` renders as href="/posts/a-better-name/" — the author wrote
the filename and got the slugged address.

This is the engine altering authored markup, which ADR-0045 polices, so the test
that matters is what it declines to touch. Fourteen cases must survive exactly as
written: an absolute URL, a scheme-relative URL, mailto:, tel:, a root-relative
path, a bare fragment, a bare query, a name climbing out of content/, and every
relative path whose extension is not .md. That last line is what keeps cover.jpg
working — a bundle's assets already resolve because its URL mirrors its directory,
so rewriting them would break what works. Nine rewrite cases sit beside them.

Key derivation goes through content.KeyFromName, exported for this: the
language-suffix rule is the part that would drift between two copies, so it lives
in one place while the five lines of joining are duplicated in check.

khosra check now reports a relative .md link resolving to no bundle, as fatal —
verified by mistyping one and watching exit 1. Only the .md form: an extensionless
relative path may be an asset, and a checker that calls a working link broken gets
ignored wholesale.

Two debts this change paid rather than deferred.

render.go reached the file-length advisory, so theme parsing moved to theme.go —
414 and 105 lines, one topic each, since parsing runs per rebuild and rendering
runs per request. Not a _helpers.go shard.

And the demo's coverage test bound its renderer with a *copy* of the rebuilder's
wiring, so it missed this feature entirely while the real binary served it
correctly. Navigation had already drifted the same way. Both now call one bind(),
which is exactly what ADR-0072 was written about — and the test failing is the only
reason the copy was found.

Extensions 7 → 8. Core 3020 → 3049 of 3400: the seam is ~20 lines, the feature is
in ext where it belongs.

18 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:44:48 +06:00
..

examples/

demo-site/ is a complete khosra site, kept here to be read and served — the demonstration a person walks through to see what the engine does, and the fixture the engine tests itself against (ADR-0051).

make demo                 # serves it at localhost:8080
khosra check -site examples/demo-site

It is content, not a fixture in disguise: every file is what an author would write. Two things keep it honest, both in verify.sh:

  • TestTheExampleSiteExercisesEveryFeature (in internal/web) serves this directory through the real handler and asserts one case per feature. A feature that changes shape breaks it.
  • khosra check runs over it and must report nothing fatal.

Adding a feature means adding it here, with a case in that test. A feature the example does not show is a feature nobody can see working, and the gates are what stop that from being optional.

What each part is for

Path Demonstrates
site.yaml the two site-level settings: base and title
content/posts/day-01…11 pagination — eleven dated posts against a page size of ten
content/posts/first-light/ two languages, a directory bundle owning a picture, a figure with a caption
content/posts/only-english the fallback chain, and a canonical link naming the variant actually served
content/posts/renamed-thing/ slug moving an address, aliases keeping the old one working
content/posts/unfinished a draft: invisible without -dev on
content/posts/scheduled a future date: published by the clock, with nothing to run
content/comics/the-long-monsoon/ a series — structural membership, sparse order, prev/next/first/last, an archive, and a landing page in two languages
content/art/monsoon-studies/ a gallery: three JPEGs sized into a srcset, and an SVG left exactly as drawn
content/writing/notes-on-water/ an include whose fragment has no URL, and an extras/ tree with a nested directory
content/writing/typography what the engine does to prose, and what it deliberately does not
content/pages/* undated bundles: reachable, and absent from every feed
content/pages/bengali-only a bundle that exists in one non-default language only
content/status/* a titleless bundle — legal, and the one check warning this site expects
templates/list.html overriding a single named block and inheriting the document
static/note.txt files served verbatim

The one expected warning

khosra check reports the titleless status note. That is deliberate: titleless is legal, the theme falls back to the bundle's key, and the warning exists so an author who did not mean it finds out. A fatal finding here would fail verify.sh.