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
|
|
@ -11,9 +11,9 @@ It uses **npm** (not the monorepo's pnpm) and has its own `package-lock.json`.
|
|||
|
||||
- Static by default: every page is prerendered to HTML and ships **zero client
|
||||
JavaScript**. A visitor downloads HTML + CSS only — no React/JS bundle.
|
||||
- SSR-capable: the Vercel adapter is wired in, so any individual route can be
|
||||
switched to per-request server rendering without ripping anything out (see
|
||||
below).
|
||||
- No adapter and no server bundle: the build emits `dist/` only. The single
|
||||
dynamic endpoint, `/api/waitlist`, ships as a **Cloudflare Pages Function**
|
||||
from `functions/` (see below).
|
||||
|
||||
## Routes
|
||||
|
||||
|
|
@ -38,38 +38,60 @@ Requires **Node ≥ 22.12** (Astro 6 requirement).
|
|||
cd site
|
||||
npm install
|
||||
npm run dev # http://localhost:4321
|
||||
npm run build # outputs to dist/ (+ .vercel/output for the adapter)
|
||||
npm run build # outputs to dist/ (static only — no adapter)
|
||||
npm run preview # serve the production build locally
|
||||
```
|
||||
|
||||
## SSR per route
|
||||
## Dynamic routes
|
||||
|
||||
Pages are static by default. To render a specific page or endpoint on demand
|
||||
(per request, as a Vercel Function), add this to its frontmatter:
|
||||
Every page is static. The one server-side endpoint is a **Cloudflare Pages
|
||||
Function**, not an Astro SSR route:
|
||||
|
||||
```astro
|
||||
---
|
||||
export const prerender = false;
|
||||
---
|
||||
```
|
||||
functions/api/waitlist.ts -> POST/OPTIONS/GET /api/waitlist
|
||||
```
|
||||
|
||||
That's the only change needed — the `@astrojs/vercel` adapter in
|
||||
`astro.config.mjs` already provides the server runtime. The rest of the site
|
||||
stays static.
|
||||
Pages maps the `functions/` tree to routes by path, and the handlers are named
|
||||
exports (`onRequestPost`, `onRequestOptions`, `onRequestGet`). Config arrives on
|
||||
`context.env`, not via `astro:env/server`. `public/_routes.json` restricts
|
||||
Function invocation to `/api/*`, so every page and asset stays a plain static
|
||||
request.
|
||||
|
||||
## Deploying to Vercel
|
||||
To add another endpoint, drop a new file in `functions/` — no Astro adapter and
|
||||
no `prerender = false` involved.
|
||||
|
||||
The `@astrojs/vercel` adapter emits the Vercel Build Output API format, so **no
|
||||
`vercel.json` is required**.
|
||||
Locally, run the built site the way Pages will serve it (this is the only way to
|
||||
exercise the Function, `astro dev` does not run `functions/`):
|
||||
|
||||
1. Push this repo to GitHub.
|
||||
2. Vercel dashboard → **New Project** → import this repo.
|
||||
3. Click **Edit** next to **Root Directory** and set it to **`site`**. This is
|
||||
the standard way to deploy a project that lives in a subdirectory.
|
||||
4. Vercel auto-detects the **Astro** framework preset and the package manager
|
||||
from the lockfile. Leave the build/install commands at their defaults.
|
||||
5. Deploy. Static pages are served from the CDN; any route with
|
||||
`prerender = false` is deployed as a Vercel Function automatically.
|
||||
```bash
|
||||
cp .dev.vars.example .dev.vars # fill in as needed; gitignored
|
||||
npm run build
|
||||
npm run pages:dev # http://localhost:8788
|
||||
```
|
||||
|
||||
Note: do **not** use `vercel.json` for URL rewrites with Astro — use Astro's
|
||||
`redirects` option in `astro.config.mjs` instead.
|
||||
## Deploying
|
||||
|
||||
`www.pcbjam.com` is a **Cloudflare Pages** project (`pcbjam-site`), deployed by
|
||||
`.github/workflows/deploy-site.yml` on every push to `main` touching `site/**` —
|
||||
content and blog posts do not wait for a release tag.
|
||||
|
||||
```
|
||||
push to main (site/**) -> npm ci -> npm test -> astro build
|
||||
-> wrangler pages deploy -> www.pcbjam.com
|
||||
```
|
||||
|
||||
Two things live outside the repo and are set once:
|
||||
|
||||
- **Secrets** — `wrangler pages secret put <NAME> --project-name pcbjam-site`
|
||||
for `RESEND_API_KEY`, `RESEND_SEGMENT_ID`, `WAITLIST_FROM_EMAIL`.
|
||||
`WAITLIST_ALLOWED_ORIGINS` is deliberately unset; it keeps the in-code default.
|
||||
- **The custom domain** — attached to the Pages project (there is no
|
||||
`wrangler pages domain` subcommand). The apex `pcbjam.com` 308s to `www` via a
|
||||
Cloudflare Redirect Rule.
|
||||
|
||||
Response headers come from `public/_headers` (copied verbatim into `dist/`), which
|
||||
carries the COOP/COEP rules the embedded Gerber viewer needs. Do **not** widen
|
||||
them to `/*` — the landing page must stay un-isolated so the YouTube hero embed
|
||||
loads.
|
||||
|
||||
The full one-time setup and the cutover runbook are in `../deploy/site/README.md`.
|
||||
|
|
|
|||
Loading…
Reference in a new issue