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
120 lines
4.9 KiB
YAML
120 lines
4.9 KiB
YAML
name: deploy-site (marketing site)
|
|
|
|
# Deploys the Astro marketing site + blog in site/ to Cloudflare Pages
|
|
# (www.pcbjam.com, Pages project `pcbjam-site`).
|
|
#
|
|
# push to main touching site/** ──▶ npm ci → npm test → astro build
|
|
# → wrangler pages deploy → www.pcbjam.com
|
|
#
|
|
# NOT tag-gated, deliberately. The site was previously deployed by Vercel's git
|
|
# integration on every push, and blog posts / copy fixes must not have to wait
|
|
# for a vX.Y.Z release. The tag-gated pipeline (release.yml) ships the WASM
|
|
# editor; this ships content, and the two are independent.
|
|
#
|
|
# The site is a STANDALONE npm project (its own package-lock.json, not the
|
|
# web/ pnpm workspace) and needs Node >= 22.12 for Astro 6 — hence npm ci and
|
|
# node-version 22 rather than the pnpm + node 20 used by the other workflows.
|
|
#
|
|
# The one dynamic route, /api/waitlist, ships as a Cloudflare Pages Function
|
|
# from site/functions/. Its secrets (RESEND_API_KEY, RESEND_SEGMENT_ID,
|
|
# WAITLIST_FROM_EMAIL) are NOT deploy inputs: set once with
|
|
# `wrangler pages secret put <NAME> --project-name pcbjam-site`.
|
|
# See deploy/site/README.md for the full runbook.
|
|
#
|
|
# Secrets (Settings → Secrets → Actions) — already present for demo/editor:
|
|
# CLOUDFLARE_API_TOKEN Cloudflare Pages:Edit
|
|
# CLOUDFLARE_ACCOUNT_ID
|
|
|
|
on:
|
|
push:
|
|
branches: ["main"]
|
|
paths: ["site/**", ".github/workflows/deploy-site.yml"]
|
|
workflow_dispatch:
|
|
|
|
# Serialize site deploys so two pushes don't race the live host (don't cancel a
|
|
# half-finished deploy — let it complete).
|
|
concurrency:
|
|
group: deploy-site
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
PAGES_PROJECT: pcbjam-site
|
|
# MUST be the Pages project's PRODUCTION branch — any other value makes
|
|
# `wrangler pages deploy` a PREVIEW deploy and www.pcbjam.com won't update.
|
|
# Direct-Upload projects default to "production".
|
|
PAGES_PROD_BRANCH: production
|
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
|
|
jobs:
|
|
deploy:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
# No submodules: the site shares no code with the WASM tools. A real
|
|
# checkout is still needed — Footer.astro resolves the GPLv3
|
|
# corresponding-source SHA from GITHUB_SHA (with a `git rev-parse`
|
|
# fallback), and that value is user-visible in the footer.
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
submodules: false
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
cache: npm
|
|
cache-dependency-path: site/package-lock.json
|
|
|
|
- name: Install site deps
|
|
working-directory: site
|
|
run: npm ci
|
|
|
|
# 1) Gate on the site's own vitest suite (waitlist Function hardening +
|
|
# the gerber-demo boot.js override gate). Nothing else runs it.
|
|
- name: Test
|
|
working-directory: site
|
|
run: npm test
|
|
|
|
# 2) Static build. Emits dist/ only — no adapter, no server bundle. The
|
|
# Function comes from site/functions/, which wrangler bundles at deploy.
|
|
- name: Build
|
|
working-directory: site
|
|
run: npm run build
|
|
|
|
# 3) Ensure the Pages project exists (first deploy creates it; no-op
|
|
# after). Its production branch must equal PAGES_PROD_BRANCH or deploys
|
|
# land as previews and www.pcbjam.com won't update.
|
|
- name: Ensure Pages project exists
|
|
working-directory: site
|
|
run: >
|
|
npx --yes wrangler@4 pages project create "$PAGES_PROJECT"
|
|
--production-branch "$PAGES_PROD_BRANCH"
|
|
--compatibility-date 2026-06-01 --compatibility-flags nodejs_compat
|
|
|| echo "pages project create skipped (already exists)"
|
|
|
|
# 4) Deploy. Run from site/ so wrangler picks up site/wrangler.toml (which
|
|
# sets pages_build_output_dir + nodejs_compat) AND discovers
|
|
# site/functions/ — deploying from the repo root would silently ship a
|
|
# static-only site with /api/waitlist 404ing.
|
|
- name: Deploy to Cloudflare Pages
|
|
working-directory: site
|
|
run: >
|
|
npx --yes wrangler@4 pages deploy
|
|
--project-name "$PAGES_PROJECT"
|
|
--branch "$PAGES_PROD_BRANCH"
|
|
--commit-dirty=true
|
|
|
|
# 5) Smoke: the deployed Function answers a preflight for the demo origin.
|
|
# demo.pcbjam.com cross-posts the waitlist form here and a CORS
|
|
# preflight cannot follow a redirect, so this must be 204 directly on
|
|
# www — not after a hop.
|
|
- name: Smoke-check the waitlist endpoint
|
|
run: |
|
|
for i in $(seq 1 20); do
|
|
code=$(curl -s -o /dev/null -w '%{http_code}' -X OPTIONS \
|
|
-H 'Origin: https://demo.pcbjam.com' \
|
|
-H 'Access-Control-Request-Method: POST' \
|
|
https://www.pcbjam.com/api/waitlist || true)
|
|
[ "$code" = "204" ] && break; sleep 3
|
|
done
|
|
test "$code" = "204" || { echo "waitlist preflight returned $code, expected 204"; exit 1; }
|