Fathom sends its pageview as an image request. script-src named the
origin and img-src did not, so the script loaded and the one thing it
loads to do was blocked — visible only in the browser console, with a
200 on every response and no pageview at the other end.
The console said it plainly:
Loading the image 'https://cdn.usefathom.com/?h=...&sid=ZPKDEHCV...'
violates the following Content-Security-Policy directive: img-src
'self'
A test now asserts the origin appears under all three directives it
actually uses, because nothing on this side of the wire can tell that it
does not.
Two changes to what the live site sends.
Analytics: Fathom's tag goes in every template's <head>, deferred. It is
cookieless and collects nothing about a person, so no consent banner —
but it is a third party, so the CSP names cdn.usefathom.com for script
and connect rather than opening the door generally, and the tests that
said 'no script at all' now say 'no script this site depends on, and no
origin nobody chose'.
Caching: the host concatenates a header two matching rules both set
rather than overriding, so /* and /static/* each setting Cache-Control
sent 'max-age=0, must-revalidate, max-age=31536000, immutable' on the
stylesheet — first max-age wins, and the year-long cache never happened.
Measured on the live site, which is where the from-memory assumption in
task 32 said to check it. Now /static/* is the only rule that sets it
and HTML takes the host's revalidating default; the post-deploy checks
in site/README.md verify both ends.
Fills the middle of the site. Nine routes, every body a heading slice of
AGENTS.md or README.md, and the article layout given the furniture the
design calls for.
The manifest gains the two concepts nothing covered: /concepts/task-files/
(the header format, from AGENTS.md's own section) and /concepts/adapters/
(the adapter summary, which is the other half of the three-layer law).
/concepts/stages/ now runs through "Moving a task", because the five
directories and moving between them are one idea.
The layout:
- A lede under the title — the one sentence an article authors, taken
from the manifest's `description` or an explicit `lede` where the two
want different words. A slice starts mid-document; a reader arriving
from the nav is owed a line saying what they are looking at.
- Prev/next at the foot, walking the sidebar's own order so the arrows
and the rail cannot disagree. Pages with no section (the landing page,
the 404) are not on the flow.
- "Edit this page on GitHub" anchors to the section the page was cut
from, built from the same `from` heading the slice starts at.
Two bugs the new pages found:
- string.Template substitutes inside HTML comments, so a comment naming
the body placeholder emitted the whole body twice and closed itself
early on the first `-->` in it.
- Promotion could produce a second <h1>. A slice that deliberately runs
past its own section carries headings at the `from` level, and those
promoted to h1 on a page that already had one. Promotion now stops at
h2, where they read as peers — which is what putting them on one page
said in the first place.
tests/test_site_pages.py covers the furniture on the real built site:
the routes, the layout, the sidebar marking one page, the contents list
being exactly the body's own h2s in order, the prev/next chain end to
end, the edit link's anchor, the six landing-page doors, and a table, a
fenced block and a nested list surviving the renderer. The scratch-repo
helper now copies every file a slice links to, since the builder checks
those exist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Turn design 1b (Dockside) into `/`: a terminal hero, the claim README.md
already makes, six doors, and a strip at the foot. The layout is task
31's; what this commit is really about is that nothing factual on the
page is typed by hand.
- **Two facts are read, not written.** `site/build.py` reads the install
one-liner out of README.md's "Install into a repo" block and the
version out of `manager/core/VERSION`, and offers them to the template
as `$install_block` and `$version`. A renamed section, a missing
VERSION or an install section that lost its command block stops the
build, exactly as a renamed heading already did. `pages.json` loses
its `version` key, and a build refuses one if it comes back.
- **A dead internal link stops the build.** Every href a rendered page
emits — a door as much as a link inside a slice — must resolve to a
route in the manifest or a file in `static/`/`root/`. The check runs
after rendering and before writing, so a bad link leaves the last good
build standing rather than shipping a 404 with a nice typeface.
- **Six real doors.** `pages.json` grows the routes they open: install
and first run, the five stages, agents on the board, PRs and review,
team mode, the three-layer law. They are heading slices, i.e. the
stub routes task 33 expected and task 34 will re-cut.
- **The terminal is a transcript.** README.md's own command, then lines
install.py and board.py really print, with the abridgement declared in
the terminal's title bar. `tests/test_site_landing.py` holds every one
of those lines against the source that prints it, so a reworded prompt
fails the suite instead of quietly making the page fiction.
- **No fake telemetry.** Turn 1's "most opened this week" strip becomes
the version, read from VERSION, and a link to the releases.
The landing page no longer carries a generated body, so the tests that
read one from it now read `/concepts/stages/`, and the two scratch repos
in the suite copy VERSION alongside the markdown.
python3 -m unittest: 407 tests, OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
site/wrangler.jsonc puts site/dist/ behind bench.12vectors.com as static
assets. No `main`: the site is files, and a Worker with no script is the
cheapest correct way to serve them.
html_handling force-trailing-slash, so /x redirects to /x/ — the
url the pages link and rel=canonical names. One
page, one address; no url ends in .html.
not_found_handling 404-page, so an unknown path gets dist/404.html
with a 404 status rather than the landing page
with a 200.
routes bench.12vectors.com as a custom domain. Cloudflare
takes the hostname at the zone level and makes the
DNS record; nothing else on 12vectors.com moves.
site/root/_headers carries the response policy. HTML revalidates on
every view, so a deploy is visible on the next reload without anyone
clearing a cache; /static/* is kept for a year and never re-checked,
which is safe because the stylesheet and icon urls carry a hash of their
contents. The general rule is written first and the specific one second,
so a host that merged the two instead of overriding would still land on
max-age=0 — the safe side. Alongside it the baseline a public page owes:
nosniff, a referrer policy, a year of HSTS without preload,
X-Frame-Options, and a default-src 'none' CSP that makes "no analytics,
no third-party anything" something the browser enforces rather than
something a test asserted once.
Deploys are run by hand, as releases already are — no Cloudflare token
in repository secrets, no first deploy pipeline. site/README.md names
the account, the Worker, the route and the four-command sequence, plus
the four things to check after a deploy that no test here can reach.
The tests cover everything before Cloudflare: that the config says what
the site needs, that the build writes the files it names, and that
wrangler.jsonc, pages.json and README.md cannot drift apart about which
domain this is. A live response is not among them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>