Guide · Deploy
Deploy Angular to Cloudflare with Claude Code
How this site ships: every page prerendered to static HTML, served by Cloudflare, with a strict Content Security Policy. Plus the build that broke and the one file that fixed it.
Last updated
1Prerender every route
The project was created with SSR on, which gives outputMode: "server" in angular.json. Then one rule turns every route into static HTML:
// src/app/app.routes.server.ts
export const serverRoutes: ServerRoute[] = [
{ path: '**', renderMode: RenderMode.Prerender },
];After ng build, dist/ahdevstudio/browser holds one index.html per route. That folder is the whole site.
2Keep browser code out of the build
Prerendering runs your components in Node, where there's no window, document or localStorage. Anything that touches them must run only in the browser, for example inside afterNextRender. I put this rule in the project's CLAUDE.md so Claude follows it in every session:
Any code touching window/document/localStorage must run browser-only (afterNextRender, or guarded), or the prerender breaks.
More on writing that file: CLAUDE.md best practices.
3Put Cloudflare's files in public/
Angular copies public/ into the build as-is, so Cloudflare's config files go there:
_headers: security headers, and long caching for fonts._redirects: old URLs, e.g./guide.html /guide 301.404.html: shown for any path that doesn't exist.
4The build that broke
I connected the repo in the Cloudflare dashboard and ran npx wrangler deploy. Wrangler saw an Angular project with SSR settings, set it up as a server app, and the build failed. The site never needed a server, but nothing told Wrangler that.
The fix is a wrangler.jsonc in the repo root that says "these are static files, just serve them":
// wrangler.jsonc: a Worker that only serves the prerendered site (no server code runs).
{
"name": "ahdevstudio",
"compatibility_date": "2026-10-23",
"assets": {
"directory": "./dist/ahdevstudio/browser",
"not_found_handling": "404-page"
}
}not_found_handling: "404-page" makes Cloudflare serve 404.html with a real 404 status. Check it before pushing:
npm run build npx wrangler deploy --dry-run # lists the files it would upload, deploys nothing
5Set the dashboard once
| Setting | Value |
|---|---|
| Build command | npm run build |
| Deploy command | npx wrangler deploy |
| Production branch | main |
Angular 22 needs a recent Node, so a .node-version file containing 22.22.3 pins it for Cloudflare's build.
6Add a strict CSP after the build
Angular's autoCsp option can't be used together with prerendering. So npm run build runs ng build and then a small script that hashes every inline script in the built HTML and writes the policy into the built _headers:
// scripts/csp.mjs (runs after ng build)
for (const page of html) {
for (const [, attrs, body] of page.matchAll(/<script([^>]*)>([\s\S]*?)<\/script>/g)) {
if (/\bsrc=/.test(attrs) || !body.trim()) continue; // external or empty: not inline code
hashes.add(`'sha256-${createHash('sha256').update(body).digest('base64')}'`);
}
}Because the hashes are recalculated on every build, changing an inline script can't break the site. Only new external origins need adding to the script by hand.
ng build skips the CSP step, so a page can work locally and still be blocked in production. I ask Claude to run npm run build, not ng build, before every commit.