track the demo as a real site in examples/, gated like the docs
The human asked for a demo extensive enough to review by hand, tracked as files, and kept current the way docs are. Generated filler cannot be reviewed — you cannot read a Go function and see what a reader sees — so the generator is deleted and `examples/demo-site/` is an ordinary site: 33 Markdown files, six pictures, site.yaml, a template override and static files. My reason for generating it was ADR-0011, and I had that rule wrong: it puts the *author's site root* outside this repository, not fixtures. conventions.md already keeps golden files in testdata/, and this is the same category one size up. Keeping both a generator and files would have been two sources of truth, so one had to go. Kept true by two gates rather than by good intentions. A table-driven test in internal/web serves the directory through the real handler with one case per feature — thirty-five of them, each naming what it proves — and verify.sh runs `khosra check` over it and fails on anything fatal. Adding a feature now means adding it to the demo and adding its case, and the build says so if you don't. Extensive on purpose: eleven dated posts so a section actually paginates, a four-chapter series so first/last are not the neighbours, a gallery with three JPEGs and an SVG so both the resampled and the untouched paths show, a Bengali-only bundle, a titleless status, a draft, a future date, an include, a nested extras tree, and a typography page that demonstrates what the engine will not do. Two expectations of mine were wrong and the demo corrected them: the site's own list template overrides *tag* listings too, so there are no group headings there — which turns out to be the better demonstration of ADR-0046, since the theme picking the flat shape is exactly the point. And template literal text is not escaped, so an apostrophe stays an apostrophe.
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 267" role="img" aria-label="A rising line">
|
||||
<rect width="400" height="267" fill="#eceae4"/>
|
||||
<polyline points="20,240 120,180 220,200 380,40" fill="none" stroke="#1a4d7a" stroke-width="6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 257 B |
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: Monsoon Studies
|
||||
date: 2026-03-20
|
||||
tags: [monsoon]
|
||||
---
|
||||
Three studies and a diagram. The gallery below is every picture in this directory, in filename order — the
|
||||
numeric prefixes do the ordering without appearing in any URL.
|
||||
|
||||
{{< gallery >}}
|
||||
|
||||
The three JPEGs carry generated widths in a `srcset`. The SVG does not: nothing here can resample it, so it is
|
||||
rendered exactly as it was drawn rather than dropped for being inconvenient.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: দীর্ঘ বর্ষা
|
||||
date: 2026-03-01
|
||||
tags: [monsoon]
|
||||
---
|
||||
চার পর্বের একটি ধারাবাহিক। কোন লেখা কোন ধারাবাহিকের অংশ, তা ঠিক হয় ফোল্ডারের অবস্থান দেখে।
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: The Long Monsoon
|
||||
date: 2026-03-01
|
||||
tags: [monsoon]
|
||||
---
|
||||
A series in four parts. Membership is *structural*: every bundle nested under this one is a chapter, so
|
||||
nothing in frontmatter has to agree with the directory.
|
||||
|
||||
Below this text the theme renders the archive — every chapter in reading order, which the engine supplies.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Aftermath
|
||||
date: 2026-03-16
|
||||
order: 30
|
||||
---
|
||||
Chapter three. Nothing here but prose — most chapters are.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: First Rain
|
||||
date: 2026-03-02
|
||||
order: 10
|
||||
---
|
||||
Chapter one. `order: 10` puts it first; the gaps between 10, 20, 30 and 40 mean inserting a chapter later is
|
||||
one edit and never a rename.
|
||||
|
||||
This page has no `prev`, so the theme renders nothing where a previous link would go.
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: The Flood
|
||||
date: 2026-03-09
|
||||
order: 20
|
||||
tags: [monsoon]
|
||||
---
|
||||
Chapter two, written as a directory bundle so it can own the picture below.
|
||||
|
||||
{{< figure src="water.jpg" alt="A wall of grey water" caption="Day three" >}}
|
||||
|
||||
It has a chapter on either side, so the sequence nav shows both, plus its position in the series.
|
||||
|
After Width: | Height: | Size: 45 KiB |
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: What Remained
|
||||
date: 2026-03-23
|
||||
order: 40
|
||||
tags: [monsoon]
|
||||
---
|
||||
Chapter four, and the last. With four chapters the theme's *first* and *last* links have somewhere to go
|
||||
that is not simply the neighbour.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: About This Demo
|
||||
---
|
||||
No date, so this page is not a feed item — the engine decides feed membership by asking whether something has
|
||||
a publication date, which is a fact already on disk.
|
||||
|
||||
Every file under `examples/demo-site/` in the khosra repository is here to be *served* and read. If the engine
|
||||
can do something this site does not show, that is a gap in the demo; if the demo asks for something the engine
|
||||
cannot do, that is a gap in the engine.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: শুধু বাংলায়
|
||||
---
|
||||
এই পৃষ্ঠাটির ইংরেজি রূপ নেই। `/pages/bengali-only/` চাইলে ইঞ্জিন এটিই দেবে — কারণ শেষ উপায় হিসেবে
|
||||
যেকোনো একটি ভাষা 404-এর চেয়ে ভালো।
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Colophon
|
||||
---
|
||||
Built by khosra: one Go binary, four dependencies, no JavaScript anywhere in the reference theme.
|
||||
|
||||
The stylesheet is inlined into every page, so a site root with no `static/` still renders. A real theme would
|
||||
replace it, and `templates/list.html` in this site root shows how — that one file overrides a single named
|
||||
block and inherits the rest of the document.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Day 01
|
||||
date: 2026-02-01
|
||||
tags: [journal]
|
||||
---
|
||||
The 01th entry, here so the section listing paginates: eleven posts against a page size of ten.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Day 02
|
||||
date: 2026-02-02
|
||||
tags: [journal]
|
||||
---
|
||||
The 02th entry, here so the section listing paginates: eleven posts against a page size of ten.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Day 03
|
||||
date: 2026-02-03
|
||||
tags: [journal]
|
||||
---
|
||||
The 03th entry, here so the section listing paginates: eleven posts against a page size of ten.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Day 04
|
||||
date: 2026-02-04
|
||||
tags: [journal]
|
||||
---
|
||||
The 04th entry, here so the section listing paginates: eleven posts against a page size of ten.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Day 05
|
||||
date: 2026-02-05
|
||||
tags: [journal]
|
||||
---
|
||||
The 05th entry, here so the section listing paginates: eleven posts against a page size of ten.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Day 06
|
||||
date: 2026-02-06
|
||||
tags: [journal]
|
||||
---
|
||||
The 06th entry, here so the section listing paginates: eleven posts against a page size of ten.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Day 07
|
||||
date: 2026-02-07
|
||||
tags: [journal]
|
||||
---
|
||||
The 07th entry, here so the section listing paginates: eleven posts against a page size of ten.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Day 08
|
||||
date: 2026-02-08
|
||||
tags: [journal]
|
||||
---
|
||||
The 08th entry, here so the section listing paginates: eleven posts against a page size of ten.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Day 09
|
||||
date: 2026-02-09
|
||||
tags: [journal]
|
||||
---
|
||||
The 09th entry, here so the section listing paginates: eleven posts against a page size of ten.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Day 10
|
||||
date: 2026-02-10
|
||||
tags: [journal]
|
||||
---
|
||||
The 10th entry, here so the section listing paginates: eleven posts against a page size of ten.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Day 11
|
||||
date: 2026-02-11
|
||||
tags: [journal]
|
||||
---
|
||||
The 11th entry, here so the section listing paginates: eleven posts against a page size of ten.
|
||||
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: প্রথম আলো
|
||||
date: 2026-03-01
|
||||
tags: [monsoon]
|
||||
---
|
||||
এই লেখাটি বাংলায়। তারিখ, সংখ্যা, আর পৃষ্ঠার নাম — সব বাংলায় দেখা যাবে, কারণ ইঞ্জিন নিজের কথাগুলো
|
||||
অনুবাদ করে রাখে।
|
||||
|
||||
লেখকের লেখা কখনও অনুবাদ হয় না; কেবল ইঞ্জিনের নিজের শব্দগুলো।
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: First Light
|
||||
date: 2026-03-01
|
||||
tags: [monsoon, beginnings]
|
||||
---
|
||||
This post exists in two languages, so the footer's language links go somewhere and `hreflang` has something
|
||||
to point at. Its Bengali twin is `index.bn.md` beside this file.
|
||||
|
||||
It also owns a picture, which only a *directory* bundle can do:
|
||||
|
||||
{{< figure src="cover.jpg" alt="A grey-green gradient standing in for a photograph" caption="A caption, rendered by the theme's figure fragment" >}}
|
||||
|
||||
The picture above is served from this bundle's own directory, and the `srcset` on it names generated widths.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Only in English
|
||||
date: 2026-02-20
|
||||
tags: [beginnings]
|
||||
---
|
||||
There is no Bengali version of this one. Ask for `/bn/posts/only-english/` and the engine serves this instead
|
||||
of a 404 — the fallback chain is requested, then the default locale, then any variant at all. The canonical
|
||||
link in the head names the variant you actually got, not the one you asked for.
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: This Was Renamed
|
||||
date: 2026-02-25
|
||||
slug: a-better-name
|
||||
aliases: [posts/renamed-thing]
|
||||
---
|
||||
This file is at `content/posts/renamed-thing/`, but `slug:` moves its address to `/posts/a-better-name/`.
|
||||
The old path still works, because `aliases:` promises it will — it answers a permanent redirect.
|
||||
|
||||
The bundle's *key* never moved, which is why the series and cache machinery could not care less.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Scheduled for Later
|
||||
date: 2099-01-01
|
||||
---
|
||||
Dated in the future, so it is not published yet. The engine reads the clock per request, which means this
|
||||
appears by itself the moment its date arrives — nothing to restart, nothing to invalidate.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Unfinished
|
||||
date: 2026-04-10
|
||||
draft: true
|
||||
---
|
||||
A draft. Not served at all: not here, not in a listing, not in a feed or a sitemap — and neither is any file
|
||||
beside it. `khosra -dev on` is the only thing that reveals it.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
date: 2026-03-30
|
||||
tags: [journal]
|
||||
---
|
||||
A short note with no title at all. Titleless is legal — the theme falls back to the bundle's key rather than
|
||||
failing — and `khosra check` warns about it on purpose, which is the one finding this demo expects.
|
||||
@@ -0,0 +1,4 @@
|
||||
## Method
|
||||
|
||||
Gauge readings, transcribed each evening. *Emphasis and links survive*, because an include is parsed as
|
||||
Markdown rather than pasted as text.
|
||||
@@ -0,0 +1,3 @@
|
||||
First attempt, abandoned. Kept because the process is worth publishing.
|
||||
|
||||
Nested one level deeper, so the extras tree has a directory in it.
|
||||
@@ -0,0 +1,3 @@
|
||||
day one: 2m, rising
|
||||
day two: 3m, holding <not markup>
|
||||
day three: 3m, falling
|
||||
@@ -0,0 +1,4 @@
|
||||
## Research
|
||||
|
||||
Rendered as Markdown *inside* the extras listing, with the tree still beside it. Selecting an entry is an
|
||||
ordinary link and a whole page, so none of this needs JavaScript.
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: Notes on Water
|
||||
date: 2026-03-25
|
||||
tags: [monsoon, journal]
|
||||
---
|
||||
The finished piece. Below, a part of it lives in a separate file and is included here:
|
||||
|
||||
{{< include file="_method.md" >}}
|
||||
|
||||
That fragment starts with an underscore, so the scanner never treats it as a bundle: it has no URL of its own
|
||||
and appears in no listing. An included file cannot itself include — one level, deliberately.
|
||||
|
||||
This bundle also has an `extras/` directory, so the theme offers a link to it at the foot of the page.
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Typography and Its Limits
|
||||
date: 2026-03-28
|
||||
tags: [journal]
|
||||
---
|
||||
"Quotes become curly," she said -- and dashes become dashes, while an ellipsis... becomes one character. That
|
||||
is the whole of what the engine does to an author's words: a Markdown parser option, nothing more.
|
||||
|
||||
Inside a code span none of it happens: `"quotes" -- and ellipses...` survive byte for byte, because the
|
||||
typographer works on the parsed tree rather than on the text.
|
||||
|
||||
Line breaking is not the engine's business at all. This paragraph is deliberately long enough to wrap several
|
||||
times on a narrow screen, which is where you would notice a lonely last word — and the stylesheet asks the
|
||||
browser to avoid one with `text-wrap: pretty`, because only the browser knows where the lines actually fall.
|
||||
|
||||
A code block, indented.
|
||||
Also untouched.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Everything a site may declare about itself (ADR-0039). Both keys are optional.
|
||||
base: http://localhost:8080
|
||||
title: A Khosra Demo
|
||||
@@ -0,0 +1,2 @@
|
||||
Anything under static/ in the site root is served verbatim under /static/, with no processing at all.
|
||||
Directory paths answer 404 rather than listing what is here.
|
||||
@@ -0,0 +1,24 @@
|
||||
{{define "main" -}}
|
||||
<h1>{{.Title}}</h1>
|
||||
<p><em>This listing is rendered by the site's own template, parsed after the embedded one. It redefines a
|
||||
single named block and inherits the whole document around it.</em></p>
|
||||
{{- if .Items}}
|
||||
{{- range .Items}}
|
||||
<article class="entry">
|
||||
<h2><a href="{{.URL}}">{{if .Title}}{{.Title}}{{else}}{{.Key}}{{end}}</a></h2>
|
||||
{{- if not .Date.IsZero}}
|
||||
<p class="meta"><time datetime="{{.Date.Format "2006-01-02"}}">{{day $.Lang .Date}}</time>{{if .Section}} · {{.Section}}{{end}}</p>
|
||||
{{- end}}
|
||||
</article>
|
||||
{{- end}}
|
||||
{{- else}}
|
||||
<p>{{t .Lang "empty"}}</p>
|
||||
{{- end}}
|
||||
{{- if or .PrevURL .NextURL}}
|
||||
<nav class="pagination">
|
||||
{{- if .PrevURL}}<a rel="prev" href="{{.PrevURL}}">{{t .Lang "newer"}}</a>{{end}}
|
||||
<span>{{t .Lang "page-of" (num .Lang .Page) (num .Lang .Pages)}}</span>
|
||||
{{- if .NextURL}}<a rel="next" href="{{.NextURL}}">{{t .Lang "older"}}</a>{{end}}
|
||||
</nav>
|
||||
{{- end}}
|
||||
{{- end}}
|
||||