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
- One HTML file per page:
index.html,about/index.html,notes/…/index.htmland so on. Cloudflare Pages serves directories as clean URLs, so/about/just works. - A single shared stylesheet at
/css/site.css. - Images, logos and the linkding theme under
/assets/. - Favicons, a web app manifest,
robots.txt,sitemap.xmland an Atom feed for the notes. - A
_headersfile, which Pages reads to attach HTTP headers to responses.
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
script-src 'none'is the important one. The site doesn't run any JavaScript, so the browser is told to refuse all of it. Even if someone managed to inject markup, there would be nothing to execute.default-src 'self'means fonts, images and styles only come from this domain. There are no third-party requests, which is also why there's no cookie banner: there's nothing to consent to.frame-ancestors 'none'andX-Frame-Options: DENYstop other sites from embedding these pages in a frame.form-action 'none'andbase-uri 'none'close two small loopholes that injected markup could otherwise use.Permissions-Policyturns off camera, microphone and location access, which a page of text never needs.
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.