Deploys restored, and the first blog post where it belongs

Cloudflare had refused two releases because its npm could not install the lockfile, so the live site stayed a release behind. That is fixed, the life update post now renders as a real blog article, and the security policy is back to naming exact script origins.

The dependency security pass on 24 October cleared every production audit finding. It also left the site unable to deploy. The two releases after it, the lockfile synchronisation and the blog publication, both failed in Cloudflare before a single file was built, so production stayed on the release before them and the new blog post never went live.

Why Cloudflare refused the lockfile

Cloudflare’s build image runs npm 10. The security pass added an overrides entry that forced undici 7.30.1 underneath miniflare, while miniflare itself asks for exactly 7.29.1. npm 11, used locally, accepts that; npm 10’s clean install does not, and stops with “lock file’s undici@7.30.1 does not satisfy undici@7.29.1”.

miniflare is the local Worker simulator behind npm run preview. It is a development dependency, never part of what the site serves, and the production audit that guards releases already excludes development dependencies. The override bought nothing that audit measures, so it is removed rather than worked around. The production audit still reports zero findings, and a clean install under npm 10.9.2 — the version Cloudflare runs — now succeeds. One high-severity undici advisory remains inside the local simulator only.

The life update post

The post had been added as src/pages/blog/2026-10-24-life-update-after-moving.md. Astro treats a Markdown file in src/pages as a standalone page, so it rendered without the site header and footer, with no canonical address or robots directives, and it appeared in neither the blog index nor the sitemap — those all come from the blog content collection in src/content/blog.

The post now lives in that collection, unchanged except for one line: its body opened with a heading identical to its title, and the article template already renders the title as the page’s only H1. It is served at /blog/life-update-after-streaming/, the address its own slug names.

Publishing the collection had also set draft: false on src/content/blog/_collection.md. That file holds the collection’s description, not an article, so it was being published as a page of its own at /blog/collection/. It is a draft again.

The security policy

script-src had gained https://*.clarity.ms. The Worker’s regression suite forbids wildcards in script-src and fails on it; validation never reached that suite only because an earlier step was already failing. The site does not need it either: Clarity’s tag loads from www.clarity.ms and its script from scripts.clarity.ms, both already named. The Google Fonts origins added to style-src and font-src are removed too, because nothing on the site loads Google Fonts. connect-src is unchanged.

Housekeeping

  • Three backups of package.json and package-lock.json taken during the security pass had been committed. They are deleted, and .gitignore now keeps astro/*.before-*.json out of the repository.
  • The presence-card check that reads connect-src assumed a double-quoted string and failed when the directive became a template literal, though the policy itself was correct. That is the step where validation stopped on the last two releases. It now reads either form.

Verification

  • npx npm@10.9.2 ci --dry-run, matching Cloudflare’s build image, failed with the error above before the fix and succeeds after it. npm audit --omit=dev --audit-level=high reports zero.
  • npm run test:delivery: 181 of 181. npm run build: 39 pages. npm run test:worker: 77 of 77, including the wildcard rule it had been failing. npm run test:built: 284 of 284. npm run check:feeds: social, community and live. The release audit in tools/audit.py passed across 38 canonical pages; before the fix it failed on the blog’s sitemap inventory.
  • The built article has one H1, the shared header and footer, and a self-canonical; it is listed on /blog/; /blog/collection/ is no longer built.
  • tools/indexing-surface.json was rewritten deliberately. Its only route changes are the new article and this entry, each with its sitemap entry.
  • Against npm run preview: npm run test:browser, 0 failures across 38 routes, and npm run test:a11y, 0 failures across 86 checks. The presence card still opens its Lanyard socket under the tightened policy, with no CSP violations or console errors.

Follow-up: the old address now redirects

The post was live for about four hours at /blog/2026-10-24-life-update-after-moving/ before this release, and the IndexNow submission ran on that deploy, so the address may already be in a search index or a shared link. After the move it answered 404.

The Worker now keeps a short list of moved addresses and answers each with a permanent 301 to its replacement, here /blog/life-update-after-streaming/. It accepts the address with or without its trailing slash or index.html, keeps any query string such as a campaign tag, and runs after the HTTPS redirect, so the new location is always an https address.

tools/test-worker.mjs asserts the redirect in each of those forms. A new built-output check, tools/test-moved-paths.mjs, fails if a moved address ever points at a page the build does not produce or the sitemap does not list, or if the old address is built again and would be hidden behind the redirect.

Verified with npm run test:delivery (184 of 184), npm run build, npm run test:worker (77 of 77, including the redirect assertions), npm run test:built (287 of 287, including the three new checks), the release audit across 38 canonical pages and npm run check:feeds. Against npm run preview, each form of the old address answered 301 with the new location and query string intact, following it reached a 200, npm run test:browser reported 0 failures across 38 routes and npm run test:a11y 0 failures across 86 checks.