When: you're putting a site up on Cloudflare Pages, attaching a domain to it, or wondering why a change you made isn't showing on the live site.
Built from: align before you give it more, where it runs and what it may do.
The worry: the quick way to get a site up is one powerful token that can do everything. It works, so nobody goes back to it. And a site that looks fine in the dashboard can be quietly broken: deploys that stopped, analytics that never counted a visit, pages you deleted still being served.
What's really going on: deploying a page and setting up a domain are two different jobs. Deploying happens all the time and needs very little access. Setting up a domain happens once and needs a lot. Mixing them gives you a standing key with far more power than anything uses. Most of what goes wrong after that is silent, so you only find it by checking.
Tokens: two, with different lives
- Rare and powerful goes in the dashboard or a throwaway token. Frequent and weak gets a standing token.
- The standing token is called pages-deploy. It has Cloudflare Pages Edit on the account and nothing else. It covers every project, including ones that don't exist yet, so a new site needs no token work. It's the one that lives in GitHub Actions. If it leaks, the worst case is bad content you fix by redeploying.
- The throwaway token is called domain-setup. It's made only when a domain is first attached, scoped to that one domain, never all of them: Zone Read, DNS Edit, SSL and Certificates Edit, Zone Settings Edit, Cache Purge. Delete it as soon as the domain and SSL are working.
- Cache purges and one-off DNS records (a Search Console TXT, DMARC) go through the dashboard. Never add DNS Edit to the standing token.
- Make tokens under the account, not under your own profile. They outlive your login and show on the one screen people check.
- When you audit, read both lists, the account's and your profile's. An audit of one list is wrong. Every token is named for its job. One with a made-up name like "sparkling-fire" gets traced or deleted.
- A token is shown once, when it's made. If you didn't save it, roll it. Don't go hunting.
- Save a secret with printf, not echo, because echo adds a newline and Cloudflare then calls the token malformed (error 9106). Error 10000 means the token arrived and was refused: check which account it's scoped to.
Deploying
- Keep the source repo private. Don't connect Pages to GitHub. GitHub Actions builds the site and uploads only the built folder with Wrangler, so Cloudflare never sees the source.
- In a pnpm workspace, call
npx --yes wrangler@3 pages deploydirectly. The official wrangler action tries to install at the repo root and fails. Don't pin a pnpm version in the setup step if package.json already says which one. - The order for a new domain: make the Pages project, point the bare domain and www at it (Cloudflare allows that at the bare domain when proxied), attach both domains, set SSL to strict with HTTPS always on and TLS 1.2 minimum, put the deploy token in GitHub, delete the throwaway, push, watch the run.
Defaults every site gets
- A real 404 page. Without one, Pages serves the home page with a 200 for any missing address.
- HTML set to
Cache-Control: public, max-age=0, must-revalidate. Without it, a deleted or changed page can stay live for up to a week. - Security headers in a
_headersfile: HSTS, nosniff, no framing, a strict referrer policy, permissions locked off, and a content security policy. - HSTS can't be taken back once a domain is on the preload list. Start at six months with no preload. Go to two years with preload only after two to four weeks of the site running cleanly.
- If the domain sends no email, add the null SPF and DMARC records so nobody can send mail pretending to be it.
Analytics: decide per site, and prove it fires
- Whether a site measures at all is a choice. Some sites deliberately carry no tracking.
- If it measures, use Cloudflare Web Analytics, not Google Analytics: no cookies, no consent banner.
- Never use the dashboard's automatic setup. It injects the script at the edge, the site's content security policy blocks it, and the dashboard still says it's set up. That happened twice: one site recorded nothing for a week, lattris.com for three months.
- Use the snippet setup instead, and add Cloudflare's two addresses to the policy.
- A tag in the page proves nothing. Load the live page in a real browser and check that the request to
cloudflareinsights.com/cdn-cgi/rumwent out, with no policy errors. Then watch the dashboard count go up. - There's no API permission for Web Analytics. The dashboard is the only way in.
Deploys fail silently
- A failed GitHub Action tells nobody. Once the pipeline was dead for four weeks and a fix sat unshipped for a week.
- So when the live site doesn't show your change, first check the deploy actually ran. That separates "my change is wrong" from "my change never shipped".
- Headers are cached at the edge too. Just after a deploy, the bare address can show the old headers. Check with a cache-buster (
?cb=plus something random) before deciding the deploy failed. - If a deleted page still answers: open the deployment's own pages.dev address first. If that shows a 404, the build is right and only the cache is behind. Purge, wait, or push a no-op change.
_headersrules add up, they don't replace each other. A content security policy on/*also applies to every more specific path, and two policies on one address make the browser enforce only what both allow. So you can't loosen the policy for one path. Use one site-wide policy that allows what every page needs.- The same goes for
Cache-Control. One on/*lands on/assets/*too, and a year-long immutable rule never takes effect. Put noCache-Controlon/*. Give each group of paths its own: HTML at max-age 0, assets at a year, immutable. - A file cached as immutable for a year keeps its old copy under the same name. When you replace an image, give it a new file name.
- A new path can take minutes to show on the production address after a deploy. The deployment's own address is the one to trust.
- Only one machine deploys. A deploy from an out-of-date copy on a second machine quietly put an older version back live. Pull first, and deploy only by pushing.
After it's up: make it findable and shareable
- Link previews. Every page needs Open Graph and Twitter card tags in its
<head>, or LinkedIn, Facebook, Discord, Slack, iMessage, X and WhatsApp show a bare link:og:title,og:type,og:url(the page's canonical address),og:site_name,og:descriptiontwitter:cardset tosummary, plustwitter:siteandtwitter:creatorwith the handleog:typeiswebsiteon the home page andarticleon content pages.og:imageis optional but makes previews much better. Use one default image at 1200×630. Without it, platforms show a text-only card, which still works.
- A
robots.txtat the site's root, allowing everything and pointing to the sitemap:User-agent: *,Allow: /,Sitemap: https://DOMAIN/sitemap.xml. - Google Search Console:
- Add the site as a URL-prefix property,
https://DOMAIN. - Verify it with the HTML tag: a
google-site-verificationmeta tag in the layout's<head>. Or use a DNS TXT record, added through the dashboard or a throwaway token. - Deploy, so the tag is live, then click Verify.
- Under Sitemaps, submit
https://DOMAIN/sitemap.xml.
- Add the site as a URL-prefix property,
- Put the site's address in your social bios.
As a skill: ask the AI to turn this page into a skill for any new site setup.