pcbjam/deploy/site/00-baseline.sh
Viktor Vaczi 7edfade53c 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:41:51 +02:00

77 lines
3.3 KiB
Shell
Executable file

#!/usr/bin/env bash
# Freeze the LIVE Vercel behaviour as the reference every later step is compared
# against. Read-only; needs no Cloudflare credentials. Run this while Vercel is
# still serving — you cannot re-create it afterwards.
#
# deploy/site/00-baseline.sh [--force]
#
# Two probes are EXPECTED to fail here: post_coi (the blog post's COOP/COEP) is
# genuinely broken in production today — the page's canonical is the
# trailing-slash URL and that URL serves 200 with no isolation headers. Recording
# it is the point: 08-verify-prod.sh asserts those same probes PASS afterwards,
# so the migration proves it fixed the bug rather than porting it.
set -euo pipefail
. "$(dirname "$0")/lib/common.sh"
. "$(dirname "$0")/lib/parity.sh"
require_cmd curl dig awk sed jq
OUT="$STATE_DIR/baseline/vercel"
if [ -d "$OUT" ] && [ "${1:-}" != "--force" ]; then
die "baseline already exists at $OUT — refusing to overwrite (use --force).
Re-baselining after cutover would silently replace the reference."
fi
mkdir -p "$OUT"
section "confirming $PROD_BASE is still served by Vercel"
hdrs="$(_headers "$PROD_BASE/")"
if [ -z "$(_hdr "$hdrs" x-vercel-id)" ]; then
die "no x-vercel-id header on $PROD_BASE — this host is not on Vercel any more.
Baselining a Cloudflare response as 'the Vercel reference' would be useless."
fi
echo "ok: x-vercel-id present"
section "DNS + SOA snapshot"
{
echo "# captured $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "apex_cname=$(dig +short CNAME "$ZONE_NAME" || true)"
echo "www_cname=$(dig +short CNAME "www.$ZONE_NAME" || true)"
echo "ns=$(dig +short NS "$ZONE_NAME" | sort | tr '\n' ' ')"
# The SOA minimum is the NEGATIVE cache TTL. It is the number that makes
# delete-then-create dangerous: a resolver that asks while the record is gone
# caches NODATA for this long, and you cannot flush it.
echo "soa=$(dig +short SOA "$ZONE_NAME" || true)"
echo "soa_minimum_ttl=$(dig +short SOA "$ZONE_NAME" | awk '{print $NF}')"
} | tee "$OUT/dns.txt"
section "page titles (used to detect a soft-404 later)"
printf 'home_title=%s\n' "$(_title "$PROD_BASE/")" | tee "$OUT/titles.txt"
section "header snapshots"
for p in / /pricing /blog /privacy /blog/porting-kicad-graphics-to-webgl-in-2026 /gerber-demo/boot.js; do
f="$(printf '%s' "$p" | sed 's|/|_|g')"; [ "$f" = "_" ] && f="_home"
t="$(_trace "$PROD_BASE$p")"; eff="$(printf '%s' "$t" | cut -f1)"
{
echo "# requested: $PROD_BASE$p"
echo "# effective: $eff (hops $(printf '%s' "$t" | cut -f3))"
# Drop volatile headers so a later diff shows real changes, not timestamps.
_headers "$eff" | grep -vE '^(date|age|etag|last-modified|content-length|server|cf-ray|cf-cache-status|nel|report-to|alt-svc|set-cookie|x-vercel-id|x-vercel-cache|x-matched-path|expires|via):' | sort
} > "$OUT/$f.headers"
echo " $p -> $OUT/$f.headers"
done
rc=0
assert_parity "$PROD_BASE" --scope prod || rc=$?
assert_apex_redirect "$APEX_BASE" || rc=$?
{
echo "# baseline captured $(date -u +%Y-%m-%dT%H:%M:%SZ) against $PROD_BASE (Vercel)"
echo "# parity exit code: $rc (non-zero is EXPECTED — see the header of this script)"
} > "$OUT/parity-exit.txt"
section "done"
echo "Baseline written to $OUT"
echo
echo "Read the FAIL rows above and keep them: they are the 'before' half of the"
echo "COOP/COEP fix. 08-verify-prod.sh requires those same probes to pass."
echo "done: file://$OUT"