How I Built This Site
I started a blog to write down thoughts I already have, and to figure out what I actually think by trying to say it clearly. The first thing worth writing down is the website itself: how I built it, how I host it, and why I made each choice along the way.
#A Quick Map Of The Options
Broadly, there are three ways to put words on the internet.
Hosted Platforms - WordPress.com, Squarespace, Medium, Substack. You write in their editor, they handle everything else. Nothing to install, nothing to maintain. You rent their stuff.
Static site generators - Jekyll, Hugo, Eleventy, Astro. You write markdown files on your own machine, run a build command, and get a folder with plain HTML, CSS, and JS. You can host that folder just about anywhere: GitHub Pages, Netlify, Cloudflare Pages, a cheap VPS. There's no database and no code running when someone visits, the pages were built ahead of time. The generator only runs when you run it.
Dynamic sites - a server that builds pages on request. WordPress installed on your own host, or anything with a database and application code behind it. More power, more moving parts, more to keep patched.
The middle option is where most personal blogs land, and for good reason: static files are cheap to host, hard to break, and trivial to back up. What I build is a slight mutation of it. The pages are still rendered ahead of time, but the output lands in a SQLite table instead of a folder of HTML. Same idea, different filing cabinet.
#Why not just use a platform?
Building a website has never been easier. WordPress, Squarespace, Medium — or skip all of that and ask Claude or ChatGPT to build one for you.
And you get real things in return. Zero maintenance burden: security patches, backups, uptime, and scaling are all the platform's problems. Professional infrastructure with fast global delivery and DDoS protection you didn't have to think about.
But building is the easy half. What happens after is where the trade-offs show up, and they don't show up until you're already invested:
- Your content becomes harder to export or migrate cleanly.
- Prices rise, and features drift behind higher tiers or vanish entirely.
- If readers reach you through the platform's domain, the traffic and search authority accrue to them rather than to you — and none of it follows you when you leave.
- If the company folds, you're forced into a migration on someone else's schedule.
For a business, a hosted platform is often worth exactly that price. 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 the 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 re-learn.
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. This is a real reason, not a footnote.
One honest caveat: this path is not completely beginner-friendly. It assumes some comfort with the terminal and willingness to learn how to run your own server. If that sounds miserable rather than fun, a platform is genuinely the better choice.
#Okay, but why not Jekyll on Github pages?
Fair question, it's free, simple, and battle-tested. But what you gain in simplicity, you lose in control. 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: dats 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 instead of a folder of HTML
This is the only unusual choice on the page, so it's worth explaining properly.
A conventional static site generator writes a directory tree of HTML files. That works fine until you want to do anything across posts. The homepage needs the ten most recent. A tag page needs every post carrying that tag, newest first. An archive needs them grouped by year. With a folder of files, each of those is a hand-rolled index — you end up writing a small, buggy query engine and calling it a build step.
A table gives you all of that for free:
SELECT slug, title, published_at
FROM posts
WHERE tags LIKE '%dev%'
ORDER BY published_at DESC
LIMIT 10;
That's the whole tag page. Sorting, filtering, pagination, and "related posts" are one-liners in a language I already know and that will still exist in twenty years.
The choice against Postgres is easier: for a single-author blog I don't need a client-server database, connection pooling, or hosted infrastructure. better-sqlite3 gives me a synchronous, embedded database that lives in one file (data/site.sqlite), commits atomically, and requires zero setup. Backing up the site is copying a file.
The web process opens the database read-only. All writes happen exclusively through the bake script. That single flag enforces a clean separation between authoring and serving: a bug in a route handler cannot corrupt my posts, because the process serving pages has no permission to write to them. The database is a build artifact. The Markdown files are the source of truth, and they're in git.
#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.