Fix chart distortion, 404 cache poisoning, and invisible prose links - #6
Merged
Conversation
Four defects, then the waste and drift they sat next to. Bugs: - The Worker chose Cache-Control by path without checking status, so a 404 under /_astro/* or /grain.png was served immutable for a year. A half-rolled deploy or stale HTML pointing at a departed hashed asset would pin that error in every cache downstream. Errors now get 60s must-revalidate. The styled 404 also covers every method now, with a body only on GET, so HEAD and POST stop leaking the asset layer's own 404 payload. - Frame 2 pinned a 1100x480 viewBox to a fluid width with preserveAspectRatio="none". At a 375px viewport that is 0.45 horizontal distortion: cursor dots rendered as 3x6.7 ellipses and the event labels collapsed into overlapping mush. The viewBox now tracks the element's own CSS box, so both axes scale by exactly 1. Narrow layouts drop the event labels and thin the year axis rather than overprinting them. Measured on mount and on resize rather than with a ResizeObserver, which could not be shown to fire reliably. - The global anchor reset strips colour and underline from every link, and nothing restored them inside running text. Three prose links — the privacy policy, the contact address, the wallet-security guidance — were pixel-identical to the paragraph around them. Gold cannot carry this alone (1.48:1 against body text), so the fix is an underline. - The mobile menu is a role="dialog" aria-modal="true" panel, but focus never left <body> on open, which left the Tab trap inert: it only engages once focus is already on the first or last item. Focus now moves into the panel, and the skip link and nav links join main and footer in going inert. The Menu button stays live as the dialog's own control. Waste (total build output down 25.2%, 1,228,836 -> 919,412 bytes): - Fontsource emits a legacy .woff beside every .woff2. A Vite transform strips the fallback before the CSS is read, so 259,868 bytes of font that no browser capable of running this site would request are never emitted at all. - The resource filter was a 3.1KB inline block in the shared layout: seventeen pages paid for it so three could use it. It now ships with ResourceFilter.astro. Its two vestigial window globals are gone. - The Network Clock polled /pulse.json every five minutes, but that file is a build artifact and cannot change between deploys — roughly 288 requests a day per open tab for a guaranteed identical body. Removed, along with the flash affordance the poll drove. - fetch-pulse fetched, stored, and typed a bitnodes node count that nothing rendered. generate-grain used Math.random(), rewriting a 64KB PNG on every refresh; it is seeded now and byte-identical across runs. - Ten unused design tokens, plus four aliases that were exact duplicates of an existing size or tracking value and implied a granularity the design did not have. No visual change. Copy and data: - The primer promised five steps and listed six nouns; objections promised nine and enumerated eight, omitting Better Technology. Both counts are derived now rather than asserted. - The paths page kept its route questions in a hand-written map keyed by dataset id. A renamed path would have rendered an empty <h2>. They live in rabbit-holes.ts, where README already says they belong. - Frame 2's method note omitted the chart's largest assumption: the dollar line is an index and the bitcoin line is a lump-sum position entered at the July 2010 anchor. It says so now. - The archive note claimed nothing had been changed directly beside "Translated from Chinese". Two sources of truth for site freshness had already drifted a month apart; both derive from routes.json now. Security: - script-src names the sha256 of every inline script the build emits, so 'unsafe-inline' survives only as a fallback for browsers predating hash support. Verified in a browser against the real Worker: all nine scripts execute, JSON-LD parses, islands hydrate, no violations. The audit gained checks for CSP hash freshness, shipped .woff, the prose link affordance, and filter-script scope; each was confirmed to fail when its subject is broken. Its ROOT no longer uses a percent-encoded URL.pathname, which broke on any checkout path containing a space. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four defects, then the waste and drift they sat next to.
Bugs:
The Worker chose Cache-Control by path without checking status, so a 404 under /_astro/* or /grain.png was served immutable for a year. A half-rolled deploy or stale HTML pointing at a departed hashed asset would pin that error in every cache downstream. Errors now get 60s must-revalidate. The styled 404 also covers every method now, with a body only on GET, so HEAD and POST stop leaking the asset layer's own 404 payload.
Frame 2 pinned a 1100x480 viewBox to a fluid width with preserveAspectRatio="none". At a 375px viewport that is 0.45 horizontal distortion: cursor dots rendered as 3x6.7 ellipses and the event labels collapsed into overlapping mush. The viewBox now tracks the element's own CSS box, so both axes scale by exactly 1. Narrow layouts drop the event labels and thin the year axis rather than overprinting them. Measured on mount and on resize rather than with a ResizeObserver, which could not be shown to fire reliably.
The global anchor reset strips colour and underline from every link, and nothing restored them inside running text. Three prose links — the privacy policy, the contact address, the wallet-security guidance — were pixel-identical to the paragraph around them. Gold cannot carry this alone (1.48:1 against body text), so the fix is an underline.
The mobile menu is a role="dialog" aria-modal="true" panel, but focus never left on open, which left the Tab trap inert: it only engages once focus is already on the first or last item. Focus now moves into the panel, and the skip link and nav links join main and footer in going inert. The Menu button stays live as the dialog's own control.
Waste (total build output down 25.2%, 1,228,836 -> 919,412 bytes):
Fontsource emits a legacy .woff beside every .woff2. A Vite transform strips the fallback before the CSS is read, so 259,868 bytes of font that no browser capable of running this site would request are never emitted at all.
The resource filter was a 3.1KB inline block in the shared layout: seventeen pages paid for it so three could use it. It now ships with ResourceFilter.astro. Its two vestigial window globals are gone.
The Network Clock polled /pulse.json every five minutes, but that file is a build artifact and cannot change between deploys — roughly 288 requests a day per open tab for a guaranteed identical body. Removed, along with the flash affordance the poll drove.
fetch-pulse fetched, stored, and typed a bitnodes node count that nothing rendered. generate-grain used Math.random(), rewriting a 64KB PNG on every refresh; it is seeded now and byte-identical across runs.
Ten unused design tokens, plus four aliases that were exact duplicates of an existing size or tracking value and implied a granularity the design did not have. No visual change.
Copy and data:
The primer promised five steps and listed six nouns; objections promised nine and enumerated eight, omitting Better Technology. Both counts are derived now rather than asserted.
The paths page kept its route questions in a hand-written map keyed by dataset id. A renamed path would have rendered an empty
. They live in rabbit-holes.ts, where README already says they belong.
Frame 2's method note omitted the chart's largest assumption: the dollar line is an index and the bitcoin line is a lump-sum position entered at the July 2010 anchor. It says so now.
The archive note claimed nothing had been changed directly beside "Translated from Chinese". Two sources of truth for site freshness had already drifted a month apart; both derive from routes.json now.
Security:
The audit gained checks for CSP hash freshness, shipped .woff, the prose link affordance, and filter-script scope; each was confirmed to fail when its subject is broken. Its ROOT no longer uses a percent-encoded URL.pathname, which broke on any checkout path containing a space.