I spend my working days on back ends: queries, caches, search relevance, queues. So when I finally built somewhere to put my writing, my instinct was to reach for the same reflex I use at work — pick the boring option, and only add moving parts when something actually breaks without them.
Here is what that produced.
Astro, with static output
The site is Astro building to plain files. Every page is HTML on disk before anyone visits it. There is no server rendering, no database, no runtime to keep alive.
What sold me was Astro’s default: components render on the server and ship zero JavaScript unless you opt in. Most site frameworks make you work to remove client-side code. Astro makes you work to add it, which is the right way round for something that is mostly text.
The one non-obvious win is content collections. Posts live as Markdown in
src/content/blog/, and the frontmatter is validated against a Zod schema at
build time:
schema: z.strictObject({
title: z.string().min(1),
description: z.string().min(1),
publishDate: z.coerce.date(),
// Optional, so a post can leave them out and still build.
tags: z.array(z.string().min(1)).default([]),
draft: z.boolean().default(false),
}),
A typo in a date, a missing description, a draft flag spelled drafts — the
build fails instead of quietly shipping something wrong. strictObject is what
carries that last one: a plain z.object would drop the unrecognised key and
publish the post. It is the same argument as Pydantic at an API boundary:
validate at the edge, then trust the data everywhere downstream.
Hand-written CSS
No Tailwind, no framework, no component library. About three hundred lines of CSS driven by custom properties for the palette, spacing scale and type scale.
This is a single centred column of text. A framework would have given me a configuration file, a build step and a class vocabulary to learn, in exchange for solving a layout problem I do not have. At this size the framework is the liability, not the CSS.
Dark mode is a prefers-color-scheme block that swaps eight custom properties.
There is no toggle, which means there is no state to persist, no flash of the
wrong theme on first paint, and no JavaScript. Your operating system already
knows what you want.
Zero client-side JavaScript
Not as a purity exercise — I just could not find a feature that needed it. No search box, no comments, no analytics, no theme toggle. Navigation is links. The result loads instantly on a bad connection and works with JavaScript disabled, and I get to skip an entire category of bug.
If I add something later that genuinely needs interactivity, Astro lets me hydrate that one component and leave the rest of the site alone. That is the part I wanted: the cost stays local to the feature.
Deployment
Vercel, connected to the GitHub repository, building on push. Because the
output is static there is nothing to configure — Vercel detects Astro, runs the
build and serves the files from its CDN. The Node version is pinned in
.nvmrc so my machine and the build agent agree.
What I would tell myself before starting
Decide how much site you actually need first. I wanted a bio, a CV, a list of what I have built, and somewhere to write. That is four page templates and a content collection. Every hour I did not spend on tooling went into the words instead, which is the only part a reader will ever notice.