esteban@devtrillo:~/blog/writing$
← cd ~/writing
$ cat how-i-built-this-blog.md

How I built this blog

Jun 24, 2026·6 min read

I wanted a blog I could understand top to bottom. Not a platform I rent, not a dashboard I configure — a handful of files I could read in an afternoon and change without fear. This is the story of the decisions that got me there, and why each one earned its place.

Start from what a blog actually is

Strip a personal blog down and it’s mostly static text. The words don’t change between one reader and the next. That single observation decides almost everything downstream: if every page can be built once and handed out unchanged, I don’t need a server running my code on every request, a database in the hot path, or a framework that re-renders the world to show someone a paragraph I wrote last week.

So the foundation is a static site — every page rendered ahead of time into plain HTML. It’s the fastest thing to serve, the cheapest thing to host, and the hardest thing to break.

Astro, because content comes first

I chose Astro as the generator. The pitch that won me over: it ships zero JavaScript by default. A page is HTML until I explicitly ask for something interactive. For a site that’s 95% reading, that default is exactly backwards from most modern frameworks — and exactly right for me.

Posts are written in MDX, which is Markdown that can embed components. Most of the time I’m just writing prose. But when I want a callout, a keyboard shortcut, or a captioned figure, I drop in a small component mid-sentence and keep going. The writing stays the priority; the formatting gets out of the way.

NOTE

Reading time isn’t something I type into each post. It’s computed from the body when the site builds, so it can never drift out of sync with what I actually wrote.

A few things I deliberately let the build handle so I never have to think about them: the table of contents is generated from each post’s headings, the RSS feed and sitemap fall out of the content automatically, and code blocks get their syntax highlighting baked in at build time rather than in the reader’s browser.

Interactivity only where I ask for it

“Zero JavaScript by default” doesn’t mean no JavaScript ever — it means each interactive piece is an island: a self-contained component that ships its own behavior, and only that component pays the cost. The rest of the page stays static HTML.

The fastest way to believe that is to use one. Everything around this paragraph is inert text. This little terminal is the only thing on the page running code — because I dropped it in on purpose. Type help and poke around (theme is my favorite — it reaches out and flips the real site):

island.sh
Type help and press Enter. This widget is the only JavaScript on the page.
visitor@devtrillo:~$

That’s the whole model. I didn’t pull in a UI framework to get it; it’s a plain component with a small script attached, no different in spirit from the like button or the theme toggle. Interactivity is something I add deliberately, one island at a time, instead of paying for it everywhere up front.

A terminal theme, and no CDN

The look is a terminal: monospace accents, a shell-prompt header, a dark/light toggle. That’s taste, not engineering — but two decisions underneath it are engineering.

First, the theme is applied before the page paints. Nothing is worse than loading a dark site and getting flashed with white for a beat. The reader’s preference is settled in the very first moment, before anything is drawn.

Second, the fonts are mine. Plenty of sites pull fonts from a third-party CDN at load time, which means every visitor’s browser phones a stranger before my words show up. Instead the fonts are downloaded at build time and served from my own domain. Fewer moving parts, nothing to go down, and nobody tracking my readers on my behalf.

TIP

Self-hosting an asset isn’t just a privacy choice — it removes an entire category of “the site broke because someone else’s service did.”

The one interactive piece: counting likes and views

Here’s where pure static stops being enough. I wanted two living numbers on each post — how many times it’s been read, and how many likes it’s collected — and those genuinely have to be shared across every visitor. A number that’s the same for everyone can’t live in a file.

The instinct is to reach for a separate backend. I didn’t want one. Standing up a second service, a second deployment, a second thing to monitor — all to increment a couple of integers — felt absurd.

The trick that made it click: the same thing serving my static pages can also run a sliver of code, but only when I ask it to. The host that delivers the site is told to run my code first for one specific path — anything under /api/ — and to serve every other path straight from the prebuilt files, never touching my code at all.

the whole routing rule
"assets": {
"directory": "./dist",
"run_worker_first": ["/api/*"],
}

So a reader loading a post hits pure static HTML, exactly as fast as before. But when the page quietly reports “someone viewed this” or “someone liked this,” that one request — and only that one — runs real code and talks to a small database. One deployment. One domain. No second service.

Counting honestly without tracking anyone

A counter is only interesting if the numbers mean something. Two problems to solve, both without storing anything that identifies a reader:

The shape of the answer for both: instead of storing who did something, store a one-way fingerprint of the request mixed with a secret, so the same reader maps to the same anonymous token — and for views, fold the date into that fingerprint so it naturally expires at midnight. I learn that “this reader already counted today” without ever learning who the reader is.

NOTE

The numbers are stored as running totals, not as a pile of events I count up later. Showing a post’s stats is a single lookup, no matter how popular it gets. The cheap path stays cheap forever.

What I optimized for

Every decision here points the same direction: the fewest moving parts that still does the job. Static pages so there’s almost nothing to break. One host doing double duty so there’s nothing extra to deploy. My own fonts so I don’t depend on anyone else’s uptime. A counter that’s honest without being invasive.

The reward isn’t just speed or cost, though it’s both of those. It’s that I can open this repo months from now, read it end to end, and still know exactly why every piece is there. That’s the blog I wanted: small enough to hold in my head, and entirely mine.