| name | cloudflare-pages-static-site |
| description | Deploy single-page static sites to Cloudflare via Workers static assets (current dashboard flow, not the deprecated Pages Connect-to-Git flow) with custom domains. No framework, no build step. Trigger keywords: static site, Cloudflare Pages, Cloudflare Workers, landing page, deploy website, custom domain, pages.dev, workers.dev, wrangler. |
| license | MIT |
| metadata | {"version":"2.0.0","hermes":{"tags":["Cloudflare","Deployment","Static-sites","Frontend","Workers"],"related_skills":["static-html-app-development"]}} |
Deploying Static Sites to Cloudflare (Workers static assets)
Ship static HTML/CSS with custom domain in under an hour.
Cloudflare's dashboard as of 2026-07 routes new static sites through Workers static assets (wrangler deploy), not the older "Pages tab → Connect to Git → Framework: None" flow — that classic flow may still work for existing Pages projects, but "Create application" no longer surfaces it as the default path for a new one. Don't assume the old flow when working from memory; verify against the live dashboard, since Cloudflare has been consolidating Pages into Workers.
Workflow
- Create repo:
mkdir -p ~/src/<site>/public && cd ~/src/<site> && git init
- Write files into
public/: public/index.html + public/style.css (no framework for single-page sites) — do not put them at the repo root (see Pitfalls)
- Add
wrangler.jsonc at the repo root:
{
"name": "<site>",
"compatibility_date": "YYYY-MM-DD",
"assets": { "directory": "./public" }
}
- Verify before touching Cloudflare — dry-run the actual deploy tool locally:
npx wrangler deploy --dry-run --outdir /tmp/dry-run. It should report Read N files from the assets directory .../public where N matches exactly your public files (e.g. 2 for index.html + style.css). If it reports far more, something outside public/ is leaking in — check assets.directory isn't pointing at . (repo root).
- Push to GitHub:
gh repo create <site> --public --source=. --push
- Cloudflare dashboard: Workers & Pages → Create application → Workers → Connect to Git → select repo. Build command: leave empty. Deploy command:
npx wrangler deploy (prefilled default — correct, it reads your wrangler.jsonc).
- Custom domain: Worker project → Settings → Domains & Routes → add domain. Add CNAME in DNS if not auto-added.
- Iterate: Every
git push auto-deploys in ~30-60 seconds — but note Cloudflare's own GitHub App may also open a PR proposing its own wrangler.jsonc (see Pitfalls) shortly after you connect the repo.
DNS Setup
- Delete old A records pointing to previous host
- Add CNAME:
@ (or subdomain) → <project>.workers.dev (or <project>.pages.dev if still on classic Pages)
- For
www: add as a custom domain on the project AND add www CNAME in DNS (both required)
- Don't use wildcard (
*) CNAME — causes 522 errors on undefined subdomains
Domain transfer: Change nameservers to Cloudflare's NS at old registrar, enter auth/EPP code at Cloudflare, ~$10/year at cost.
Anti-Patterns
- Over-engineering v1: Single-page site doesn't need React/Tailwind/CMS.
index.html + style.css ships in 30 minutes.
Pitfalls
assets.directory: "." (repo root) publicly serves the WHOLE repo — scripts, README, .github/ workflow files, .git/, everything, not just the intended page. Confirmed 2026-07-12: a dry-run against a bare repo root read 150 files (including .git/ objects); scoping assets.directory to a dedicated ./public subdirectory containing only the real deployable files brought it down to exactly 2. Isolate deployable files into their own subdirectory from the start — don't rely on a .assetsignore exclude-list, which is easy to get subtly wrong (e.g. forgetting .git/ itself needs excluding) and harder to verify at a glance than "this directory contains only what should be public."
- Cloudflare's GitHub App auto-opens its own config PR (branch
cloudflare/workers-autoconfig, commit "Add Cloudflare Workers configuration", authored by cloudflare-workers-and-pages[bot]) shortly after you connect a repo — and its auto-generated wrangler.jsonc defaults to assets.directory: "." (unscoped repo root), which is exactly the unsafe pattern above. If you've already set up a properly-scoped config, do not merge this PR — it will silently regress your fix. Check gh pr list after connecting a repo; close it with an explanatory comment if found.
git add pathA pathB where one path doesn't exist on disk aborts the ENTIRE add, silently, for every path in the call — not just the invalid one. Hit this after a git mv old new: a single git add old new otherfile1 otherfile2 failed with fatal: pathspec 'old' did not match any files because old no longer exists post-rename, and NONE of the paths (including the valid, unrelated otherfile1/otherfile2) got staged. The failure looked like a no-op rather than an error, and a subsequent bare git commit silently committed less than intended. Always run git show --stat HEAD after committing a batch that included any move/rename to confirm the full intended file list actually landed — don't trust that a git add with multiple paths staged everything just because the command didn't visibly explode.