main
19
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
9349c54d2e
|
let a feature own a route, and serve the site's own files at exact paths
Addresses like /.well-known/security.txt are fixed by somebody else's spec. None is a bundle, none belongs under /static/, and core had no way to serve one. This is the trigger the extension registry has been held for, in those words: ADR-0042 called core's generic derived-file route "the seam to revisit when a second feature wants output of its own", and state.md's counter note said to build the registry "when a feature wants a route". Raw passthrough is that feature, so the seam is built rather than worked around. Only Routes, not the seven-field Extension struct extensions.md describes. Five of the other six fields have no implementor and building them would be the speculation rule 6 forbids. It also kept the change inside the core budget, which had 65 lines left: the seam is ~30 core lines and the feature's own code lands in internal/ext/, where there is room. Core is 2965/3000. A feature returns map[string]http.Handler; core mounts each as an exact pattern and learns nothing about who owns it. A path core already answers is skipped with a warning, not overridden — http.ServeMux panics on a duplicate pattern, so a site shipping root/robots.txt would otherwise take the server down at startup. Verified: server alive, engine keeps /robots.txt, warning logged, zero panics. Templating is opt-in by filename. A .tmpl suffix is stripped from the URL and the file is rendered with text/template — never html/template, which would turn an ampersand in a contact address into & and a JSON quote into ". Opt-in by name rather than by sniffing the type, because a key or a signature may contain anything and a pass choosing for itself which files to rewrite would eventually eat one. The data is the site's own declarations and nothing more, which is the point: a security.txt naming its canonical URL should not repeat what site.yaml already says. Headers come from root/_headers.yaml, exact paths only. Globs are a second-use feature and the concrete need is a handful of .well-known names. The manifest is not served, by the leading-underscore rule that already means "not addressable" everywhere else — no special case was added for it. A manifest that will not parse is logged and ignored; the files still serve. Found while counting: the Extensions row read 4 while five packages existed. notation landed in ADR-0061/0062 and was never counted, though the prose beside the number already named all five. Corrected to 6. That is the latent item about counters having no mechanical check, demonstrating itself. Not done, and logged as latent: khosra check cannot report a root/ file shadowing an engine path, because verify.sh fails a feature that imports a sibling and the reserved paths live in passthrough. The startup warning fires on every boot, which is louder than a check finding. Evidence against the demo with a fresh binary: /pubkey answers with its declared text/plain despite having no extension; /.well-known/security.txt answers with Canonical filled from site.yaml's base, plus the declared CORS header; /humans.txt gets a derived type; /_headers.yaml is 404; / and a bundle page are untouched. Eight unit tests cover layout, absence, interpolation, non-escaping, declared and derived headers, a broken template, and a broken manifest. 24 files, +514/-46. Extensions 4 (miscounted) → 6. Routing cases unmoved: exact paths are mux entries, not resolver cases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
64e53f28c8
|
give a page the assets its own content asked for, and write down the rule
Two decisions and one mechanism. The human wants demos, games and runnable
embeds to carry real CSS and JS while every ordinary page stays scriptless, and
wants adding an asset to be theme work rather than a rebuild.
The mechanism reuses what already had that property. A theme defines
`assets:<name>` beside its other fragments; shortcodes record their own name as
they are opened; after conversion the engine renders each matching fragment once
into Page.Assets. So a gallery calling one shortcode forty times carries its
stylesheet once, and a page that called nothing carries nothing. Frontmatter
`use:` reaches the same fragments without a call.
Considered and rejected: templates/assets.yaml, which reads more declaratively
and buys a parser, a contract shape and a rebuild for conditional markup; and a
table in Go mapping shortcode to files, which would hardcode exactly what was
deliberately made data-driven.
Collection is parse-phase, so no transform counter moves — goldmark's extender
list is already the ordered pipeline for parse work, which state.md's counter
says in its "does not count" column.
Separately, styles/scripts are lifted at last. They sat in content-model.md's
table unread, and the theme contract listed them under "what the engine
provides", which was aspirational rather than true. Both are bundle-relative: a
name with .. or a leading / is dropped and logged, the refusal ::include and a
code block's file= already make. The engine builds the URLs because a theme must
not construct an address.
ADR-0080 writes the antifeature list down, with its single exception inside it.
An antifeature nobody recorded does not bind anything, and each of these dies to
one reasonable-looking request at a time. The exception is author-invoked and
cannot fire by accident.
The reference theme emits the stylesheets and no script element at all. That was
the human's correction to a first attempt which had page.html emitting the tag
and verify.sh narrowed to permit it — narrowing the gate to fit the code was
backwards, and the narrowing was also wrong, passing a probe with a hardcoded src
because it filtered whole lines and every line carries {{define}}. verify.sh is
untouched. examples/demo-site redefines the head block instead, so the JavaScript
half is demonstrated by a site rather than built into the binary, which is a
better demonstration and a stronger property.
Evidence, against the demo site with a freshly built binary: the sandbox page
carries its own css and js at bundle-relative URLs; colophon calls ::tally twice
and carries tally.css once with zero scripts; about calls it never and carries
neither; listings unaffected. Plus a table test for the escape refusal, which
until now had only the running server behind it.
19 files, +355/-86. No counter moves. Demo is 31 bundles.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
69f43e5791
|
enable task lists, superseding the decision that excluded them
ADR-0058 kept task lists out as "a note-taking affordance, not a publishing one". That reasoning measured the wrong axis: a checklist inside a published technical piece — setup steps, a runbook, a what-I-tried list — is publishing, and nothing else in the dialect expresses "this item is done" without the author hand-writing an entity. ADR-0078 supersedes that half and records why. The half of ADR-0058 that mattered is untouched: extension.GFM stays refused, because the bundle drags linkify in with the tables it is wanted for, and linkify rewrites an author's plain text into markup — the line ADR-0034 draws. One named extension is not a bundle, and wire.go now says so where the temptation to reach for GFM will next appear. ADR-0058's Status line names its successor, so a reader arriving there learns the task-list sentence no longer holds. Same in-place Status annotation the mutability rule allows, Decision text untouched. Checkboxes render disabled: static markup, nothing clickable, nothing stored. A reader with scripting off sees the same page, which is the property the whole theme is built on. goldmark adds no class to the list, so theme.css finds it with :has rather than the engine inventing markup to be styled by. Demo carries the case ADR-0051 requires — a colophon checklist of what this build does and does not do, including the unticked "ship a single byte of JavaScript", which is true of that page and asserted by the test's absent list. No counter moves: state.md's counters are explicit that an upstream extension enabled in the list is dialect, not a feature of this engine — only a package under internal/ext/ counts. 8 files, +55/-10. 1 line of engine code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
aa9bd2a489
|
make merging the default include model
Composing a page from several files is why includes exist, and one endnote list at its end is what that page wants. ADR-0066 kept the old default only so existing content would not re-render; both models have now been seen and the human chose. `include: embed` still asks for the other thing. One-level needed enforcing on the new path: a spliced fragment's own include line would otherwise be parsed as a call and expanded a second time, which a test caught. It is dropped during the splice, matching what embed already did. |
||
|
|
67defae912
|
highlight code server-side, and let a block quote a file
chroma at render time, emitting CSS classes rather than inline colour, handed to a `code` theme fragment. Highlighting works with scripting off, in a feed reader, in a browser that never runs JavaScript. No lighter pure-Go option exists — every "alternative to chroma" is JavaScript, which the reference theme is gated against. A fence's info string carries the rest: title, numbers, start, hl=3,7-9, and file=name lines=A-B, which reads the snippet out of a file beside the bundle and numbers it by that file's own lines. So a post quotes several parts of one program without the copies drifting from it, and a reader can find what they are looking at. Verified on the real binary: the same file at lines 5-10 and 12-14, each numbered as it really is, with different lines tinted. Not a new package: a new one could not import the key=value parser this repo already has, because ADR-0069 forbids a feature importing its sibling, and a second parser for the same syntax is what §6 stops. Two costs, both stated in the ADR rather than buried. The binary goes from ~15MB to 19MB, for a project whose story is one small binary. And the reference theme now carries a token palette — the first thing in it that is a taste rather than a demonstration — kept to eight classes for that reason. The demo quotes a shell file, not a Go one: a .go file under examples/ joins the module and has to compile, which the build gate caught before it shipped. 6 of 9 modules, ext 2188/3500. |
||
|
|
7136f6e2d9
|
let a shortcode fragment speak the reader's language
Three fragments added this session needed a word the author did not write: an untitled :::warn told the reader nothing about being a warning, an untitled panel fell back to whatever the browser calls <details>, and the contents list had no label at all — an accessibility gap as much as an untranslated one. None of them could be fixed, because `t` needs a language and Fragment had none, so those words could only ever have been English on a site that serves Bengali. Fragment gains Lang, captured on each call at parse time — a node renderer never receives the parse context, the same constraint that put pictures and headings on the node. Five phrase keys follow, and a Bengali page now reads সূচিপত্র, সতর্কতা and বিস্তারিত where an English one reads Contents, Warning and Details. The demo's own list.html was the better example of the problem and now shows the answer: a site's own sentences are not in the engine's phrase table, so a template needing its own words branches on the language it was given. That is what theme-contract.md has always told a theme to do, demonstrated rather than asserted, and a case proves the Bengali listing carries no English. Two files crossed the size advisory on the way. render.go shed the contract types to view.go, where state.md already claimed they lived and where the file's own header said they belonged; shortcodes_test.go split to mirror its sources, which the one-file-per-source convention already asked for. Both are pure moves. |
||
|
|
b5be77498e
|
give the author three controls the engine was deciding alone
`include: merge` in frontmatter splices a bundle's includes before the parse, so
a page assembled from several files is one document: one footnote list at its
end, numbered straight through, and an abbreviation defined anywhere reaching
every part. Moving the rendered block afterwards would have meant editing
goldmark's own markup; handing the parser one source gets the right answer from
it instead. Without the flag nothing changes — each fragment stays its own
document with namespaced ids, so no existing content re-renders.
Heading ids are unique under either model. Merging gets that free, because one
parse means one id set, but embedding did not: three `## Description`s across a
page and its fragments produced three identical anchors, and every link to them
landed on the first. A nested parse now shares the parent's id set, so the
second becomes #description-1 — goldmark's own suffixing, reaching across files
because they finally share the set it counts in.
Auditing for other policies the author could not reach found two more.
A heading may declare its anchor: `## Title {#stable-anchor}`. This is the one
that mattered most and nobody had asked for it — a derived id changes when the
text does, so rewording a heading silently broke every link to that anchor,
which is indefensible in an engine whose first value is that published addresses
are permanent.
`::toc{depth=2}` shortens a contents list, because a theme cannot know per page
how deep is useful and the author can.
Deliberately not added: a typographer toggle, a per-picture "do not resample",
icon overrides. No second user for any of them.
The hand-copied wiring in example_test.go drifted for the third time this
session — Compose this time, after the dialect and notation — each caught by a
demo case rather than by the copy. The latent row is now marked due, with what
moving the list would require.
|
||
|
|
91c733a0e0
|
spread the demo's features across the pages that would carry them
notes-on-water had become a kitchen sink: every feature from the last eight loops on one page, which demonstrates the engine and misrepresents the site. Now each lands where a real site would put it. The text marks, the abbreviation, authored <kbd> and the margin note go to writing/typography, which is already the page about what the engine does to words. Icons and the grouped panels go to pages/colophon, which is the page about the theme — install instructions are what tabs are actually for. about gets a lone expando, the-flood gets a chapter note, and notes-on-water keeps what it is really for: the include and its namespaced footnotes, the gauge table, the glossary and a contents list over its own headings. Seven bundles carry features now instead of two, and the demo cases moved with them, so each still names the page it proves. |
||
|
|
fdc76ba1b9
|
add panels and margin notes, with no engine code at all
Expandable sections, tabs and asides — three of the things parked earlier — turn out to be two theme fragments and nine lines of CSS. That is what the container mechanism bought: they needed no Go. Tabs without script were the only real problem, and containers do not nest, so :::tabs wrapping :::tab was never available. Sibling <details> elements sharing a name attribute are natively mutually exclusive, which is what tabs are, so grouping is one argument on the same fragment an expando already uses. A browser too old for grouping opens them independently — the content is never hidden, which is the failure mode worth caring about. The aside is beside the text where the viewport has room and in the flow where it does not, in one media query. No JS anywhere, and the demo case asserts the page contains no <script> at all. No ADR: nothing here is expensive to reverse, and the contract grew additively as its stability rule allows. |
||
|
|
8bfacc7e98
|
build a table of contents from the document's headings
`::toc` renders through a `toc` fragment receiving level, text and the id goldmark already assigns. The engine collects, the theme decides whether that is a list, a sidebar or nothing — the reference theme emits a flat <ol> with a level class per entry, so indentation is CSS rather than markup. A theme cannot enumerate headings, because templates cannot parse HTML, so the engine is the only thing that can supply them. Collection lives in ext; only the Heading type and one Fragment field are core. That is what needed the ceiling: CORE_LOC_MAX 2800 -> 2850, the second raise. ADR-0041 said to read a second raise as evidence something belongs in ext, and the test was applied rather than waived. feed.go and discover.go are the features that should leave core, and they cannot, because an ext feature cannot own a route until the extension registry exists — which the counter says buys nothing yet. The thing that should move is blocked on a different decision, so the honest fix was the ceiling and an ADR saying exactly that. When routes become ownable, they leave and this comes back down. Entry text is the heading's words with markup stripped: a link inside a link is not markup a browser accepts. An entry whose heading has no id is skipped rather than linked nowhere, and a page with no headings renders no nav at all. core 2804/2850, ext 1825/2000, 34 gates green, 0 warnings. |
||
|
|
78b8c51dff
|
add container directives, and admonitions as their first user
`:::name{…}`, a body of Markdown, then `:::`. This spends the form reserved by
ADR-0059 rather than leaving it a promise — and building it back then would have
been a mechanism with no user, which is what the reserve was avoiding.
The body renders first and reaches the theme fragment as .Body, already HTML, so
emphasis, links, subscripts and icons all work inside an admonition. That is one
addition to the theme contract, additive as the stability rule requires, and two
lines of core — which is what the remaining budget allowed.
Rendered by a transformer rather than the node renderer, for the same reason an
include is: rendering a subtree needs the document, and a node renderer never
gets one.
When the theme has no template for a kind, the engine writes the body out
unwrapped. Same principle as an unknown icon keeping its text, and it matters
more here: a theme not knowing one name must never cost an author paragraphs,
and an unstyled aside is a far smaller failure than a missing one.
The leaf parser was parameterised by prefix rather than copied — a second copy of
parsing logic is a stop condition, and the two forms differ by one colon.
Containers do not nest: a `:::` inside closes the one it is in, the same limit an
include carries. Stated in the ADR and the contract rather than left to be found.
core 2796/2800, ext 1743/2000, 34 gates green, 0 warnings.
|
||
|
|
df9335df33
|
parse :name: as an icon, and let the theme decide what one is
The engine's half is one call: parse the name, hand it to a single `icon` fragment, decide nothing else. No icon table in Go, ever. The surveyed engines split three ways — Unicode (Hugo, Pandoc), a remote image per icon (Jekyll's jemoji, which fails the sovereignty test outright), and inlined SVG from a bundled set (MkDocs Material) — and all three are decisions about markup, which ADR-0036 puts in the theme. A theme wanting Font Awesome ships a sprite in its own base.html and redefines one fragment: no webfont, no request, no script. The boundary rules are the whole difficulty, because the colon is the commonest punctuation in technical prose. A name must start with a letter, hold only letters, digits, hyphens and underscores, and neither colon may touch an alphanumeric. Verified on the real binary that 10:30:15, key:value:pair, "Note: this", a URL and a code span all come through untouched — each of them would otherwise be a silent edit to someone's sentence. The literal fallback closes the same hole from the other side: when the theme renders nothing, the engine writes the author's `:name:` back, so an unrecognised icon is never deleted from the middle of a paragraph. An icon needed its own inline node rather than the block one — goldmark distinguishes the two by type — which also avoids the type switch CLAUDE.md §6 forbids: two kinds, two renderer functions. The reference theme maps six names to Unicode and ships no sprite, font or asset. core 2794/2800, ext 1615/2000, 34 gates green. |
||
|
|
661fb469e8
|
expand abbreviations from a definition line
`*[TERM]: expansion` on its own line, PHP Markdown Extra's form, and every whole-word use in the document becomes <abbr title="…">. A transformer rather than an inline parser, because a definition may appear after the use it explains and a parser only ever sees what it has already read. A block parser rather than a pattern found later, because the line has to stop being content — an author would notice that going wrong before anything else. Whole-word matching is the part that would have bitten: without it a definition of HTML quietly rewrites HTMLish and xHTML too, so both have cases. Code spans, autolinks, raw HTML and an already-expanded term are skipped, the longest definition wins where two could match, and the expansion is escaped into the attribute so a quoted phrase cannot end it. Definitions are document-scoped. A term defined in a page does not reach an included fragment, which is parsed on its own bytes exactly as footnotes are — stated in content-model.md rather than left to be discovered. The nesting gate caught firstMatch four levels deep; the inner search is its own function now, which reads better than it did before the warning. core 2794/2800, ext 1483/2000, 34 gates green, 0 warnings. |
||
|
|
53e9ef473a
|
add inline notation, and take the tilde back from strikethrough
~sub~, ^sup^, ==mark==, and ~~strike~~ moved in from goldmark. Not a preference: goldmark's strikethrough claims a single tilde as well as a double, so with it enabled H~2~O rendered as H<del>2</del>O — measured before the change. Two features cannot share a byte and both be correct, so notation owns it and the authored syntax stays exactly as ADR-0058 documented. The second failure was worse and only showed up under test. Under delimiter rules `x^2 + y^2 = z^2` pairs its carets across the whole expression and renders x<sup>2 + y</sup>2 — prose silently becoming markup, in exactly the content this engine is for. So a single run is scanned rather than paired, and may not cross whitespace: a subscript holds a formula, never a phrase. Pandoc draws the same line. The cost is that a single run takes its content literally, so there is no emphasis inside a subscript, which the ADR states rather than leaving to be discovered. New package under internal/ext, which is a stop condition and was asked. It takes the extensions counter to 4, past its threshold, and the answer is still no: four features attach in three unrelated ways, and two goldmark extenders compose in goldmark's own extender list, which is already the registry for that shape. The example site's hand-copied extender list drifted, exactly as the latent row added last loop predicted — the demo case failed and named it. Both are now in step again. core 2794/2800, ext 1236/2000, 34 gates green, 0 warnings. |
||
|
|
1f168d973b
|
render the HTML an author writes, and narrow the gate to one call site
Dropping raw HTML was silently destructive. H<sub>2</sub>O rendered as "H2O",
10<sup>6</sup> as "106", <kbd>Ctrl</kbd> as "Ctrl", and khosra check reported
nothing — an author lost meaning with no signal anywhere. Measured on the real
binary before and after.
Invariant 2 already says content from the site root is trusted, so the old gate
was defending the half of the boundary that was never in question while the
untrusted half has no code to defend yet. Chemistry, units, exponents and
keystrokes are what a hard-science site needs and what no Markdown dialect
expresses, so html.WithUnsafe() goes on in internal/render/render.go.
The gate does not disappear; it narrows. verify.sh used to fail on WithUnsafe
appearing anywhere and now fails unless it appears in exactly that one file —
watched doing both, accepting one call site and naming both files when a second
appears. A second pipeline trusting its input is the failure ADR-0003 exists to
prevent, and when comments arrive they get their own goldmark without it. The
gate is the reminder that the split has to be built rather than assumed.
The security test that asserted "raw HTML must still be dropped" now asserts the
property that actually holds and matters more: a shortcode argument stays data
whatever the page around it is allowed to do. ::figure{alt=<b>bold</b>} still
arrives as <b> while the <span> beside it renders.
core 2793/2800, ext 1077/2000, 34 gates green, 0 warnings.
|
||
|
|
a893ab1821
|
replace the shortcode syntax with generic directives
`::name{key=value}` alone on a line, quotes only where a value has spaces,
braces omitted when there are none. The old form cost eleven characters of
punctuation per call and could not carry a body, which admonitions will need.
Generic directives are an existing convention — remark-directive, MyST,
Docusaurus — so this is a syntax authors and tools already know rather than one
more invention, and it reserves `:::name` for containers and `:name[…]` for the
inline dynamic calls that come later.
Retired outright rather than aliased: two syntaxes is two parsers and two test
sets forever. `khosra check` reports every leftover call as fatal and names the
replacement, so migrating a site root is running it until it exits zero — proven
on an unmigrated root, which exits 1 with the file and the fix.
The trigger byte moves from `{` to `:`, which prose uses constantly, so the
parser refuses `3::4`, `: a definition` and `:::note`, each with a case. The
definition-list parser sits at priority 101 and this one at 100, so it gets
first refusal and everything it rejects falls through.
Two things the syntax change would have broken silently. check's alt-text regex
still matched the old form, so the one accessibility check the engine has would
have stopped finding anything — it moves with the syntax and keeps its case. And
a test asserted that hostile arguments fail at the syntax because quotes cannot
be expressed; unquoted values are legal now, so it asserts the property that
actually holds: the fragment escapes them.
Demo migrated. core 2790/2800, ext 1077/2000, 34 gates green, 0 warnings.
|
||
|
|
0465785e81
|
settle the Markdown dialect, and namespace an include's footnotes
Tables, footnotes, definition lists, strikethrough and automatic heading ids. Which dialect a site is written against is permanent, so ADR-0058 names the whole set at once — including the four refused, each for a reason rather than a taste: task lists publish nothing, linkify rewrites plain text into markup that ADR-0034 forbids the engine to touch, CJK is the wrong script family for a Bengali site, and the GFM bundle is a package deal for the first two. Footnotes collided with includes, as the queue predicted but worse. An include converts its file on its own bytes (ADR-0038), so goldmark numbered its notes from one again and the page carried two id="fn:1"s — the parent's reference jumped to the fragment's note. shortcodes.FootnotePrefix stamps the file name on the nested document and hands it to goldmark's id-prefix function, so the fragment gets _method-fn:1 and the page keeps fn:1. Two things nothing tested before. The extender list ships from cmd/khosra, which no package can import, so the dialect had never been rendered through the list the binary actually uses — cmd/khosra/wire_test.go now does exactly that, including that the typographer no longer eats a table's delimiter row. And the demo carries the dialect and the footnote namespacing as cases, which caught auto heading ids changing markup in three existing assertions. The reference theme gains five lines: a rule under each table row, an indent for definitions, smaller footnotes. core 2790/2800, ext 1058/2000, 34 gates green. |
||
|
|
de1ce73430
|
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. |