Migrating from Cloudflare Pages to Workers Static Assets
Cloudflare Pages was, for years, the default home for static sites on Cloudflare. Workers can now serve static assets directly, with the same free, unlimited static requests, and Cloudflare is putting new capabilities there first: observability, Durable Objects, Cron Triggers, and a single configuration file for static and dynamic parts of a project. For a purely static site the move is mostly configuration, and it can be done without downtime.
This guide migrates a static site from Pages to a Worker with static assets, covering the Wrangler configuration, _headers and _redirects, trailing slashes and 404 handling, preview deployments, CI, and the custom domain cutover. It is part of Cloudflare Pages Edge Caching Setup.
Prerequisites
- A static site currently deployed on Cloudflare Pages.
- Wrangler 3.90 or later (
npm install -D wrangler) and a Cloudflare API token with Workers edit permissions. - Access to the custom domain's DNS zone in the same Cloudflare account.
What Changes and What Does Not
The build does not change: the generator still writes a folder of HTML, CSS and assets. What changes is how that folder is deployed and configured.
Step 1: Write the Wrangler Configuration
Create wrangler.jsonc at the repository root. A purely static site needs no script at all:
{
"name": "docs-site",
"compatibility_date": "2026-09-01",
"assets": {
"directory": "./dist",
"html_handling": "auto-trailing-slash",
"not_found_handling": "404-page"
}
}
html_handling controls clean URLs. auto-trailing-slash, the default, serves /about/index.html at /about/ and redirects /about to it, matching Pages' behaviour. Use force-trailing-slash or drop-trailing-slash if your site's canonical URLs require one form. not_found_handling set to 404-page serves the nearest 404.html with a 404 status; use single-page-application only for client-routed apps.
If the site had Pages Functions, add "main": "src/worker.ts" and move the function code into a Worker, calling env.ASSETS.fetch(request) to fall through to static files. Declare the binding with "binding": "ASSETS" inside the assets block.
Step 2: Keep _headers and _redirects
Workers static assets read _headers and _redirects from the assets directory with the same syntax as Pages. Leave them where they are. Two differences to check:
- Headers on Worker responses.
_headersapplies only to responses served from static assets. If a Worker script generates a response, set headers in code. - Run order. By default, a request that matches a static file is served without invoking the Worker script. If you need the script to run first — for authentication, for example — set
"run_worker_first": trueor a list of path patterns, and remember each such request is billed.
Security headers and cache rules carry over unchanged; see Writing a Content Security Policy for a Static Site and Configuring Redirects on Cloudflare Pages.
Step 3: Deploy and Test on workers.dev
Build and deploy:
npm run build
npx wrangler deploy
Wrangler uploads only changed files, prints the Worker's workers.dev URL, and the new site is live there while production traffic still goes to Pages. Test it as you would a preview: a crawl for broken links, a check of redirects and response headers, and a spot check of the 404 page. A header comparison between the two hosts catches most regressions:
for p in / /docs/ /docs/install/ /assets/app.css /missing/; do
diff <(curl -sI "https://docs.example.com$p" | sort) \
<(curl -sI "https://docs-site.acct.workers.dev$p" | sort) \
| grep -Ev '^[<>] (date|cf-ray|age|server-timing):' && echo "differs: $p"
done
Step 4: Move CI
On Pages, the Git integration built and deployed each push. For Workers, either connect the repository to Workers Builds in the dashboard, which works the same way, or deploy from your existing CI:
- run: npm ci && npm run build
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy
For pull requests, wrangler versions upload creates a new version without sending traffic to it and returns a preview URL, which replaces Pages' per-branch preview deployments. Wire that URL into the pull request comment the same way; see Automating Preview Deploy Pipelines with GitHub Actions.
Step 5: Switch the Custom Domain
The cutover takes seconds and is reversible:
- In the Pages project, remove the custom domain.
- Immediately add it to the Worker: in
wrangler.jsonc, add"routes": [{ "pattern": "docs.example.com", "custom_domain": true }]and runwrangler deploy, or add it under the Worker's Domains & Routes in the dashboard. - Cloudflare creates the DNS record and certificate for the Worker. Requests in flight during the switch may see one error; most readers see nothing.
Afterwards, purge the zone cache once if cache rules depended on the old hostname, and watch error rates and 404 counts for a day. Keep the Pages project deployed for a couple of weeks so rollback is just the same two steps in reverse. For a whole-site safety net, see Rolling Back a Bad Static Deploy in Under a Minute.
Porting Pages Functions
Many "static" Pages projects carry one or two functions: a contact form handler, an authentication check for a private section, or an API proxy. Pages routed them by file path under /functions. In a Worker, routing is up to you. For a handful of routes, a small switch on url.pathname is enough; for more, a router such as Hono keeps the code readable. Anything that does not match falls through to env.ASSETS.fetch(request).
Bindings move to wrangler.jsonc: KV namespaces, D1 databases, R2 buckets and secrets declared in the Pages dashboard need matching entries in the config, and secrets are set with wrangler secret put. Middleware written as _middleware.ts becomes ordinary code at the top of the Worker's fetch handler. Test each ported route on the workers.dev URL before cutover; the header comparison script only covers static responses. For form handling specifically, see Adding a Contact Form with Cloudflare Workers.
Rollbacks on Workers
Workers keep a history of deployed versions. wrangler rollback returns production to the previous version in seconds, and the dashboard lists versions with their messages. Versions also support gradual deployments — sending, say, 10% of traffic to a new version before promoting it — which Pages did not offer; see Canary Releases for Static Sites. Pass --message "$(git log -1 --format=%s)" on deploy so each version is identifiable later.
Measured Impact
A 2,400-page documentation site moved from Pages to a Worker with static assets and no script. Response headers, redirects and 404 behaviour were identical in the comparison script. TTFB at the edge was unchanged within measurement noise. Push-to-live time fell from 3 minutes 40 seconds to 2 minutes 10 seconds, mainly because the move to GitHub Actions added dependency caching. The cutover produced no errors in the zone's analytics, and the Pages project was deleted after 14 days.
Pitfalls & Rollback
- Trailing slash mode. A different
html_handlingfrom Pages' behaviour changes canonical URLs; compare redirects before cutover. run_worker_firstleft on. Every request then invokes the script and is billed; enable it only for paths that need it.- Functions directory ignored. Workers do not read
/functions; port the code to the Worker script. - Environment variables. Pages project variables do not move automatically; add them under
varsor as secrets. - Rollback: move the custom domain back to the Pages project.
Conclusion
For a static site, moving from Cloudflare Pages to Workers static assets is a configuration change: a wrangler.jsonc with an assets directory, the same _headers and _redirects, a new deploy step in CI, and a ten-second domain switch. Test on the workers.dev URL first, keep the Pages project as a rollback target for a couple of weeks, and the site gains versioned deploys, gradual rollouts and room to add server code later without changing platforms.
FAQ
Why move a static site from Cloudflare Pages to Workers?
Cloudflare now directs new features to Workers, which can serve static assets directly and add code only where needed. Workers also give you Durable Objects, Cron Triggers, observability features and one deployment model for static and dynamic parts of a site.
Do _headers and _redirects files still work on Workers?
Yes. Workers static assets read _headers and _redirects from the assets directory with the same syntax and limits as Pages, so most static sites can move without rewriting those files.
Does a Worker serving static assets cost more than Pages?
Requests that are served purely from static assets are free and unlimited, as on Pages. You pay only when a request invokes your Worker script, so a site with no script, or with run_worker_first disabled, costs the same.
How do I avoid downtime when switching the custom domain?
Deploy the Worker and test it on its workers.dev URL first. Then remove the custom domain from the Pages project and immediately add it to the Worker; the switch takes seconds, and keeping the Pages project for a while lets you roll back by reversing the step.
Related
- Parent: Cloudflare Pages Edge Caching Setup — the full Cloudflare setup.
- Deploying Hugo to Cloudflare Pages and Workers — a generator-specific walkthrough.
- Custom Domains and TLS on Cloudflare Pages — domains and certificates in detail.
- Configuring Redirects on Cloudflare Pages — the _redirects file you keep.
- Canary Releases for Static Sites — using Worker versions for gradual rollouts.