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
This commit is contained in:
parent
2e1b69998f
commit
7edfade53c
41 changed files with 2443 additions and 910 deletions
|
|
@ -1,18 +1,21 @@
|
|||
// @ts-check
|
||||
import { defineConfig, envField } from 'astro/config';
|
||||
import vercel from '@astrojs/vercel';
|
||||
import { defineConfig } from 'astro/config';
|
||||
import mdx from '@astrojs/mdx';
|
||||
|
||||
// Static by default (every page prerenders to HTML, zero client JS).
|
||||
// The Vercel adapter is wired in so any single route can opt into
|
||||
// per-request SSR later with `export const prerender = false;`.
|
||||
// See README.md ("SSR per route") for how.
|
||||
// Fully static: every page prerenders to HTML with zero client JS, and there is
|
||||
// NO adapter — the build output in dist/ is what `wrangler pages deploy` ships.
|
||||
// The one dynamic route, /api/waitlist, is a Cloudflare Pages Function in
|
||||
// functions/ rather than an Astro SSR route. See README.md ("Deploying").
|
||||
export default defineConfig({
|
||||
// Canonical origin (apex 308s to www). Without this, prerendered Astro.url
|
||||
// is localhost, which leaked into canonical/OG tags on production.
|
||||
site: 'https://www.pcbjam.com',
|
||||
output: 'static',
|
||||
adapter: vercel(),
|
||||
// Deliberately NO trailingSlash setting: Astro's default emits the canonical
|
||||
// with a trailing slash, which is also the form Cloudflare Pages serves at
|
||||
// 200 (the bare form 308s to it). Forcing 'never' would make canonical and
|
||||
// the served URL disagree.
|
||||
//
|
||||
// MDX lets the Gerber-viewer blog post embed the <GerberDemo /> component
|
||||
// inline (markdown posts can't import components).
|
||||
integrations: [mdx()],
|
||||
|
|
@ -20,9 +23,9 @@ export default defineConfig({
|
|||
prefetch: { prefetchAll: true, defaultStrategy: 'viewport' },
|
||||
// Dev-server cross-origin isolation so the embedded Gerber viewer's WASM
|
||||
// threads (SharedArrayBuffer) work under `npm run dev`. Production headers are
|
||||
// scoped per-route in vercel.json. require-corp (not credentialless) for the
|
||||
// widest browser support incl. Safari 15.2+; safe because the site loads only
|
||||
// same-origin subresources.
|
||||
// scoped per-route in public/_headers. require-corp (not credentialless) for
|
||||
// the widest browser support incl. Safari 15.2+; safe because the site loads
|
||||
// only same-origin subresources.
|
||||
vite: {
|
||||
server: {
|
||||
headers: {
|
||||
|
|
@ -31,29 +34,8 @@ export default defineConfig({
|
|||
},
|
||||
},
|
||||
},
|
||||
// Typed, validated server secrets for the waitlist endpoint. All optional so
|
||||
// the build never requires them and the endpoint degrades gracefully when a
|
||||
// key is absent (see src/pages/api/waitlist.ts). The @astrojs/vercel adapter
|
||||
// reads these from process.env at runtime — never inlined into the bundle.
|
||||
env: {
|
||||
schema: {
|
||||
RESEND_API_KEY: envField.string({ context: 'server', access: 'secret', optional: true }),
|
||||
RESEND_SEGMENT_ID: envField.string({ context: 'server', access: 'secret', optional: true }),
|
||||
WAITLIST_FROM_EMAIL: envField.string({
|
||||
context: 'server',
|
||||
access: 'secret',
|
||||
optional: true,
|
||||
default: 'PCBJam <hello@pcbjam.com>',
|
||||
}),
|
||||
// Comma-separated origins allowed to cross-post the waitlist form (e.g. the
|
||||
// static demo at demo.pcbjam.com, which has no backend of its own). Not a
|
||||
// secret — just config. Same-origin submits never hit this.
|
||||
WAITLIST_ALLOWED_ORIGINS: envField.string({
|
||||
context: 'server',
|
||||
access: 'public',
|
||||
optional: true,
|
||||
default: 'https://demo.pcbjam.com',
|
||||
}),
|
||||
},
|
||||
},
|
||||
// No `env.schema`: the waitlist secrets are no longer read through
|
||||
// `astro:env/server`. They reach the Pages Function as bindings on its
|
||||
// `context.env` (`wrangler pages secret put`), and the two former schema
|
||||
// defaults now live in functions/api/waitlist.ts.
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue