How this site is built
This site has three constraints: it loads fast, it sends nothing about you to anyone, and it stays simple enough that I can maintain it in an evening.
Static pages, no JavaScript
The pages are built with Astro into plain HTML files. Astro ships no JavaScript unless a component asks for it, and none of mine do. The home page weighs a few kilobytes, styles included.
Nothing loaded from elsewhere
There are no web fonts, no analytics and no CDN. Text uses fonts already installed on your device: Charter on Apple systems, Sitka or Cambria on Windows. Your browser only ever talks to this server.
A strict Content Security Policy
Astro inlines small stylesheets in the page. Instead of allowing every inline style, the build computes a hash of each one and writes it into a per-page policy:
export default defineConfig({
security: {
csp: {
directives: ["default-src 'self'", "img-src 'self' data:"],
},
},
});
The web server adds what a <meta> policy cannot carry, such as frame-ancestors 'none', along with Referrer-Policy: no-referrer and HSTS.
The portrait
The portrait on the home page is text. A script crops a photo, converts it to grayscale, and maps each cell to one of eight characters, from a space to #. It produces two versions: dark characters on light paper, and light characters on a dark background, so the face never appears inverted in dark mode. The photo itself is not published.
Serving it
Caddy serves the files from a container. The official Caddy images are built on Alpine and run as root, so mine takes the release binary, checks it against its published SHA-512, and puts it on a distroless Debian image: no shell, an unprivileged user, a read-only filesystem. It runs under Podman on my laptop and Docker on the server.