No database. One JSON file per post.
Here are three lines of Markdown, and here is what the file on disk looks like after you save them.
What you type:
Posts are **files**. The build is a loop over a folder.
There is no step three.
What lands on disk, one JSON file per post:
{
"slug": "no-step-three",
"title": "No step three",
"date": "2026-07-31T09:00:00+02:00",
"state": "published",
"tags": ["content"],
"content": [
{
"type": "text",
"text": "Posts are files. The build is a loop over a folder.",
"formatting": [
{ "type": "bold", "start": 10, "end": 15 }
]
},
{
"type": "text",
"text": "There is no step three."
}
],
"source": { "platform": "manual" }
}
(Trimmed for the post — a real file carries a couple more housekeeping fields — but this is the real shape.)
Look at where the bold went
The text is plain text. The bold is a pair of offsets pointing into it — not <strong> tags baked into a string, not a Markdown blob waiting to be parsed again. That one decision quietly pays for half the engine:
- Search indexes the text as-is. No stripping tags, no "why does searching for strong match every post".
- The build never parses Markdown. Parsing happens once, at save time; the build just renders blocks it already understands.
- Importers from seven platforms all target this same schema — which is why a new import source is an adapter with three methods, not a fork of the parser.
Round trips, with a seatbelt
edit converts the JSON back to Markdown, you edit, it parses again on save. And when a post contains something Markdown can't express — an embed imported from Bluesky, a link card — the editor warns you before saving would flatten it, and asks. Nothing gets silently thrown away in the round trip.
The boring finale
One post is one file, and I mean that as infrastructure, not poetry:
- backup is
tar - history is
git - migrating away from ./blog.sh is a
forloop over a folder of self-describing JSON
And one consequence worth saying out loud: the deployed site is just a build artifact. The working copy — content, media, config — lives on your machine, entirely under your control, no matter where the web part runs or what happens to it. Host disappears, server catches fire, provider triples its prices? Nothing of value was there. You point the deploy at a different backend and the site exists again, byte for byte. The server was never the blog. Your folder is.
An engine should be easiest to leave the same way it was easiest to join. This is that, in one file per post.

Comments