Migrating Astro from Vercel to Cloudflare Pages

Moving a static Astro site from Vercel to Cloudflare Pages is mostly straightforward, but there are a few sharp edges worth documenting.

Cloudflare Pages is a compelling alternative to Vercel for static sites—especially if you’re already using Cloudflare for DNS and want to consolidate. Here’s what I learned migrating an Astro site.

The adapter

First, swap the Vercel adapter for Cloudflare’s:

pnpm remove @astrojs/vercel
pnpm add @astrojs/cloudflare wrangler

Configure astro.config.mjs:

import cloudflare from "@astrojs/cloudflare";

export default defineConfig({
  adapter: cloudflare({
    imageService: "passthrough",
  }),
});

Proxying external APIs

Vercel’s vercel.json supports rewrites for proxying external services. In my case, I was proxying Plausible Analytics to avoid ad blockers:

{
  "rewrites": [
    { "source": "/api/event", "destination": "https://plausible.io/api/event" },
    { "source": "/js/script.js", "destination": "https://plausible.io/js/script.js" }
  ]
}

Cloudflare doesn’t have an equivalent. You need server endpoints.

Create src/pages/api/event.ts:

export const prerender = false;

export async function POST({ request }: { request: Request }) {
  const url = new URL(request.url);
  const plausibleUrl = new URL("https://plausible.io/api/event");

  url.searchParams.forEach((value, key) => {
    plausibleUrl.searchParams.set(key, value);
  });

  const response = await fetch(plausibleUrl.toString(), {
    method: "POST",
    headers: {
      "Content-Type": request.headers.get("Content-Type") || "application/json",
      "User-Agent": request.headers.get("User-Agent") || "",
      "X-Forwarded-For": request.headers.get("X-Forwarded-For") ||
                         request.headers.get("CF-Connecting-IP") || "",
    },
    body: request.body, // Stream, don't buffer
  });

  return new Response(response.body, {
    status: response.status,
    headers: {
      "Content-Type": response.headers.get("Content-Type") || "application/json",
    },
  });
}

The critical detail: export const prerender = false; prevents Astro from trying to prerender the endpoint at build time, which fails when it tries to fetch from an external service.

Also note body: request.body streams the request through without buffering into memory. Same for response.body in the return value.

Headers and redirects

Vercel’s vercel.json handles headers and redirects. Cloudflare Pages uses platform-native files instead.

Create public/_headers:

/*
  Content-Security-Policy: base-uri 'none'; child-src 'self'; ...
  Permissions-Policy: camera=(), microphone=(), geolocation=()
  Referrer-Policy: no-referrer-when-downgrade
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY

Create public/_redirects:

/old-path /new-path 301

These files get copied to dist/ during build and Cloudflare respects them.

The node:crypto problem

When testing locally with pnpm wrangler pages dev dist, you might hit:

Error: No such module "node:crypto"

Cloudflare Workers don’t support Node.js built-ins by default. Enable them in wrangler.jsonc:

{
  "compatibility_flags": ["nodejs_compat"]
}

Image optimization

The Cloudflare adapter supports imageService: "cloudflare" for runtime image optimization via Cloudflare Images. But if your pages are all prerendered (the usual case for static sites), images are already optimized at build time by Sharp.

Runtime optimization only matters for server-rendered pages that need dynamic image sizing. Check your build output—if you see symbols (not λ), those pages are prerendered and don’t need the runtime service.

Use imageService: "passthrough" unless you specifically need runtime transforms.

Local testing

pnpm build
pnpm wrangler pages dev dist

This runs your site with Cloudflare’s local runtime. The _headers, _redirects, and Pages Functions all work exactly like production.

Deploy

Connect your repo to Cloudflare Pages via the dashboard. Set:

  • Build command: pnpm build
  • Output directory: dist

The wrangler.jsonc configuration gets picked up automatically.

References