Notes/note

How this site is built

Every technical decision on this site came from one constraint: it has to be something I can still reason about in two years, on an evening, without rebuilding my mental model first. That rules out more than it sounds like it does.

Static output, no production runtime

The site is Astro in static output mode. The build produces HTML, CSS, and images; nginx serves them. There is no Node process in production, no database, no CMS, and no server-rendered route. The failure modes of a directory of files behind nginx are the failure modes of nginx.

This is not asceticism. It is that every runtime you add in production is a thing that can be down at 2am, and the number of pages here does not justify one.

Almost no client-side JavaScript

The site ships one small script, served from its own origin: a theme toggle that persists a choice, and a search dialog that fetches a static JSON index. Everything else is HTML and CSS. Every page is fully readable and navigable with that file blocked — the nav is a plain <nav>, the theme still follows prefers-color-scheme, and search degrades to browsing.

That restraint is what keeps the content security policy tight:

default-src 'none'; script-src 'self' 'sha256-…'; style-src 'self';
img-src 'self'; font-src 'self'; connect-src 'self'; base-uri 'none';
frame-ancestors 'none'; object-src 'none'

The only inline script is the pre-paint theme read, allowed by hash rather than by 'unsafe-inline'. default-src 'none' is a policy most sites cannot adopt, because one analytics snippet or one CDN font starts a queue of exceptions. Having almost nothing to allow is what makes the strict policy affordable.

System fonts

font-src 'self' and no self-hosted webfont means the display face is a stack of system serifs — Iowan Old Style, Palatino, Georgia. It renders differently on different machines, which is a real cost, and it is the reason there is no layout shift and no font request on any page load. For a site whose job is to be read once by someone who was sent a link, that trade is not close.

Where the constraints leak

Two places, and I would rather name them than pretend the design is clean.

The social preview image is generated, not hand-made — an SVG rendered to PNG through sharp by the same CLI that runs link and secret checks over the build output. It has to be regenerated by hand whenever the headline changes, and nothing enforces that it was.

And the contact path needs a form handler, which means one origin allowed in form-action and a Cloudflare Worker in front of the delivery provider so the destination address never appears in the page source. That is the only piece of this architecture that is not a file on a disk.

What this is not an argument for

None of this generalizes to an application. A personal site is the rare case where the requirements genuinely are “render text, quickly, forever” — and the right architecture for that has been a directory of static files for about thirty years. The discipline worth borrowing is not the stack. It is deciding what the thing actually has to do before choosing what to run.

Sources

  1. astro.config.mjs
  2. infra/nginx/vtchevalier.com.conf
  3. src/styles/global.css
  4. package.json
ESC
↑↓ navigate↵ open