My personal site. One quiet column of text — no framework, almost no JavaScript, and the source is meant to read as well as the page.
The content is hand-written static HTML, so the text paints before any script runs. Critical path is roughly 5 KB gzipped. Everything interactive is a small progressive enhancement that loads only when you reach for it; turn JavaScript off and the whole site still reads.
- Type — Newsreader,
self-hosted and glyph-subset to the characters the site actually uses, shipped
as two optical cuts (a display cut for the name, a text cut for body). A
metric-matched
@font-facefallback gives a measured CLS of 0 on font swap. System monospace for labels and metadata. - Color — warm paper / warm near-black, chosen by
prefers-color-schemeor the theme toggle, with a single teal accent used only on focus rings, markers, and active states. Hierarchy comes from weight and gray value, not size. All text meets WCAG AA. - Motion — one staggered fade on load and cross-page
View Transitions,
both gated behind
prefers-reduced-motion. Nothing moves on scroll.
All code-split, so they only load on the page that uses them:
- A ⌘K command palette (native
<dialog>, fully keyboard-driven) to jump to any page, project, or post. - A three-state theme toggle (system / light / dark) with a no-flash inline script.
- Two explorables embedded in posts: an Oath state machine that runs the real on-chain classification logic in the browser, and a RAWRB demo where you break a model's chain-of-thought and watch the answer (not) follow.
Each post is a markdown file in writing/posts/. A build step
(scripts/build-writing.mjs) turns the folder into the /writing index, the
post pages, a /colophon, an RSS feed, and a sitemap. Adding a post is dropping
a file and rebuilding.
The colophon documents the engineering in more detail — the craft is part of the content.
npm install
npm run dev # vite dev server (regenerates writing pages first)
npm run build # generate pages → tsc -b → vite build → dist/
npm run preview # serve the build
npm run lintDeployed on Vercel from main.