pcbjam/deploy/site/README.md

108 lines
4.9 KiB
Markdown
Raw Normal View History

chore(deploy/site): retire the migration scaffolding, keep the health check The Vercel -> Cloudflare Pages move is done and the Vercel project is deleted, so the one-shot scripts have no remaining purpose. Nothing in CI ever called them — deploy-site.yml runs npm ci / test / build / pages deploy inline — so this removes 10 files and orphans nothing. Deleted: 00-baseline (refused to run without x-vercel-id, so permanently unrunnable), 01-preflight (proved Vercel state and API-token scopes), 07-dns-cutover (the phased cutover; in the end the records were attached through the dashboard, and the rules/apex phases went unused once we chose APEX_MODE=serve), 09-detach-vercel (its target project is gone), plus 03-ensure-project, 04-set-secrets, 05-deploy, 06-verify-deploy, 02-verify-local and 99-rollback, all either spent or duplicating CI. Their lib/cf-api.sh went with them: the survivors use wrangler, so the whole remaining path needs only `wrangler login` and no zone scopes. What is kept is the part with ongoing value: lib/parity.sh, the assertion set that caught five real defects during the migration — the live COOP/COEP bug on the post's canonical URL, the soft-404 Pages would have introduced, the cross-site form-POST guard Vercel had been providing for free, the missing immutable header, and HSTS max-age=0. "Does the page return 200" catches none of those. 08-verify-prod.sh becomes verify.sh, since the numbered sequence it belonged to no longer exists. It drops the stamp machinery, the dry-run plumbing and the Vercel-fallback messaging (there is no fallback now: recovery is promoting a previous Pages deployment), and gains --skip-dns / --skip-domains so it can be pointed at a single deployment via PROD_BASE before promoting it. The README is rewritten around the four invariants that fail SILENTLY — never widen _headers to /*, keep both URL forms of the Gerber post, never delete 404.astro, keep the cross-site form-POST guard — each with the reason, since the reason is the only thing that stops someone simplifying them back out. Verified after: 21 probes, 20 pass, 1 warn (HSTS max-age is 6 months vs Vercel's 2 years — on, just shorter), 0 fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
2026-07-27 15:08:56 +02:00
# www.pcbjam.com
feat(site): move the marketing site from Vercel to Cloudflare Pages www.pcbjam.com was the last piece of the stack on Vercel. It is now a Cloudflare Pages project (pcbjam-site) deployed by deploy-site.yml on every push to main touching site/** — content must not wait for a release tag. The Astro adapter is gone entirely: the build is pure static and the one dynamic route, /api/waitlist, is a Pages Function. Going adapter-free (rather than swapping in @astrojs/cloudflare, which has dropped Pages support and only targets Workers) removes three problems at once — no Astro/adapter major-version coupling, Footer.astro's build-time execSync keeps working because prerendering stays in Node, and image optimisation stays plain build-time sharp with no Cloudflare Images binding. Verified against a real Pages runtime (wrangler pages dev): 21/21 parity probes pass, versus 19/21 on live Vercel. The scripted runbook is in deploy/site/ — every mutating step is dry-run by default. Four behaviour differences were found by measurement and are handled here: - The blog post's COOP/COEP was already broken in production. vercel.json scoped the headers to the bare URL, but the page's own canonical is the trailing-slash form, which served 200 with no isolation headers — so search arrivals lost SharedArrayBuffer and the embedded Gerber viewer degraded. public/_headers covers both forms. - Pages answers unknown URLs with the homepage at HTTP 200 when the output has no 404.html — a soft-404 that invites indexing junk URLs as the homepage. Hence src/pages/404.astro. - Vercel's edge refused cross-site form POSTs ("Cross-site POST form submissions are forbidden"); Pages does not, and a cross-site <form> submit needs no CORS permission to be sent, so the allowlist cannot stop it. The Function reproduces the guard; JSON posts stay exempt as that is demo.pcbjam.com's allowlisted path. - Cache-Control: immutable on /_astro/* came from the Vercel adapter's generated route config, so it is now an explicit _headers rule. Secrets move to `wrangler pages secret put --project-name pcbjam-site` (RESEND_API_KEY, RESEND_SEGMENT_ID, WAITLIST_FROM_EMAIL); WAITLIST_ALLOWED_ORIGINS stays unset so the allowlist stays in code. Local dev reads .dev.vars, now gitignored — the root repo's **/.dev.vars does not cover a nested git repo. privacy.md and cookies.md named Vercel as a GDPR Art. 28 processor; those mentions are removed and the existing Cloudflare entry widened to cover website hosting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
2026-07-27 13:34:12 +02:00
The Astro marketing site + blog (`../../site`) on **Cloudflare Pages** (project
chore(deploy/site): retire the migration scaffolding, keep the health check The Vercel -> Cloudflare Pages move is done and the Vercel project is deleted, so the one-shot scripts have no remaining purpose. Nothing in CI ever called them — deploy-site.yml runs npm ci / test / build / pages deploy inline — so this removes 10 files and orphans nothing. Deleted: 00-baseline (refused to run without x-vercel-id, so permanently unrunnable), 01-preflight (proved Vercel state and API-token scopes), 07-dns-cutover (the phased cutover; in the end the records were attached through the dashboard, and the rules/apex phases went unused once we chose APEX_MODE=serve), 09-detach-vercel (its target project is gone), plus 03-ensure-project, 04-set-secrets, 05-deploy, 06-verify-deploy, 02-verify-local and 99-rollback, all either spent or duplicating CI. Their lib/cf-api.sh went with them: the survivors use wrangler, so the whole remaining path needs only `wrangler login` and no zone scopes. What is kept is the part with ongoing value: lib/parity.sh, the assertion set that caught five real defects during the migration — the live COOP/COEP bug on the post's canonical URL, the soft-404 Pages would have introduced, the cross-site form-POST guard Vercel had been providing for free, the missing immutable header, and HSTS max-age=0. "Does the page return 200" catches none of those. 08-verify-prod.sh becomes verify.sh, since the numbered sequence it belonged to no longer exists. It drops the stamp machinery, the dry-run plumbing and the Vercel-fallback messaging (there is no fallback now: recovery is promoting a previous Pages deployment), and gains --skip-dns / --skip-domains so it can be pointed at a single deployment via PROD_BASE before promoting it. The README is rewritten around the four invariants that fail SILENTLY — never widen _headers to /*, keep both URL forms of the Gerber post, never delete 404.astro, keep the cross-site form-POST guard — each with the reason, since the reason is the only thing that stops someone simplifying them back out. Verified after: 21 probes, 20 pass, 1 warn (HSTS max-age is 6 months vs Vercel's 2 years — on, just shorter), 0 fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
2026-07-27 15:08:56 +02:00
`pcbjam-site`), with one **Pages Function** for `/api/waitlist`. Both
`www.pcbjam.com` and the apex `pcbjam.com` are custom domains on that project and
serve identically; the pages emit `canonical=www`, which is what consolidates them
for search.
feat(site): move the marketing site from Vercel to Cloudflare Pages www.pcbjam.com was the last piece of the stack on Vercel. It is now a Cloudflare Pages project (pcbjam-site) deployed by deploy-site.yml on every push to main touching site/** — content must not wait for a release tag. The Astro adapter is gone entirely: the build is pure static and the one dynamic route, /api/waitlist, is a Pages Function. Going adapter-free (rather than swapping in @astrojs/cloudflare, which has dropped Pages support and only targets Workers) removes three problems at once — no Astro/adapter major-version coupling, Footer.astro's build-time execSync keeps working because prerendering stays in Node, and image optimisation stays plain build-time sharp with no Cloudflare Images binding. Verified against a real Pages runtime (wrangler pages dev): 21/21 parity probes pass, versus 19/21 on live Vercel. The scripted runbook is in deploy/site/ — every mutating step is dry-run by default. Four behaviour differences were found by measurement and are handled here: - The blog post's COOP/COEP was already broken in production. vercel.json scoped the headers to the bare URL, but the page's own canonical is the trailing-slash form, which served 200 with no isolation headers — so search arrivals lost SharedArrayBuffer and the embedded Gerber viewer degraded. public/_headers covers both forms. - Pages answers unknown URLs with the homepage at HTTP 200 when the output has no 404.html — a soft-404 that invites indexing junk URLs as the homepage. Hence src/pages/404.astro. - Vercel's edge refused cross-site form POSTs ("Cross-site POST form submissions are forbidden"); Pages does not, and a cross-site <form> submit needs no CORS permission to be sent, so the allowlist cannot stop it. The Function reproduces the guard; JSON posts stay exempt as that is demo.pcbjam.com's allowlisted path. - Cache-Control: immutable on /_astro/* came from the Vercel adapter's generated route config, so it is now an explicit _headers rule. Secrets move to `wrangler pages secret put --project-name pcbjam-site` (RESEND_API_KEY, RESEND_SEGMENT_ID, WAITLIST_FROM_EMAIL); WAITLIST_ALLOWED_ORIGINS stays unset so the allowlist stays in code. Local dev reads .dev.vars, now gitignored — the root repo's **/.dev.vars does not cover a nested git repo. privacy.md and cookies.md named Vercel as a GDPR Art. 28 processor; those mentions are removed and the existing Cloudflare entry widened to cover website hosting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
2026-07-27 13:34:12 +02:00
```
push to main (site/**) ──▶ .github/workflows/deploy-site.yml
1. npm ci
2. npm test (vitest — nothing else runs it)
3. astro build → site/dist/ (static; no adapter)
4. wrangler pages deploy → www.pcbjam.com
5. smoke: /api/waitlist preflight == 204
```
Not tag-gated: content must not wait for a release. The WASM editor ships from
`release.yml`; the two are independent.
chore(deploy/site): retire the migration scaffolding, keep the health check The Vercel -> Cloudflare Pages move is done and the Vercel project is deleted, so the one-shot scripts have no remaining purpose. Nothing in CI ever called them — deploy-site.yml runs npm ci / test / build / pages deploy inline — so this removes 10 files and orphans nothing. Deleted: 00-baseline (refused to run without x-vercel-id, so permanently unrunnable), 01-preflight (proved Vercel state and API-token scopes), 07-dns-cutover (the phased cutover; in the end the records were attached through the dashboard, and the rules/apex phases went unused once we chose APEX_MODE=serve), 09-detach-vercel (its target project is gone), plus 03-ensure-project, 04-set-secrets, 05-deploy, 06-verify-deploy, 02-verify-local and 99-rollback, all either spent or duplicating CI. Their lib/cf-api.sh went with them: the survivors use wrangler, so the whole remaining path needs only `wrangler login` and no zone scopes. What is kept is the part with ongoing value: lib/parity.sh, the assertion set that caught five real defects during the migration — the live COOP/COEP bug on the post's canonical URL, the soft-404 Pages would have introduced, the cross-site form-POST guard Vercel had been providing for free, the missing immutable header, and HSTS max-age=0. "Does the page return 200" catches none of those. 08-verify-prod.sh becomes verify.sh, since the numbered sequence it belonged to no longer exists. It drops the stamp machinery, the dry-run plumbing and the Vercel-fallback messaging (there is no fallback now: recovery is promoting a previous Pages deployment), and gains --skip-dns / --skip-domains so it can be pointed at a single deployment via PROD_BASE before promoting it. The README is rewritten around the four invariants that fail SILENTLY — never widen _headers to /*, keep both URL forms of the Gerber post, never delete 404.astro, keep the cross-site form-POST guard — each with the reason, since the reason is the only thing that stops someone simplifying them back out. Verified after: 21 probes, 20 pass, 1 warn (HSTS max-age is 6 months vs Vercel's 2 years — on, just shorter), 0 fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
2026-07-27 15:08:56 +02:00
## Files that carry load
feat(site): move the marketing site from Vercel to Cloudflare Pages www.pcbjam.com was the last piece of the stack on Vercel. It is now a Cloudflare Pages project (pcbjam-site) deployed by deploy-site.yml on every push to main touching site/** — content must not wait for a release tag. The Astro adapter is gone entirely: the build is pure static and the one dynamic route, /api/waitlist, is a Pages Function. Going adapter-free (rather than swapping in @astrojs/cloudflare, which has dropped Pages support and only targets Workers) removes three problems at once — no Astro/adapter major-version coupling, Footer.astro's build-time execSync keeps working because prerendering stays in Node, and image optimisation stays plain build-time sharp with no Cloudflare Images binding. Verified against a real Pages runtime (wrangler pages dev): 21/21 parity probes pass, versus 19/21 on live Vercel. The scripted runbook is in deploy/site/ — every mutating step is dry-run by default. Four behaviour differences were found by measurement and are handled here: - The blog post's COOP/COEP was already broken in production. vercel.json scoped the headers to the bare URL, but the page's own canonical is the trailing-slash form, which served 200 with no isolation headers — so search arrivals lost SharedArrayBuffer and the embedded Gerber viewer degraded. public/_headers covers both forms. - Pages answers unknown URLs with the homepage at HTTP 200 when the output has no 404.html — a soft-404 that invites indexing junk URLs as the homepage. Hence src/pages/404.astro. - Vercel's edge refused cross-site form POSTs ("Cross-site POST form submissions are forbidden"); Pages does not, and a cross-site <form> submit needs no CORS permission to be sent, so the allowlist cannot stop it. The Function reproduces the guard; JSON posts stay exempt as that is demo.pcbjam.com's allowlisted path. - Cache-Control: immutable on /_astro/* came from the Vercel adapter's generated route config, so it is now an explicit _headers rule. Secrets move to `wrangler pages secret put --project-name pcbjam-site` (RESEND_API_KEY, RESEND_SEGMENT_ID, WAITLIST_FROM_EMAIL); WAITLIST_ALLOWED_ORIGINS stays unset so the allowlist stays in code. Local dev reads .dev.vars, now gitignored — the root repo's **/.dev.vars does not cover a nested git repo. privacy.md and cookies.md named Vercel as a GDPR Art. 28 processor; those mentions are removed and the existing Cloudflare entry widened to cover website hosting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
2026-07-27 13:34:12 +02:00
```
site/functions/api/waitlist.ts the only server-side code (Pages Function)
chore(deploy/site): retire the migration scaffolding, keep the health check The Vercel -> Cloudflare Pages move is done and the Vercel project is deleted, so the one-shot scripts have no remaining purpose. Nothing in CI ever called them — deploy-site.yml runs npm ci / test / build / pages deploy inline — so this removes 10 files and orphans nothing. Deleted: 00-baseline (refused to run without x-vercel-id, so permanently unrunnable), 01-preflight (proved Vercel state and API-token scopes), 07-dns-cutover (the phased cutover; in the end the records were attached through the dashboard, and the rules/apex phases went unused once we chose APEX_MODE=serve), 09-detach-vercel (its target project is gone), plus 03-ensure-project, 04-set-secrets, 05-deploy, 06-verify-deploy, 02-verify-local and 99-rollback, all either spent or duplicating CI. Their lib/cf-api.sh went with them: the survivors use wrangler, so the whole remaining path needs only `wrangler login` and no zone scopes. What is kept is the part with ongoing value: lib/parity.sh, the assertion set that caught five real defects during the migration — the live COOP/COEP bug on the post's canonical URL, the soft-404 Pages would have introduced, the cross-site form-POST guard Vercel had been providing for free, the missing immutable header, and HSTS max-age=0. "Does the page return 200" catches none of those. 08-verify-prod.sh becomes verify.sh, since the numbered sequence it belonged to no longer exists. It drops the stamp machinery, the dry-run plumbing and the Vercel-fallback messaging (there is no fallback now: recovery is promoting a previous Pages deployment), and gains --skip-dns / --skip-domains so it can be pointed at a single deployment via PROD_BASE before promoting it. The README is rewritten around the four invariants that fail SILENTLY — never widen _headers to /*, keep both URL forms of the Gerber post, never delete 404.astro, keep the cross-site form-POST guard — each with the reason, since the reason is the only thing that stops someone simplifying them back out. Verified after: 21 probes, 20 pass, 1 warn (HSTS max-age is 6 months vs Vercel's 2 years — on, just shorter), 0 fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
2026-07-27 15:08:56 +02:00
site/public/_headers prod response headers, scoped to 3 patterns
feat(site): move the marketing site from Vercel to Cloudflare Pages www.pcbjam.com was the last piece of the stack on Vercel. It is now a Cloudflare Pages project (pcbjam-site) deployed by deploy-site.yml on every push to main touching site/** — content must not wait for a release tag. The Astro adapter is gone entirely: the build is pure static and the one dynamic route, /api/waitlist, is a Pages Function. Going adapter-free (rather than swapping in @astrojs/cloudflare, which has dropped Pages support and only targets Workers) removes three problems at once — no Astro/adapter major-version coupling, Footer.astro's build-time execSync keeps working because prerendering stays in Node, and image optimisation stays plain build-time sharp with no Cloudflare Images binding. Verified against a real Pages runtime (wrangler pages dev): 21/21 parity probes pass, versus 19/21 on live Vercel. The scripted runbook is in deploy/site/ — every mutating step is dry-run by default. Four behaviour differences were found by measurement and are handled here: - The blog post's COOP/COEP was already broken in production. vercel.json scoped the headers to the bare URL, but the page's own canonical is the trailing-slash form, which served 200 with no isolation headers — so search arrivals lost SharedArrayBuffer and the embedded Gerber viewer degraded. public/_headers covers both forms. - Pages answers unknown URLs with the homepage at HTTP 200 when the output has no 404.html — a soft-404 that invites indexing junk URLs as the homepage. Hence src/pages/404.astro. - Vercel's edge refused cross-site form POSTs ("Cross-site POST form submissions are forbidden"); Pages does not, and a cross-site <form> submit needs no CORS permission to be sent, so the allowlist cannot stop it. The Function reproduces the guard; JSON posts stay exempt as that is demo.pcbjam.com's allowlisted path. - Cache-Control: immutable on /_astro/* came from the Vercel adapter's generated route config, so it is now an explicit _headers rule. Secrets move to `wrangler pages secret put --project-name pcbjam-site` (RESEND_API_KEY, RESEND_SEGMENT_ID, WAITLIST_FROM_EMAIL); WAITLIST_ALLOWED_ORIGINS stays unset so the allowlist stays in code. Local dev reads .dev.vars, now gitignored — the root repo's **/.dev.vars does not cover a nested git repo. privacy.md and cookies.md named Vercel as a GDPR Art. 28 processor; those mentions are removed and the existing Cloudflare entry widened to cover website hosting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
2026-07-27 13:34:12 +02:00
site/public/_routes.json only /api/* invokes the Function
site/wrangler.toml nodejs_compat + pages_build_output_dir
chore(deploy/site): retire the migration scaffolding, keep the health check The Vercel -> Cloudflare Pages move is done and the Vercel project is deleted, so the one-shot scripts have no remaining purpose. Nothing in CI ever called them — deploy-site.yml runs npm ci / test / build / pages deploy inline — so this removes 10 files and orphans nothing. Deleted: 00-baseline (refused to run without x-vercel-id, so permanently unrunnable), 01-preflight (proved Vercel state and API-token scopes), 07-dns-cutover (the phased cutover; in the end the records were attached through the dashboard, and the rules/apex phases went unused once we chose APEX_MODE=serve), 09-detach-vercel (its target project is gone), plus 03-ensure-project, 04-set-secrets, 05-deploy, 06-verify-deploy, 02-verify-local and 99-rollback, all either spent or duplicating CI. Their lib/cf-api.sh went with them: the survivors use wrangler, so the whole remaining path needs only `wrangler login` and no zone scopes. What is kept is the part with ongoing value: lib/parity.sh, the assertion set that caught five real defects during the migration — the live COOP/COEP bug on the post's canonical URL, the soft-404 Pages would have introduced, the cross-site form-POST guard Vercel had been providing for free, the missing immutable header, and HSTS max-age=0. "Does the page return 200" catches none of those. 08-verify-prod.sh becomes verify.sh, since the numbered sequence it belonged to no longer exists. It drops the stamp machinery, the dry-run plumbing and the Vercel-fallback messaging (there is no fallback now: recovery is promoting a previous Pages deployment), and gains --skip-dns / --skip-domains so it can be pointed at a single deployment via PROD_BASE before promoting it. The README is rewritten around the four invariants that fail SILENTLY — never widen _headers to /*, keep both URL forms of the Gerber post, never delete 404.astro, keep the cross-site form-POST guard — each with the reason, since the reason is the only thing that stops someone simplifying them back out. Verified after: 21 probes, 20 pass, 1 warn (HSTS max-age is 6 months vs Vercel's 2 years — on, just shorter), 0 fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
2026-07-27 15:08:56 +02:00
site/src/pages/404.astro required — see below
feat(site): move the marketing site from Vercel to Cloudflare Pages www.pcbjam.com was the last piece of the stack on Vercel. It is now a Cloudflare Pages project (pcbjam-site) deployed by deploy-site.yml on every push to main touching site/** — content must not wait for a release tag. The Astro adapter is gone entirely: the build is pure static and the one dynamic route, /api/waitlist, is a Pages Function. Going adapter-free (rather than swapping in @astrojs/cloudflare, which has dropped Pages support and only targets Workers) removes three problems at once — no Astro/adapter major-version coupling, Footer.astro's build-time execSync keeps working because prerendering stays in Node, and image optimisation stays plain build-time sharp with no Cloudflare Images binding. Verified against a real Pages runtime (wrangler pages dev): 21/21 parity probes pass, versus 19/21 on live Vercel. The scripted runbook is in deploy/site/ — every mutating step is dry-run by default. Four behaviour differences were found by measurement and are handled here: - The blog post's COOP/COEP was already broken in production. vercel.json scoped the headers to the bare URL, but the page's own canonical is the trailing-slash form, which served 200 with no isolation headers — so search arrivals lost SharedArrayBuffer and the embedded Gerber viewer degraded. public/_headers covers both forms. - Pages answers unknown URLs with the homepage at HTTP 200 when the output has no 404.html — a soft-404 that invites indexing junk URLs as the homepage. Hence src/pages/404.astro. - Vercel's edge refused cross-site form POSTs ("Cross-site POST form submissions are forbidden"); Pages does not, and a cross-site <form> submit needs no CORS permission to be sent, so the allowlist cannot stop it. The Function reproduces the guard; JSON posts stay exempt as that is demo.pcbjam.com's allowlisted path. - Cache-Control: immutable on /_astro/* came from the Vercel adapter's generated route config, so it is now an explicit _headers rule. Secrets move to `wrangler pages secret put --project-name pcbjam-site` (RESEND_API_KEY, RESEND_SEGMENT_ID, WAITLIST_FROM_EMAIL); WAITLIST_ALLOWED_ORIGINS stays unset so the allowlist stays in code. Local dev reads .dev.vars, now gitignored — the root repo's **/.dev.vars does not cover a nested git repo. privacy.md and cookies.md named Vercel as a GDPR Art. 28 processor; those mentions are removed and the existing Cloudflare entry widened to cover website hosting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
2026-07-27 13:34:12 +02:00
```
chore(deploy/site): retire the migration scaffolding, keep the health check The Vercel -> Cloudflare Pages move is done and the Vercel project is deleted, so the one-shot scripts have no remaining purpose. Nothing in CI ever called them — deploy-site.yml runs npm ci / test / build / pages deploy inline — so this removes 10 files and orphans nothing. Deleted: 00-baseline (refused to run without x-vercel-id, so permanently unrunnable), 01-preflight (proved Vercel state and API-token scopes), 07-dns-cutover (the phased cutover; in the end the records were attached through the dashboard, and the rules/apex phases went unused once we chose APEX_MODE=serve), 09-detach-vercel (its target project is gone), plus 03-ensure-project, 04-set-secrets, 05-deploy, 06-verify-deploy, 02-verify-local and 99-rollback, all either spent or duplicating CI. Their lib/cf-api.sh went with them: the survivors use wrangler, so the whole remaining path needs only `wrangler login` and no zone scopes. What is kept is the part with ongoing value: lib/parity.sh, the assertion set that caught five real defects during the migration — the live COOP/COEP bug on the post's canonical URL, the soft-404 Pages would have introduced, the cross-site form-POST guard Vercel had been providing for free, the missing immutable header, and HSTS max-age=0. "Does the page return 200" catches none of those. 08-verify-prod.sh becomes verify.sh, since the numbered sequence it belonged to no longer exists. It drops the stamp machinery, the dry-run plumbing and the Vercel-fallback messaging (there is no fallback now: recovery is promoting a previous Pages deployment), and gains --skip-dns / --skip-domains so it can be pointed at a single deployment via PROD_BASE before promoting it. The README is rewritten around the four invariants that fail SILENTLY — never widen _headers to /*, keep both URL forms of the Gerber post, never delete 404.astro, keep the cross-site form-POST guard — each with the reason, since the reason is the only thing that stops someone simplifying them back out. Verified after: 21 probes, 20 pass, 1 warn (HSTS max-age is 6 months vs Vercel's 2 years — on, just shorter), 0 fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
2026-07-27 15:08:56 +02:00
Four things here fail **silently** if changed carelessly. Each is asserted by
`verify.sh`, and each is asserted because it was a real defect at some point:
- **Never widen `_headers` to `/*`.** `../demo/_headers` does exactly that, which
is right for the demo and wrong here: a `require-corp` document cannot load the
no-COEP YouTube hero iframe, so the landing page must stay un-isolated.
- **Keep both URL forms of the Gerber blog post in `_headers`.** Pages serves the
trailing-slash form and 308s the bare form to it, and the trailing-slash form is
the page's own canonical. Scoping the headers to one form only means search
arrivals get a page without `SharedArrayBuffer` and the embedded viewer quietly
degrades. That shipped in production for months.
- **Never delete `404.astro`.** With no `404.html` in the output, Pages answers
every unknown URL with the **homepage at HTTP 200** — a soft 404 that invites
indexing junk URLs as the homepage.
- **Keep the cross-site form-POST guard** in `waitlist.ts`. Vercel's edge refused
those for free; Cloudflare does not, and a cross-site `<form>` submit needs no
CORS permission to be *sent*, so the allowlist cannot stop it.
## Verifying
```sh
deploy/site/verify.sh # production
EXPECT_HSTS=1 deploy/site/verify.sh # once HSTS max-age is set, assert it
# a specific deployment, before promoting it
PROD_BASE=https://abc123.pcbjam-site.pages.dev \
deploy/site/verify.sh --skip-dns --skip-domains
```
`lib/parity.sh` holds the assertion set: pages resolve to themselves (which is how
a soft-404 gets caught), cross-origin isolation on the right paths *and its
absence on the landing page*, immutable asset caching, and the waitlist endpoint's
whole contract — preflight 204 with **zero** redirects for the demo origin, CORS
denied for others, 405 on GET, 400 on a bad address, 303 for the no-JS form, 403
for a cross-site form POST.
Header assertions are made on the **final** response after following redirects,
via two separate requests (`_trace`, then `_headers`). That split is deliberate:
the isolation bug above was invisible precisely because the headers were present
on a redirect hop and absent on the document.
The POSTs are safe against production. The full-path probe uses the honeypot
branch, which returns before validation, before the rate limiter and before any
Resend call — so it exercises routing, Functions bundling, body parsing and CORS
while sending no mail. The one destructive probe (a *valid* email POST) is behind
`--live-post` and is not used here.
## Secrets
`RESEND_API_KEY`, `RESEND_SEGMENT_ID`, `WAITLIST_FROM_EMAIL`, set on the Pages
project (dashboard → Settings → Variables and Secrets, or
`wrangler pages secret put <NAME> --project-name pcbjam-site`). Pages applies
changes to **new deployments only**, so redeploy after editing them.
`WAITLIST_ALLOWED_ORIGINS` is deliberately unset — it keeps its default in
`functions/api/waitlist.ts`, where the allowlist is reviewable in code. Preview is
deliberately keyless too: a preview holding the live key would email real people
and write real Resend contacts.
## Recovery
There is no cross-vendor fallback — the Vercel project is gone. Recovery is a
Pages concern: every deployment stays addressable at its own `*.pages.dev` URL, so
verify a candidate first, then either roll back to it in the dashboard
(Deployments → Rollback to this deployment) or re-deploy a known-good tree so git
stays the source of truth.
feat(site): move the marketing site from Vercel to Cloudflare Pages www.pcbjam.com was the last piece of the stack on Vercel. It is now a Cloudflare Pages project (pcbjam-site) deployed by deploy-site.yml on every push to main touching site/** — content must not wait for a release tag. The Astro adapter is gone entirely: the build is pure static and the one dynamic route, /api/waitlist, is a Pages Function. Going adapter-free (rather than swapping in @astrojs/cloudflare, which has dropped Pages support and only targets Workers) removes three problems at once — no Astro/adapter major-version coupling, Footer.astro's build-time execSync keeps working because prerendering stays in Node, and image optimisation stays plain build-time sharp with no Cloudflare Images binding. Verified against a real Pages runtime (wrangler pages dev): 21/21 parity probes pass, versus 19/21 on live Vercel. The scripted runbook is in deploy/site/ — every mutating step is dry-run by default. Four behaviour differences were found by measurement and are handled here: - The blog post's COOP/COEP was already broken in production. vercel.json scoped the headers to the bare URL, but the page's own canonical is the trailing-slash form, which served 200 with no isolation headers — so search arrivals lost SharedArrayBuffer and the embedded Gerber viewer degraded. public/_headers covers both forms. - Pages answers unknown URLs with the homepage at HTTP 200 when the output has no 404.html — a soft-404 that invites indexing junk URLs as the homepage. Hence src/pages/404.astro. - Vercel's edge refused cross-site form POSTs ("Cross-site POST form submissions are forbidden"); Pages does not, and a cross-site <form> submit needs no CORS permission to be sent, so the allowlist cannot stop it. The Function reproduces the guard; JSON posts stay exempt as that is demo.pcbjam.com's allowlisted path. - Cache-Control: immutable on /_astro/* came from the Vercel adapter's generated route config, so it is now an explicit _headers rule. Secrets move to `wrangler pages secret put --project-name pcbjam-site` (RESEND_API_KEY, RESEND_SEGMENT_ID, WAITLIST_FROM_EMAIL); WAITLIST_ALLOWED_ORIGINS stays unset so the allowlist stays in code. Local dev reads .dev.vars, now gitignored — the root repo's **/.dev.vars does not cover a nested git repo. privacy.md and cookies.md named Vercel as a GDPR Art. 28 processor; those mentions are removed and the existing Cloudflare entry widened to cover website hosting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
2026-07-27 13:34:12 +02:00
## Local development
```sh
cd site
npm run dev # Astro only — does NOT run functions/
cp .dev.vars.example .dev.vars # gitignored
npm run build && npm run pages:dev # http://localhost:8788, Function included
```
chore(deploy/site): retire the migration scaffolding, keep the health check The Vercel -> Cloudflare Pages move is done and the Vercel project is deleted, so the one-shot scripts have no remaining purpose. Nothing in CI ever called them — deploy-site.yml runs npm ci / test / build / pages deploy inline — so this removes 10 files and orphans nothing. Deleted: 00-baseline (refused to run without x-vercel-id, so permanently unrunnable), 01-preflight (proved Vercel state and API-token scopes), 07-dns-cutover (the phased cutover; in the end the records were attached through the dashboard, and the rules/apex phases went unused once we chose APEX_MODE=serve), 09-detach-vercel (its target project is gone), plus 03-ensure-project, 04-set-secrets, 05-deploy, 06-verify-deploy, 02-verify-local and 99-rollback, all either spent or duplicating CI. Their lib/cf-api.sh went with them: the survivors use wrangler, so the whole remaining path needs only `wrangler login` and no zone scopes. What is kept is the part with ongoing value: lib/parity.sh, the assertion set that caught five real defects during the migration — the live COOP/COEP bug on the post's canonical URL, the soft-404 Pages would have introduced, the cross-site form-POST guard Vercel had been providing for free, the missing immutable header, and HSTS max-age=0. "Does the page return 200" catches none of those. 08-verify-prod.sh becomes verify.sh, since the numbered sequence it belonged to no longer exists. It drops the stamp machinery, the dry-run plumbing and the Vercel-fallback messaging (there is no fallback now: recovery is promoting a previous Pages deployment), and gains --skip-dns / --skip-domains so it can be pointed at a single deployment via PROD_BASE before promoting it. The README is rewritten around the four invariants that fail SILENTLY — never widen _headers to /*, keep both URL forms of the Gerber post, never delete 404.astro, keep the cross-site form-POST guard — each with the reason, since the reason is the only thing that stops someone simplifying them back out. Verified after: 21 probes, 20 pass, 1 warn (HSTS max-age is 6 months vs Vercel's 2 years — on, just shorter), 0 fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
2026-07-27 15:08:56 +02:00
`astro dev` does not execute `functions/`, so any change to the waitlist endpoint
needs `pages:dev` to be exercised at all.