A site this size doesn't need a framework. It's a handful of pages that change a few times a year, so the tooling should be about as big as the problem: an editor, Git, and a host that serves files. No package manager, no bundler, nothing to keep patched, and nothing that will refuse to build in two years' time.

What's in the repository

The header and footer are repeated on every page instead of being stitched in by a template engine. At this scale that's a fair trade: a find-and-replace now and then, in exchange for every page being plain HTML you can open straight from disk.

Hosting on Cloudflare Pages

Pages is connected to the Git repository. The project settings are about as short as they can be:

Framework preset:        None
Build command:           (blank)
Build output directory:  .
Production branch:       main

A push to main publishes the site. Other branches get their own preview URLs, which makes it easy to check a change on a phone before it goes live.

Security headers

Static doesn't automatically mean safe, so _headers locks the browser down to exactly what the site needs:

/*
  Content-Security-Policy: default-src 'self'; img-src 'self' data:;
    style-src 'self' 'unsafe-inline'; script-src 'none'; object-src 'none';
    base-uri 'none'; form-action 'none'; frame-ancestors 'none'
  Permissions-Policy: camera=(), microphone=(), geolocation=()
  Referrer-Policy: strict-origin-when-cross-origin
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY

Caching, and why some files have version numbers

Everything under /assets/ is served with a one-year, immutable cache:

/assets/*
  Cache-Control: public, max-age=31536000, immutable

Browsers won't even ask whether those files have changed, which makes repeat visits fast. The catch is that a changed file has to get a new name, or visitors will keep the old copy for a year. That's why the logos are …-v2.svg and the social preview image is og-image-v2.png.

The stylesheet deliberately lives outside that folder, at /css/site.css, so it picks up Pages' normal caching and a style change shows up on the next visit without any renaming.