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.