add khosra demo, and give the site a front page

The demo writes a whole site root that exercises every feature: two languages with
a fallback, a series with ordered chapters, a gallery, a figure, an include,
extras, tags across sections, a slug with an alias, an undated page, a draft, a
template override, static files and site.yaml. It generates its filler rather than
copying stored files, because nothing in this repository is content (ADR-0011) — and
that makes it a test of the engine rather than a fixture: anything khosra can do
that the demo cannot express is a gap.

Two things found by generating and then serving it, which is the whole point:

`khosra check` reported the demo's own series as mixing ordered and unordered
members. It was right — the chapter bodies *described* `order: 10` while the
frontmatter never carried it. The checker caught its own author.

And `/` was a **404**. ADR-0008 leaves the root engine-owned, which is right, but
"engine-owned" was never given an answer, so a visitor to the site's own address got
nothing. The root now lists every bundle, newest first, paginated like any other
listing, and 404s only when nothing is published. A hand-written home page stays a
separate decision, recorded as such.

Verified end to end: 23 files written, 12 bundles, 12 derivatives, `check` clean,
and every URL the demo promises answers — including the alias redirecting, the draft
hidden, and the front page rendering through the site's *own* template override.
This commit is contained in:
Claude Opus 5
2026-07-31 19:54:23 +06:00
committed by bdeshi
parent 96313e4eda
commit 4396303771
13 changed files with 360 additions and 14 deletions
+3 -1
View File
@@ -41,9 +41,11 @@ func resolve(path string, site *content.Site) (resolution, bool) {
if path == "" || path[0] != '/' {
return resolution{}, false
}
// The root is a listing of everything, not a miss: the engine owns "/" (ADR-0008), so it answers with the
// one thing it can — every bundle, newest first (ADR-0050).
trimmed := strings.Trim(path, "/")
if trimmed == "" {
return resolution{}, false
return resolution{lang: content.DefaultLang, page: 1}, true
}
lang, key, redirect := cutLang(content.Normalise(trimmed), site)
+3 -1
View File
@@ -74,7 +74,9 @@ func serveStatic(sub fs.FS) http.Handler {
// A section is not a bundle, so this runs only after the bundle lookup misses. A page number past the
// end is a 404 rather than an empty page, because an empty page is a URL that means nothing.
func serveListing(w http.ResponseWriter, req *http.Request, site *content.Site, r *render.Renderer, res resolution) bool {
if res.key == "" || strings.Contains(res.key, "/") {
// An empty key is the site root, which lists everything. Anything with a slash in it is a bundle path that
// missed, not a section.
if strings.Contains(res.key, "/") {
return false
}
items := site.Run(content.Query{Section: res.key, Lang: res.lang})
+31 -1
View File
@@ -50,9 +50,39 @@ func TestServeBundleAtItsPermalink(t *testing.T) {
}
}
func TestTheRootListsEverything(t *testing.T) {
// The engine owns "/" (ADR-0008), so it answers with the one thing it can: every bundle, newest first
// (ADR-0050). Found by serving the demo, where the front page was a 404.
h := testHandler(t)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
if rec.Code != http.StatusOK {
t.Fatalf("GET / = %d, want 200", rec.Code)
}
body := rec.Body.String()
for _, want := range []string{"About", "Hello"} {
if !strings.Contains(body, want) {
t.Errorf("the root should list every section's bundles, missing %q:\n%s", want, body)
}
}
// A site with nothing published has no front page rather than an empty one, which is the same rule every
// listing follows.
empty, err := render.New(nil, content.Settings{}, nil)
if err != nil {
t.Fatal(err)
}
bare := Handler(Fixed(content.NewSite(nil)), empty, nil, nil, content.Settings{})
rec = httptest.NewRecorder()
bare.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
if rec.Code != http.StatusNotFound {
t.Errorf("an empty site's root = %d, want 404", rec.Code)
}
}
func TestUnknownPathsAre404(t *testing.T) {
h := testHandler(t)
for _, path := range []string{"/", "/nope/", "/pages/nope", "/pages/about/deeper/"} {
for _, path := range []string{"/nope/", "/pages/nope", "/pages/about/deeper/"} {
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
if rec.Code != http.StatusNotFound {