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.

Pages concepts and their Workers equivalents Pages build output directory maps to assets.directory in wrangler config. _headers and _redirects stay the same. Pages Functions map to the Worker script. Preview deployments map to Worker versions with preview URLs. Git integration maps to Workers Builds or a CI workflow. Custom domains map to Worker custom domains. Pages → Workers static assets Pages Workers build output directory assets.directory in wrangler config _headers, _redirects unchanged Pages Functions (/functions) Worker script (main) preview deployments versions with preview URLs Git integration Workers Builds or CI + wrangler deploy custom domains Worker custom domains static requests remain free on both; you pay only for requests that run Worker code
For a static site, the migration is mostly a new config file and a domain switch.

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. _headers applies 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": true or 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:

  1. In the Pages project, remove the custom domain.
  2. Immediately add it to the Worker: in wrangler.jsonc, add "routes": [{ "pattern": "docs.example.com", "custom_domain": true }] and run wrangler deploy, or add it under the Worker's Domains & Routes in the dashboard.
  3. Cloudflare creates the DNS record and certificate for the Worker. Requests in flight during the switch may see one error; most readers see nothing.
Custom domain cutover timeline Traffic goes to Pages until the domain is removed. Within about ten seconds the domain is added to the Worker and traffic flows there. The Pages project remains deployed for two weeks as a rollback target, then it is deleted. Cutover without downtime Pages kept as rollback target (2 weeks) Worker tested on workers.dev serves production traffic domain switch ≈ 10 s day 0 cutover day 14: delete Pages project
Reversing the two domain steps is the rollback plan until the Pages project is deleted.

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.

Deploy times before and after the migration For a 2,400-page site, the Pages Git integration took 3 minutes 40 seconds from push to live. The Worker deployed from GitHub Actions with cached dependencies took 2 minutes 10 seconds, with the upload step taking 14 seconds because only changed files are sent. Push to live, 2,400-page docs site Pages Git integration 3 min 40 s Actions + wrangler deploy 2 min 10 s upload 14 s most of the gain came from dependency and build caching in CI, not from the platform
Only changed assets are uploaded, so the deploy step stays short as the site grows.

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_handling from Pages' behaviour changes canonical URLs; compare redirects before cutover.
  • run_worker_first left 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 vars or 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.