All articles
cloudflare

Migrating a Nuxt site from Cloudflare Pages to Workers

The full path from the cloudflare-pages preset to a cloudflare_module Worker: the config changes, the custom-domain cutover that no deploy step handles for you, and the gotchas that only show up in CI.

Vinayak Kulkarni
3 min read

I moved this site off Cloudflare Pages and onto a Cloudflare Worker. The reasons were concrete: Workers gives you the Workers Cache API, smart placement, per-Worker observability, and a deploy model that no longer treats the framework output as a second-class citizen. Pages had served the site fine for years, but the ceiling was lower than where I wanted to build.

Here is what the migration actually involved, including the parts no tutorial warns you about.

The preset and the deploy config

The core change is the Nitro preset. Pages uses cloudflare-pages; a Worker uses cloudflare_module. With the module preset you also inline the deploy config so Nitro generates a complete wrangler.json at build time:

export default defineNuxtConfig({
  nitro: {
    preset: 'cloudflare_module',
    cloudflare: {
      deployConfig: true,
      wrangler: {
        name: 'my-site',
        workers_dev: false,
        d1_databases: [
          { binding: 'DB', database_name: 'my-site-db', database_id: '...' },
        ],
        cache: { enabled: true },
        observability: { enabled: true },
        placement: { mode: 'smart' },
      },
    },
  },
});

Because Nitro passes arbitrary keys in that wrangler block straight through, you configure the whole Worker here: the D1 binding, smart placement, observability, and Workers Cache all live in one place. I deleted the standalone wrangler.jsonc entirely; the generated file is now the source of truth. The CD step changed to deploy that generated file directly:

wrangler deploy --config .output/server/wrangler.json

The custom-domain cutover nobody automates

This is the part that surprised me. Deploying the Worker does not move your custom domain to it.

My site was a proxied CNAME pointing at the Pages project. The Pages project owned the domain. When I deployed the Worker with workers_dev: false and no routes, Wrangler reported "No targets deployed", which is technically correct: the Worker had nowhere to serve from because the domain still belonged to Pages.

There is no deploy flag for this. You move the custom domain from the Pages project to the Worker manually in the Cloudflare dashboard. It is a one-time, user-driven step, and it is the moment the switch actually happens. Everything before it is preparation.

One nice side effect: after the cutover, the Worker's own robots.txt route replaced Cloudflare's managed AI-bot block automatically. I did not need a separate toggle. Your MX and TXT records (mail, SPF) are untouched throughout, since you are only re-pointing the site's hostname.

Two gotchas that only appeared in CI

Formatting shims. This repo runs oxfmt and oxlint through vite-plus LSP shims. Running the shimmed formatter locally corrupted source files: doubled quotes, mangled strings. The fix was to call the real binaries directly (./node_modules/.bin/oxfmt, ./node_modules/.bin/oxlint). CI was fine because it invokes them through pnpm run, which resolves the real binaries. So the corruption only happened in local ad-hoc runs, which is exactly the kind of divergence that wastes an afternoon.

A quality gate that passed locally and failed in CI. I run a doctor action on deploy. It passed identically on my machine (zero errors, healthy score, HTTP 200) but failed in CI. One real cause was findable and fixed: the action was forwarding an invalid preset value. A second divergence I could not reproduce. Rather than chase a CI-only ghost that was blocking every deploy, I made the gate advisory: it still reports and comments on pull requests, but it no longer sits in the deploy's critical path. Advisory-but-visible beats blocking-but-flaky.

Was it worth it

Yes, but be honest about the shape of the work. The config migration is an afternoon. The custom-domain cutover is five minutes of dashboard clicking that you cannot script. The long tail is the CI divergences, which are where the real time goes. Budget for the tail, not the config.

cloudflareworkersnuxtnitrodevops