I started a blog to write down thoughts I already have and to share and figure out what I think. I saw a blog post by Colophon that made a lot of sense to me, so this website is inspired by his post and development philosohpy. The first thing I want to figure out in writing is the website itself: how I built it, how I host it, and why I made each choice along the way.

Why not just use a platform?

If you want build a website today you have a lot of options. WordPress, Squarespace, Medium, or you can skip all of that and just ask Claude or ChatGPT to build one for you. Building a website has never been easier.

But building is the easy half. But what happens after? That's where hosted platforms come with trade-offs that don't show up until you're already invested:

  • Your content becomes harder to export or migrate cleanly
  • Prices rise, and features drift behind the higher tiers or vanish
  • If your audience lives on their domain, the platform owns your traffic and your SEO
  • If the company folds, you are foced into a migrations on someone else's schedule

To be fair, you also get really beneficial things in return: zero maintenance burden - security patches, backups, uptime, and scaling are all the platform's problems. Plus professional infastructure with fastglobal delivery and protection.

For a business, the trade offs might be worth it. For a personal blog that's mostly just words on a page, I decided it wasn't.

Why Build Your Own

Nobody can take it away. No one can shut the site down, change the pricing, or discontinue a product. The content is just Markdown files, the code is mine, and it runs everywhere Node runs. There is no company in the loop.

I can hold the whole thing in my head. The entire stack fits in one mental model, which means it will still make sense to me in two years. Less to break, less to update, less to relearn.

It's fast and private by default With no client-side Javascript, pages load quickly even on a bad connection, there's no code tracking visitors, and everything works with javascript disabled. Try getting that from a hosted platform stuffed with analytics scripts.

It will outlive trends. Markdown files plus a small amount of boring, standard code will still work in a decade. No proprietaty formatm no export-migrate anxiety.

Builing things is fun.

One honest caveat: this path is not completely beginner-friendly. It assumes some comfort wth the terminal and willingness to learn how to run your own server. If that sounds like a miserable experience, a platform is genuinely the better choice.

Okay, but why not Jekyll on Github pages?

That would be a fair question, it's free, simple, and battle-tested. But what you gain in simplicity, you lose in control. Take one small example: a friendly, styled RSS feed requires setting custom HTTP headers, and GitHub pages won't let you touch headers. I don't want a system that constrains what I can build, or that lures me into propriety features I couldn't recreate myself decades later.

Here's the principle underneath all of this: dat and simplicity are what actually last. Languages and frameworks come and go. What survives the web? The posts themselves, and a system simple enough to rebuild from memory. This entire engine is something I could rewrite in half a day. Control is worth more than convenience.

The stack

  • Backend: Node.js + Express
  • Templating: Nunjucks (.njk files, Jinja-like syntax)
  • Content: Markdown files with YAML frontmatter, parsed by gray-matter
  • Storage: SQLite, via better-sqlite3
  • Rendering: marked for Markdown → HTML, sanitize-html to clean it
  • Styling: Vanilla CSS with native custom properties for light/dark mode
  • Client-side JavaScript: none

Writing is just Markdown files

Every post starts as a .md file in content/posts/, with frontmatter for metadata:

---
title: How I Built This Site
published_at: 2026-07-09
tags: dev, meta
summary: The tech behind this blog and why I picked each piece of it.
---

Post content goes here...

No admin panel, no database editing by hand — I write in a text editor like I would anywhere else, and the file itself is the source of truth.

The "bake" step

This is the part I'd explain first if someone asked how the site actually works. Instead of rendering Markdown on every request, a build script (scripts/bake.js) does it once, ahead of time:

  1. Reads every .md file in content/posts/
  2. Extracts frontmatter with gray-matter
  3. Converts the Markdown body to HTML with marked
  4. Sanitizes that HTML with sanitize-html (defense in depth, even though I'm the only author)
  5. Writes the result — title, tags, published date, and pre-rendered HTML — into a SQLite table

At request time, Express just does a SELECT against that table and hands the already-rendered HTML to Nunjucks. No Markdown parsing, no template compilation of post bodies, on the hot path. It's effectively a static site generator, except the "generated" output lives in a database instead of a folder of HTML files — which made querying, sorting, and filtering posts by tag or date trivial with plain SQL instead of hand-rolled file indexing.

Why SQLite

For a single-author blog, I don't need a client-server database, connection pooling, or a hosted Postgres instance. better-sqlite3 gives me a synchronous, embedded database that lives in one file (data/site.sqlite), commits atomically, and requires zero infrastructure. The web process opens it read-only — all writes happen exclusively through the bake script — so there's a clean separation between "authoring" and "serving."

Why server-side rendering with no client JS

Every page — the post list, individual posts, the about page — is rendered on the server with Nunjucks and sent as plain HTML. There's no hydration, no bundle, no framework runtime shipped to the browser. That means:

  • Pages load fast, even on a slow connection
  • There's nothing to track visitors with, because there's no client-side code to do the tracking
  • The site keeps working if JavaScript is disabled entirely

For a blog, none of the usual reasons to reach for a JS framework — client routing, complex interactive state, real-time updates — actually apply.

Why Nunjucks

Express supports several template engines; I picked Nunjucks because it's Jinja-style (familiar if you've used Python/Flask/Django templates), has first-class Express integration, and supports layout inheritance (base.njk) so the header, nav, and footer live in exactly one place.

Keeping it flat

The project structure mirrors the request lifecycle on purpose:

content/posts/   → what I write
scripts/bake.js  → turns writing into queryable data
src/routes/      → maps URLs to database queries
src/views/       → maps data to HTML
public/style.css → maps HTML to something that looks decent

Nothing here is clever, and that's the point — I wanted a stack I could hold entirely in my head, that I could deploy anywhere Node runs, and that would still make sense to me in two years.

What's next