Every blogging setup I have run before this one eventually got in the way. A database to back up. An editor that reformatted my code blocks. A plugin that needed updating on a Sunday. The writing was never the hard part, the machinery around it was.
So this one has almost no machinery. A post is a markdown file in a folder. I write it in the same editor I write everything else in, commit it, and push. Cloudflare builds the site and puts it on the edge. There is no database, no admin login, and nothing running between a reader and a file.
What a post actually is
This file:
src/content/blog/a-blog-that-is-a-folder-of-markdown-files.md
is served at /a-blog-that-is-a-folder-of-markdown-files. The file name is the
URL, which means renaming a file breaks a link, which means I think about the
name once, properly, before I publish.
The front matter at the top is checked against a schema at build time. Not warned about, checked. If the description is missing, or the title is 200 characters, or a tag has a capital letter in it, the build fails and names the file and the reason. Nothing broken can reach the site, because the site cannot be built while it is broken.
That turns out to matter more than it sounds. The failure modes of a personal blog are boring and constant: a post with no meta description, a draft that went out by accident, two posts fighting over the same URL. All of those are now build errors.
What the build refuses
A few rules that are worth naming, because each one is a mistake I have made before:
- A post with
draft: trueis dropped in exactly one place, and every page, the RSS feed, the sitemap and the social card generator read from that same list. A draft cannot leak into one of them by being forgotten in another. - Slugs are checked against the pages that already exist. A post called
archive.mdwould quietly shadow the archive page, so the build stops instead. - An image without alt text is a type error, not a lint warning.
- No file in the repository may exceed 300 lines. When one wants to, it is doing two jobs and gets split.
The parts
| Piece | What it does |
|---|---|
| Astro | Turns the markdown into static HTML at build time |
| Cloudflare | Serves dist/ from the edge |
| Satori | Draws a social card per post, also at build time |
| pnpm | Installs nothing published in the last seven days |
That last one deserves a sentence. Every dependency has to have been on the registry for a full week before it can enter the lockfile. Most supply chain attacks on npm are found and pulled within days, so waiting a week is a cheap and very effective filter. There is no exception list.
What it costs
Nothing renders per request. The HTML, the RSS feed, the sitemap and the 1200 by 630 social card for every post are all files written during the build. A post going somewhere busy costs the same as a post nobody reads.
The pages carry no third party scripts, no analytics, and no fonts fetched from
somebody else’s CDN. The content security policy is generated with a hash for
every inline style the build emitted, so it does not need unsafe-inline to
work.
What I plan to write here
Mostly AI engineering, and mostly the unglamorous middle of it: agents that have to work on somebody else’s machine, evals that catch a regression before a user does, the retrieval and tooling underneath, and the cost of running any of it in production.
Alongside that, whatever I learn shipping small products end to end. I have spent years on large publishing stacks, so the systems half of this comes naturally and will show up wherever it is the actual problem.
Things I got wrong will get their own posts. Those are the ones worth reading.
If something here is useful, or wrong, tell me.
