A content format for hand-authored records
Why the records stopped being element lines: a metadata line, a blank line, a markdown body, and what that costs once code, diagrams and recordings live inside the prose.
Every record is a file in a git repository: a metadata line, a blank line, then the body. The seven project records were a JSON object per line until recently, which made the paragraph rather than the sentence the unit of authoring — a blank line inside a paragraph meant something to the file. The body region removed that, and with the file shape no longer the invariant, the invariants moved into a script.
The dialect is small on purpose. Four things in a body are more than prose, and all four are whole lines at column 0: a fenced code block, an image with an optional ^ caption, an @@embed:<kind>/<slug> marker, and a @@html block for imported markup. src/lib/content/markdown.ts is the only module that knows those four rules: it exports the scanner that extracts a construct, and the renderer, which never sees a fence.
What changed in the metadata line
Line 1 keeps its closed key set, and hash still cuts after the first LF, so editing a title never rehashes the prose. It is written, not typed: node scripts/verify-content.mjs --write replaces the twelve hex characters and touches nothing else. Line 1 of this file, minus the hash and the summary text:
{"kind":"article","slug":"a-content-format-for-hand-authored-records","title":"A content format for hand-authored records","summary":"…","date":"2026-11-09","tags":["content-format","markdown","static-site"],"draft":false,"supersedes":null}The gate replaced the old shape's guarantees: it enumerates every record through the parser the build uses, so the two cannot disagree about what a file holds, and it checks the key set, the hash, slug against filename, supersedes and image src.
The path a body takes: segments become elements of a closed union, and the renderer is total over it, so an unknown type fails the build.
graph LR
A[record file] --> B[scanBody]
B --> C[element union]
C --> D[renderer or player]Embeds cost more than the body did. A recording is a directory beside the records: a payload chosen by the family's file pattern, and a meta.json naming a contract version, a kind, a title and a digest over the payload bytes. The gate holds the loader's support matrix, so an unsupported contract, a payload over 512 KiB, or a poster a player has no prop for fails the build. The player loads at mount time, so the prerendered page is a figure and no player markup.
This one is a real asciinema recording: ten events of git log --oneline, git status and git diff. No poster is declared, so the player's own default applies.
The AI family needed a stated contract rather than a parser call. @mlad/ai-replay renders a derived string and ships no payload parser, so the derivation is this site's own: drop a leading tool-less assistant message, pair each prompt with the reply after it, enforce alternation, and treat turn as a 0-based index into that list. Exchange 0's prompt is not part of what the player shows, so the page carries it. This file plays turn 2, the whole session, an export from another project, whose opening prompt was "ok. step back. We need to check the bigger picture and prepare to implement".
Still unbuilt: the web family resolves and has never been authored, and a body linking to another record is unchecked when that record is renamed.
