pcbjam/site/astro.config.mjs
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

41 lines
1.9 KiB
JavaScript

// @ts-check
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
// 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',
// 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()],
// Prefetch linked pages so SPA-style navigation feels instant.
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 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: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
},
},
// 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.
});