How I Built This Site
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 (
.njkfiles, Jinja-like syntax) - Content: Markdown files with YAML frontmatter, parsed by
gray-matter - Storage: SQLite, via
better-sqlite3 - Rendering:
markedfor Markdown → HTML,sanitize-htmlto 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:
- Reads every
.mdfile incontent/posts/ - Extracts frontmatter with
gray-matter - Converts the Markdown body to HTML with
marked - Sanitizes that HTML with
sanitize-html(defense in depth, even though I'm the only author) - 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.