Operator playbook for apps/docs (the @freecodecamp/uikit-docs
Astro site) on Cloudflare Pages. Cite this file from any incident,
GitHub-App audit, or first-deploy ticket.
Decision context: ADR-0008
(supersedes ADR-0007).
Repository wiring: apps/docs/public/{_headers,_redirects,robots.txt},
apps/docs/scripts/verify-dist-pages-artefacts.mjs,
apps/docs/src/_meta/pages-config.test.ts.
The zero-build CDN bundle (styles.min.css, uikit.global.js,
sprite.svg, fonts, manifest.json) ships from the same deploy, at
/cdn/. It is built by apps/docs/scripts/build-cdn-bundle.mjs
during prebuild/predev and covered by the same post-build gate.
See ADR-0010 - it
is rolling/unversioned, not a separate release.
- Project: Cloudflare Pages project named
fcc-design. Git integration mode (no direct uploads). - Production branch:
main. CF Pages tags every deployment frommainas production. - Custom domain:
design.freecodecamp.org. Maps to the project via CF Pages → Custom Domains. - Git integration: deploy mode where the Cloudflare GitHub App
watches the connected repo, builds on CF infra, and uploads the
result. No
CLOUDFLARE_API_TOKENlives in repo secrets. - Preview: deployment to
https://<branch>.fcc-design.pages.devfor any non-production branch. Fork PRs receive previews automatically.
- Cloudflare account access with
Pages: Editon the freeCodeCamp account. - GitHub repo admin for
freeCodeCamp/UIkit(to install the Cloudflare GitHub App). - DNS access for
freecodecamp.org(or whichever zone ownsdesign.freecodecamp.org).
Run in order. Stop and triage if any step fails - do not skip.
CF dashboard → Workers & Pages → Create → Pages → Connect to Git.
- Install the Cloudflare GitHub App on
freeCodeCamp/UIkitwhen prompted. Org admin approval may be required. - Select the repo. Project name:
fcc-design. Production branch:main. - Build settings:
- Framework preset: None.
- Build command:
pnpm install --frozen-lockfile && pnpm build:docs. - Build output directory:
apps/docs/dist. - Root directory: leave default (repo root).
- Environment variables (Production + Preview):
NODE_VERSION=22(matchesengines.node>=22).
- Save and Deploy.
The first build may take ~3–5 min (cold pnpm cache, full workspace build).
Subsequent deploys land automatically:
- Push to
main→ production deploy. - Open PR (canonical or fork) → preview deploy at
https://<branch>.fcc-design.pages.dev. CF posts the preview URL as a PR comment.
To manually re-run a deploy: CF dashboard → fcc-design →
Deployments → "Retry deployment".
Verify in the build log:
pnpm installcompletes.pnpm build:docsexits 0 and the post-build gate prints✓ Cloudflare Pages artefacts present in dist/ (6 files).
Smoke-test the deployment URL in a browser:
/renders the landing page./components/,/handbook/,/brand/,/playground/render./sitemap-index.xmlreturns 200./robots.txtshows the absoluteSitemap:line.- DevTools → Network →
/_astro/<hash>.js:Cache-Control: public, max-age=31536000, immutable. - DevTools → Network →
/: every security global (Strict-Transport-Security,X-Content-Type-Options,X-Frame-Options,Referrer-Policy,Permissions-Policy,Content-Security-Policy-Report-Only).
CF dashboard → fcc-design → Custom Domains → Set up a domain
→ enter design.freecodecamp.org.
If freecodecamp.org is a CF zone, CF auto-creates the CNAME. If
not, CF prints the CNAME content; add it at the DNS provider
manually.
Wait for the certificate provisioning indicator to turn green (usually < 10 min). Verify:
dig +short design.freecodecamp.org
curl -I https://design.freecodecamp.orgcurl should show a cf-ray: header and a 200 status.
Re-run the smoke checks against the apex URL.
In order of preference:
CF dashboard → fcc-design → Deployments → find the last
known-good deployment → "Manage deployment" → "Rollback to this
deployment".
Instant, no rebuild.
git revert <commit> on main, push. CF Git integration
redeploys automatically. Use this when the bad change is in source
rather than _headers / config.
For operators with local CF credentials:
pnpm dlx wrangler pages deployment list --project-name=fcc-design
pnpm dlx wrangler pages deployment rollback <deployment-id> \
--project-name=fcc-designCF dashboard → Custom Domains → design.freecodecamp.org →
Remove. The site returns DNS-NXDOMAIN until reattached. Use only
when the entire project is compromised.
_headers declares max-age=31536000, immutable for
/_astro/* and /assets/*. A bad CSP / HSTS sticks for up to a
year on browsers that already cached an asset. Mitigations:
- CSP ships in
Content-Security-Policy-Report-Onlyfor the first deploy window. Promotion to enforce-mode is a separate, explicit follow-up commit only after observed violation reports = 0 for ≥ 7 days. - HSTS uses
max-age=63072000; includeSubDomains; preloadon day one. Removing HSTS once shipped is hard; verify the policy is correct before the first cutover.
After the observation window:
- Set up a CSP report endpoint (CF logs / GlitchTip / self-hosted)
and add the URI to the policy via
report-to. - Wait ≥ 7 days. Read the report stream end-to-end.
- If violations = 0, open a PR that swaps
Content-Security-Policy-Report-Only:forContent-Security-Policy:inapps/docs/public/_headers. Updateapps/docs/src/_meta/pages-config.test.tsaccordingly. - Land it on
main; watch the first prod deploy. - If violations appear post-flip, revert immediately (Option B) and re-tune the policy in Report-Only first.
The CF GitHub App is the only credential CF holds for the repo.
To audit the App's scope:
GitHub → Settings → Integrations → Applications → Cloudflare Pages → Configure.
To revoke (e.g., after suspected compromise): same page → Suspend or Uninstall. Re-installing follows the original Step 1 procedure.
To rotate to a different account / org:
- Uninstall the App on the old org.
- Re-install on the target org.
- CF detects the move; reconnect the project under Settings → Builds & deployments → Source.
| Symptom | First check |
|---|---|
Build fails at pnpm install |
NODE_VERSION env var set on project; pnpm version inferred |
Build fails at pnpm build:docs |
Same as local: read the log; turbo task graph + Astro build |
| Post-build gate prints "missing artefact" | Public folder lost a file; check apps/docs/public/ |
404 on /_astro/<hash>.js |
Build output dir mismatch; verify apps/docs/dist setting |
| 500 / blank page after deploy | CSP enforce blocked a runtime asset; revert with Option B |
cf-ray header missing on design.freecodecamp.org |
DNS not yet pointed at CF Pages; confirm CNAME |
| PR has no preview comment | CF GitHub App not installed on repo; or App suspended |
| Fork PR shows no preview | Expected on first push from the fork; CF deploys on second push |
This file is the operator's source of truth for everything outside the repo (CF dashboard state, GitHub App, DNS). Repo state is the source of truth for everything inside the repo. Disagreements between the two are bugs; reconcile by reading ADR-0008 and following the procedures here.