diff --git a/site/README.md b/site/README.md index 4358569..6409878 100644 --- a/site/README.md +++ b/site/README.md @@ -18,13 +18,98 @@ python3 site/build.py # → site/dist/ ``` `site/dist/` is gitignored — it is output, rebuilt on every deploy. -Deployment is task 32's; this directory only has to produce the files. `site/` never reaches a host repo: releases ship exactly what `../manager/core/release-manifest` lists, and it does not list this directory. `tests/test_release_artifact.py` asserts that out loud rather than leaving it to inference. +## Where the site lives + +| | | +| --- | --- | +| Host | Cloudflare Workers, [static assets](https://developers.cloudflare.com/workers/static-assets/) — no script, no KV, no database | +| Account | the Cloudflare account holding the `12vectors.com` zone. `npx wrangler whoami` must list it before a deploy will work | +| Worker | `bench-site` | +| Route | `bench.12vectors.com`, a **custom domain** — Cloudflare owns the hostname at the zone level and creates the DNS record itself | +| Served from | `site/dist/`, uploaded whole on every deploy | +| Config | `site/wrangler.jsonc` (routing) and `site/root/_headers` (caching, security) | + +Deploys are run by hand, by a person, exactly as releases are +(`../release.sh`). There is no deploy pipeline and no Cloudflare token in +repository secrets; adding a GitHub Action on merge to `main` is a +separate decision with a separate cost, and a follow-up card. + +## Deploying + +From a clean checkout, four commands. Re-running the whole sequence is +safe: the build empties and rewrites `dist/`, and `wrangler deploy` +replaces the Worker's assets rather than adding to them. + +```bash +python3 -m pip install -r site/requirements.txt # once per machine +python3 site/fetch-fonts.py # once per checkout +python3 site/build.py # → site/dist/ +npx wrangler@4 deploy --config site/wrangler.jsonc +``` + +`npx wrangler@4 login` first, once per machine, against an account that +can see the `12vectors.com` zone. Paths inside `wrangler.jsonc` are +relative to that file, so the command works from anywhere in the repo. + +The first deploy is the one that takes the hostname over. It creates the +DNS record for `bench.12vectors.com` and routes it to the Worker; +anything else answering on that name stops answering. Every deploy after +it is an asset upload. + +### Preview it locally + +```bash +python3 site/build.py +npx wrangler@4 dev --config site/wrangler.jsonc # → http://localhost:8787 +``` + +`dev` serves `dist/` through the same static-assets router as production, +so trailing-slash redirects and the 404 page behave as they will live. +The custom domain is ignored locally. + +### After a deploy, check these four + +The things this repository's tests cannot reach, because they are +answers rather than files: + +1. `https://bench.12vectors.com/` serves the landing page over TLS. +2. `https://bench.12vectors.com/concepts/claiming-a-card` redirects to + the same path with a trailing slash, and no url anywhere ends in + `.html`. +3. A path that does not exist — `/nope/` — renders the site's own 404 + page **with a 404 status**, not the landing page with a 200. +4. `curl -sI https://bench.12vectors.com/` shows + `x-content-type-options`, `referrer-policy`, + `strict-transport-security` and a `cache-control` that revalidates. + +## How it is served + +- **One url per page.** `html_handling: "force-trailing-slash"` redirects + `/concepts/claiming-a-card` to `/concepts/claiming-a-card/`, which is + what the pages link and what `` names. Pages are + written as `/index.html`, so no url ends in `.html`. +- **A real 404.** `not_found_handling: "404-page"` serves `dist/404.html` + with a 404 status. That page is a normal manifest entry (`/404.html`, + layout `notfound`) — the site's own design, its own nav, and a link + back to the landing page. +- **Two caching policies, because there are two kinds of file.** HTML + revalidates on every view, so a deploy is visible on the next reload + with nobody clearing anything. Everything under `/static/` is kept for + a year and never re-checked, which is only safe because the stylesheet + and the icon are linked with a `?v=` of their own contents: + change the file and the url changes with it. +- **Baseline headers, no third parties.** `nosniff`, a referrer policy, + a year of HSTS, `X-Frame-Options`, and a Content-Security-Policy of + `default-src 'none'` with `'self'` for styles, fonts and images. The + site collects nothing and loads nothing from anywhere else; the CSP is + that promise in a form the browser enforces. + ## What is where | Path | What it is | @@ -33,6 +118,8 @@ than leaving it to inference. | `build.py` | The generator — slicing, drift detection, rendering, links | | `templates/` | One `string.Template` per layout (`$name`, `$$` for a literal dollar) | | `static/` | Copied to `dist/static/` verbatim: the stylesheet, the icon, the fonts | +| `root/` | Copied to the **top** of `dist/` verbatim: `_headers`, which the host reads and never serves | +| `wrangler.jsonc` | The Worker: assets directory, url handling, 404, custom domain | | `requirements.txt` | `markdown-it-py`, pinned. The only dependency | | `fetch-fonts.py` | Downloads the self-hosted woff2 files, once | @@ -51,12 +138,15 @@ than leaving it to inference. } ``` -- **`path`** starts and ends with `/`; `/x/y/` is written to - `dist/x/y/index.html`. +- **`path`** starts with `/` and ends with `/`; `/x/y/` is written to + `dist/x/y/index.html`. A route that names an `.html` file instead is + written to exactly that path — `/404.html` is the only one, and it + exists because the host looks for that literal filename. - **`layout`** names a file in `templates/`. - **`section`** groups the page in the nav and the sidebar. The IA is read out of this file in this file's order — nothing is derived from - the directory layout. + the directory layout. A `null` section keeps the page out of both, + which is how the 404 page stays off the nav. - **`source`** is repo-relative, or `null` for a landing page whose body is authored in its template rather than sliced. - **`from`** is the heading the slice starts at. It is matched on the @@ -78,7 +168,8 @@ Each of these exits non-zero with a message naming the route: - a slice that comes out empty; - a markdown link to a repo path that does not exist, or that escapes the repo — a dead relative link must never reach the site; -- a template placeholder the builder does not supply. +- a template placeholder the builder does not supply; +- a file in `root/` that a route would also write. Repo-relative links that *do* resolve are rewritten: to a site route if `link_routes` maps the file to one, otherwise to the file on GitHub. diff --git a/site/root/_headers b/site/root/_headers new file mode 100644 index 0000000..967d760 --- /dev/null +++ b/site/root/_headers @@ -0,0 +1,42 @@ +# Response headers for bench.12vectors.com. +# +# Read by Cloudflare Workers static assets at deploy time — the file is +# consumed, never served — and copied here from site/root/ by +# site/build.py, because the host looks for it at the root of the build +# and nowhere else. +# +# Rules apply in order and a later rule wins on a header it repeats. The +# two blocks below are written so that even a host that merged them +# instead would land on the safe side: HTML would still revalidate. + +# Everything, so that no page can ever forget one of these. +# +# nosniff a text/plain file must not become a script +# Referrer-Policy a full url is never sent to another origin +# HSTS one year, this host and anything below it. No +# `preload`: that is a submission to browser +# vendors and a commitment this card did not make +# X-Frame-Options nothing here is meant to be framed +# CSP the runtime form of the site's own promise — +# no analytics, no font CDN, no third-party +# anything. `default-src 'none'` means an asset +# must be named below to load at all, and no +# 'unsafe-inline' anywhere means an injected +#