Branch Deploy Controls on Cloudflare Pages
Cloudflare Pages deploys every branch you push by default. For a small site with a handful of contributors that is convenient: every branch gets a preview URL without anyone configuring anything. On a busier repository it becomes noise and cost. Renovate opens a branch per dependency update, each of which builds; writers push work-in-progress branches several times an hour; branches that only touch the README or a workflow file still trigger a full site build; and dozens of preview URLs accumulate, each publicly reachable at a guessable hostname.
This guide configures branch deploy controls for a documentation site on Pages with about 30 contributors: which branches deploy at all, how preview aliases are named and shared, how builds are skipped for changes that cannot affect the site, and how previews are kept private. It is part of Cloudflare Pages Edge Caching Setup, and the general practice of previews is covered in Preview Environments for Pull Requests.
Prerequisites
- A Pages project connected to GitHub or GitLab (Git integration), or deployed with
wrangler pages deployfrom your own CI. - A naming convention for branches — here
docs/*for content work,feat/*andfix/*for code,renovate/*for dependency updates. - Cloudflare Zero Trust (Access) available on the account, for private previews.
The Starting Point
A month of build history before any controls:
| Source of builds | Builds per month | Useful previews |
|---|---|---|
main (production) | 118 | all |
docs/* branches | 640 | most, but many were intermediate pushes |
feat/*, fix/* | 210 | all |
renovate/* | 290 | few — reviewed through CI checks, not previews |
| README, workflow, config-only changes | 170 | none |
| Total | 1,428 |
The build queue on the project's plan allowed one concurrent build; at peak times writers waited over 20 minutes for their preview because Renovate branches were ahead of them in the queue.
Step 1: Choose Which Branches Deploy
In the project's Settings → Builds & deployments, set Preview branch deployments to "Custom branches", then include and exclude patterns:
Production branch: main
Preview branches include: docs/*, feat/*, fix/*
Preview branches exclude: renovate/*, dependabot/*, wip/*
Renovate branches no longer build on Pages; their changes are validated by the GitHub Actions build and tests, which is where dependency problems show up anyway. A wip/* prefix gives writers a way to push work without triggering previews at all. The same settings are available through the API and Terraform (cloudflare_pages_project with source.config.preview_branch_includes and preview_branch_excludes), which keeps them reviewable.
Step 2: Skip Builds for Changes That Cannot Affect the Site
Build watch paths limit builds to commits that touch the site's inputs:
Include paths: src/*, content/*, public/*, astro.config.mjs, package.json, package-lock.json
Exclude paths: *.md at the repository root, .github/*
A commit that only changes README.md, CONTRIBUTING.md or a workflow file now shows as "skipped" in the Pages dashboard. For one-off cases, [CI Skip] in a commit message skips the Pages build too.
Watch paths need care: a change to a shared data file outside the listed paths, or a new top-level config file, would silently not trigger a build. The team added a monthly review of the paths list and a note in the contributing guide.
Step 3: Stable, Shareable Preview Aliases
Each preview deployment has a unique hash URL (3f9c1a2b.docs-site.pages.dev) and a branch alias (docs-search-tuning.docs-site.pages.dev for docs/search-tuning). The alias always points at the latest deployment of the branch, which is what reviewers should get: a link that stays valid as the author pushes fixes. Branch names are normalized — lowercase, non-alphanumeric characters become hyphens, and long names are truncated — so a GitHub Action that comments the preview link on the pull request computes the alias from the branch name rather than guessing:
const alias = branch.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 28);
const url = `https://${alias}.docs-site.pages.dev/`;
The comment workflow, and how to update rather than duplicate comments, is described in Commenting on Preview Deploys.
Step 4: Keep Previews Private
Preview hostnames follow a predictable pattern, and work-in-progress documentation can include unreleased features. Two layers keep previews private:
- Cloudflare Access for previews. In the Pages project settings, enabling access policy for preview deployments puts
*.docs-site.pages.dev(excluding the production alias) behind an Access application. Contributors sign in with the company identity provider; reviewers outside the company get one-time PIN access by email when needed. noindexon non-production. Pages addsX-Robots-Tag: noindexto preview deployments by default; a_headersrule makes it explicit, and the production custom domain is excluded, as in robots.txt and noindex for Preview and Staging.
The production deployment's own pages.dev hostname is not a preview and is not covered by the preview Access policy; a Bulk Redirect rule sends it to the custom domain.
Direct Uploads From Your Own CI
Teams that build in GitHub Actions and upload with wrangler pages deploy dist --branch "$BRANCH" get the same aliases and Access protection, but branch controls in the Pages dashboard do not apply to direct uploads — the workflow decides. The equivalent rules then live in the workflow's on: filters and paths settings, and a job-level if: skips Renovate branches. This site used the Git integration for simplicity; a sibling site with a heavier build used direct uploads, following Automating Preview Deploy Pipelines with GitHub Actions.
Cleaning Up and Explaining the Rules
Pages keeps every deployment, including previews for branches deleted long ago, and each one remains reachable at its hash URL until removed. With Access on previews that is no longer a leak, but the list becomes noise in the dashboard and in any tooling that enumerates deployments. A weekly scheduled job lists preview deployments through the Pages API and deletes those whose branch no longer exists or whose last deployment is older than 30 days, following Cleaning Up Stale Preview Deployments. The first run removed 1,140 old previews.
The rules are only useful if contributors know them. The contributing guide gained a short table — branch prefix, whether it builds a preview, how to share it — and the pull request template links to it. Most confusion in the first week came from a writer naming a branch docs-fix-typo instead of docs/fix-typo, which matched no include pattern; after the table was added, the question never came up again.
Measured Impact
| Measure | Before | After |
|---|---|---|
| Builds per month | 1,428 | 786 |
| Median wait for a preview at peak times | 21 min | 3 min |
| Useful previews lost | — | 0 (none reported in 3 months) |
| Publicly reachable preview URLs | 160+ | 0 |
| Preview URLs indexed by search engines | 12 | 0 |
Pitfalls & Rollback
- Watch paths that miss an input. A build silently does not happen; review the paths list when adding config files.
- Excluding branches that need previews. Communicate the naming convention and the
wip/*escape hatch. - Assuming dashboard controls apply to direct uploads. They do not; put the rules in the workflow.
- Access without a reviewer path. External reviewers need one-time PIN or a guest policy.
- Rollback: every control is a project setting; switching preview deployments back to "All branches" restores the default immediately.
Conclusion
Default "deploy every branch" is fine until it is not. Include only the branch patterns that need previews, exclude dependency bots and work-in-progress branches, skip builds for changes that cannot affect the site, share stable branch aliases, and put previews behind Access. On this repository that halved the build count, cut preview waits at busy times from twenty minutes to three, and took every preview off the public internet.
FAQ
What are branch deploy controls on Cloudflare Pages?
Project settings that decide which Git branches trigger deployments. You can deploy all non-production branches, none, or a set chosen with include and exclude patterns, and separately choose the production branch.
How do preview aliases work?
Each preview deployment gets a unique URL with a hash, plus a branch alias such as feature-search.project.pages.dev that always points at the latest deployment of that branch. Branch names are normalized, so slashes and uppercase letters become hyphens and lowercase.
Can I skip a build for commits that do not change the site?
Yes. Cloudflare Pages skips builds when the commit message contains CI Skip or similar markers, and a build watch paths setting can limit builds to changes in specific directories. With direct uploads from your own CI, you decide when to deploy.
How do I keep previews private?
Put Cloudflare Access in front of the preview hostnames, which requires sign-in with your identity provider, and send X-Robots-Tag noindex on non-production deployments. Pages can enable Access for preview deployments with a single project setting.
Related
- Parent: Cloudflare Pages Edge Caching Setup — the Pages setup as a whole.
- Cloudflare Pages Functions Middleware — code on the request path.
- Password-Protecting Preview Deployments — other ways to keep previews private.
- Cleaning Up Stale Preview Deployments — removing old previews.
- Concurrency Groups to Cancel Stale Builds — the same idea in GitHub Actions.
Filed underHow-tosCloudflare