How this site is built
I wanted a site I could publish to with git push and then not think about. No CMS to log into, no database, no server to patch. This is what I landed on.
#The shape of it
Every page is pre-rendered to static HTML at build time. There is no server at runtime — Cloudflare hands visitors files off its edge, which is why pages arrive fast from anywhere.
| Concern | Choice | Why |
|---|---|---|
| Framework | Next.js, output: 'export' | Good MDX story, emits a plain static out/ |
| Styling | Tailwind v4 with CSS tokens | Theme lives in variables, not a JS config |
| Content | MDX files in content/ | Posts are diffable; review works like code |
| Highlighting | Shiki via rehype-pretty-code | Runs at build, so no JS shipped to colour it |
| Hosting | Cloudflare Pages | Free, global, deploys from CI |
#Content is just files
A post is one .mdx file with frontmatter on top:
---
title: A post about something
date: '2026-07-28'
summary: One or two sentences that show up in listings and RSS.
tags: ['rust']
---
The body starts here. Markdown, plus components when I need them.The build reads that directory, validates the frontmatter, and fails loudly if a required field is missing. I would rather break the build than publish a post with no summary and find out from RSS.
#Code blocks
Highlighting happens once, at build time. Adding {3} after the language emphasises a line, which is useful when the interesting part is buried:
function readingMinutes(body: string): number {
// Strip code so snippets don't inflate the estimate.
const prose = body.replace(/```[\s\S]*?```/g, ' ').replace(/`[^`]*`/g, ' ')
const words = prose.split(/\s+/).filter(Boolean).length
return Math.max(1, Math.round(words / 220))
}Nothing ships to the browser for this. The colours are inline CSS variables and a few rules in globals.css pick the light or dark set.
#The GitHub list builds itself
The projects page pulls my public repos from the GitHub API — but at build time, not in the visitor's browser. That means no rate limits for readers, no loading spinner, and no layout shift. The trade-off is that stars are as fresh as the last deploy, which for a personal site is fine.
The one rule I gave myself: if GitHub is down or rate-limited, the build must still succeed with an empty list. A social widget is not worth a failed deploy.
#Deploys
Pushing to main runs typecheck and build, then wrangler pages deploy. Pull requests get a preview URL and a Lighthouse run, so a regression in performance or accessibility shows up as a comment rather than a surprise.
That is the entire system. Two commands to remember:
npm run dev # write
git push # publish