This site

2026-07-12

This site is a SolidStart application written in plain JavaScript, built by Bun, prerendered to static HTML, and served by nginx over HTTP/3 from a Hetzner box I administer myself. Every page you can visit exists as a file on disk before you ask for it. The server's job is to hand it over.

That is an unfashionable amount of infrastructure for a personal site, and an unfashionably small amount of framework. Both are deliberate.

Why prerendered SolidStart, in plain JavaScript

I wanted three things that rarely come together: static files at rest, a real component model while writing, and an escape hatch into interactivity when a page genuinely needs it. Most sites never need the hatch. I knew mine eventually would - there is a music tracker and a small RTS hiding in this site, and both are ordinary routes that hydrate into applications.

SolidStart does all three without ceremony. Routes are files, components compile down to close-to-the-metal DOM updates, and the prerenderer walks every route at build time and writes HTML. The result behaves like a 1998 site until the moment it behaves like an application, which is the exact gradient I wanted.

Plain JavaScript rather than TypeScript is the choice people question. I write TypeScript professionally and like it. But this codebase is a few thousand lines maintained by one person, and every type I would write here describes a thing I can hold in my head. The toolchain got smaller, the build got simpler, and the discipline moved into tests and into checking the built output - which you should be doing anyway, because "prerendered" is a claim about artifacts, not intentions. The one page that quietly failed that claim early on was the front page itself: a streaming race left its content in a template tag that only JavaScript unpacked. Every crawler saw an empty page. The fix was one line; the lesson was to grep the built HTML, not trust the framework's description of it.

What the zero-third-party rule costs and buys

Nothing on this site makes a request to anyone else's servers. No font CDN, no analytics, no comment widget, no tag manager. One typeface ships from this domain (a VGA bitmap font for the DOS corners of the site); the rest is your system's own.

The costs are real but small. Web fonts: none, so the typography leans on what your OS ships, which is better than its reputation. Analytics: the server's own access log, summarized weekly by a cron job into an email to me - counts and pages, no fingerprints, and your browser is never asked to participate. Comments: my inbox. A CDN would shave some milliseconds off for readers far from Germany; HTTP/3 and sixteen-kilobyte pages shave most of them back.

What it buys is the whole point. There is no consent banner because there is nothing to consent to. There is no supply chain to audit at 2 a.m. because there are no third-party scripts to compromise. The privacy claims on the colophon are checkable in your browser's network tab in ten seconds, which is the only kind of privacy claim worth making.

The nginx and HTTP/3 setup

nginx terminates TLS and QUIC and proxies to the app server on localhost, which mostly just serves the prerendered files. Hashed assets get a one-year immutable cache; HTML gets no-cache, so a deploy is visible on the next refresh. The headers are strict enough that the browser has little room to be creative: HSTS, nosniff, a content security policy that names this origin and nothing else, frame-ancestors none.

Two small courtesies live in the config. If you fetch the CV with curl or wget, you get redirected to a plain-text version, because if you are in a terminal I should not hand you a JavaScript application. And if the app server is ever down, nginx serves a fully self-contained error page in the style of a certain blue screen, because failure is no excuse for being boring.

What I would change

Less than I expected, so far. The honest list:

The CV exists in several hand-synced copies - the web page in three languages, a plain-text version, a PDF printed at deploy time. The sync is a discipline, not a mechanism, and discipline is the thing that fails first. If the count grows again, it becomes a build step.

The Bun-native server preset was not ready when I built this, so Bun runs the Node-flavored build instead. It works; it offends the part of me that wanted the stack to be one thing all the way down. I check back every few versions.

And I would decide about TypeScript the day this stops being a one-person codebase, which is to say: probably never, and I have left the door open anyway.

The rest of the machinery - the mail server, the palette rules, the DOS prompt you may have found by now - earns its own field notes. This one is just the ground floor.