Cloudflare Pages — publishing a local folder to the public internet
The whole capability in one sentence: a folder on a laptop becomes a website on Cloudflare's edge with one command, a real URL, and free HTTPS — and folders are first-class citizens, not an afterthought.
1. The model
A Pages project is a disposable remote mirror of a local folder. Each deploy re-uploads the whole tree and cuts to a new version at the edge; the previous version stays live until the next deploy completes. What this buys:
- Folder structure is served as-is. Every file in the tree gets a URL matching its path — nested directories included. No build step, no bundler, no framework.
- Directory URLs resolve to
index.html./docsservesdocs/index.html; the root URL serves the rootindex.html. A directory with no index 404s (or serves a configured404.html). - Relative links just work. The tree is served verbatim, so a document linking to a neighbor works the same on disk and at the edge.
- Nothing is browsable. No directory listings are ever served — only real files. Privacy-by-construction, not by permission.
- Instant-ish propagation, one quirk. The stable project URL serves new content immediately; the per-deployment hash URL (the one the CLI prints) can be unreachable from a fresh machine for a while — a same-day LibreSSL TLS-handshake failure against a brand-new hostname was observed once. Edge propagation; the project URL is the source of truth.
2. The local tools
Two small, deliberately decoupled commands carry the workflow:
~/bin/sitemap [--publish] [dir] # regenerate index.html as a folder directory
~/bin/publish [name | dir] # upload the folder to Cloudflare Pages
sitemapscans a site folder (default~/published), lists every.htmlit finds — recursively, self-excluded — and writes a responsiveindex.htmlat the root: one row per document with its title, path, and date. It only touches the local folder. The--publishflag is the entire coupling: only with it does it hand off topublish.publishreads the project name, creates the Cloudflare project if it doesn't exist yet, and deploys the folder. Auth comes from an account API token on disk, not an interactive login.- The project name lives in a config file, not the script: a
.sitefile in the folder (mode 600) names the Cloudflare project. Renaming the site is one write + one publish. An env var overrides for one-off deploys.
So the steady state is one command: sitemap --publish — regenerate the directory, then the site is live.
3. Naming — the -suffix quirk
Project names are a global namespace: when a name is already taken, Cloudflare silently appends a suffix (published became a site at published-4qu.pages.dev). The suffix is on the domain, not the project — a delete by the suffixed name fails; you delete by the real name. Practical takeaways:
- Pick a name you want to keep; availability is not guaranteed and the CLI won't warn you.
- Renames are create-then-delete, not rename. The name is fixed at creation, so the clean move is: create the new-named project → publish into it → verify the new URL → delete the old. Nothing of substance moves — the folder is the source of truth, the Cloudflare side is just a mirror.
4. Custom domains
A Pages project (free tier included) can wear a custom domain, and the pages.dev URL coexists — it is a second address, not a thing you give up. HTTPS certs are provisioned automatically in every case.
| Target | DNS mechanics | Friction |
|---|---|---|
Subdomain (x.example.com) | One CNAME record → <project>.pages.dev. | Low — if the domain is registered with Cloudflare, the record is wired automatically, no manual DNS at all. |
Apex (example.com) | The RFC forbids CNAMEs at the zone apex. Cloudflare papers over it two ways: CNAME flattening (their resolver follows the apex CNAME) when the zone is in a Cloudflare account, or plain A records at their edge IPs when the zone lives at another registrar. | A bit more bookkeeping for external zones; zero if the domain was bought through Cloudflare's registrar — about the same price as anywhere. |
The rule of thumb that fell out of comparing the two: register the domain with Cloudflare and the apex path becomes the least work, not the most. The orange-cloud proxy is the whole difference between "one CLI call" and "hand-edit records at a registrar I'll have to remember."
5. Gotchas collected along the way
pages project createdemands a--production-branch— even for direct-deploy projects with no git. Passmainand ignore git entirely; direct folder deploys don't need a repo.pages deployto a nameless project fails loud — create first (the local tooling does this automatically).- The deploy uploads everything in the folder, dotfiles included — config files ride to the edge publicly. Keep them tokenless (the
.sitefile holds only a name, so it's fine). - Deleting a project's deployments and the project itself are separate acts; the old hostname keeps serving from edge caches for minutes after the delete.
- Teardown is clean:
deletethe project, the local folder is unharmed, and the same two commands rebuild the site under any name — that is the anti-lock-in design, verified in both directions.