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
57 lines
2.4 KiB
Shell
Executable file
57 lines
2.4 KiB
Shell
Executable file
#!/usr/bin/env bash
|
|
# Idempotently ensure the Cloudflare Pages project exists with the right
|
|
# production branch and compatibility settings.
|
|
#
|
|
# deploy/site/03-ensure-project.sh # dry run
|
|
# deploy/site/03-ensure-project.sh --apply
|
|
#
|
|
# Naming this "ensure" rather than "create" is the contract: running it twice is
|
|
# normal and must not look like an error.
|
|
set -euo pipefail
|
|
. "$(dirname "$0")/lib/common.sh"
|
|
. "$(dirname "$0")/lib/cf-api.sh"
|
|
|
|
require_cmd curl jq npx
|
|
parse_common_flags "$@"
|
|
dry_banner
|
|
|
|
section "project $PAGES_PROJECT"
|
|
if existing="$(cf_pages_project 2>/dev/null)"; then
|
|
echo "exists already"
|
|
else
|
|
existing=""
|
|
dry "create Pages project $PAGES_PROJECT (production branch: $PAGES_PROD_BRANCH)" -- \
|
|
$WRANGLER pages project create "$PAGES_PROJECT" \
|
|
--production-branch "$PAGES_PROD_BRANCH" \
|
|
--compatibility-date 2026-06-01 --compatibility-flags nodejs_compat
|
|
[ "$DRY_RUN" = 1 ] && { echo; echo "done: (dry run) would create $PAGES_PROJECT"; exit 0; }
|
|
existing="$(cf_pages_project)"
|
|
fi
|
|
|
|
section "asserting settings"
|
|
pb="$(printf '%s' "$existing" | jq -r '.result.production_branch // "?"')"
|
|
src="$(printf '%s' "$existing" | jq -r '.result.source // "null"')"
|
|
sub="$(printf '%s' "$existing" | jq -r '.result.subdomain // "?"')"
|
|
|
|
# The single most expensive mistake available here: if the project's production
|
|
# branch is anything other than what deploy-site.yml passes to --branch, every
|
|
# deploy lands as a PREVIEW and the live site silently never updates. The same
|
|
# warning is written into deploy-demo.yml.
|
|
if [ "$pb" != "$PAGES_PROD_BRANCH" ]; then
|
|
die "production_branch is '$pb' but deploys use '$PAGES_PROD_BRANCH'.
|
|
Every deploy would land as a preview and www.pcbjam.com would never update.
|
|
Fix it in Pages -> $PAGES_PROJECT -> Settings -> Builds & deployments, then re-run.
|
|
(This script will NOT change it: doing so retroactively re-labels deployments.)"
|
|
fi
|
|
echo " ok production_branch = $pb"
|
|
|
|
if [ "$src" != "null" ]; then
|
|
die "project is Git-connected (source: $src). Cloudflare's own builds would race
|
|
the uploads from deploy-site.yml. Disconnect it in the dashboard first."
|
|
fi
|
|
echo " ok Direct Upload (not Git-connected)"
|
|
|
|
section "done"
|
|
echo "note: the custom domain is NOT set here — there is no 'wrangler pages domain'"
|
|
echo " subcommand. 07-dns-cutover.sh attaches www.pcbjam.com via the API."
|
|
echo "done: https://${sub}.pages.dev"
|