Back to Blog
Engineering

Deploying Astro 6 on Cloudflare Workers

Sri Annamalai

Sri Annamalai

Shipping this site was supposed to be a weekend job. Astro 6 made me earn it. Here are the three potholes I hit, documented so the next person doesn’t.

1. Workers, not Pages

Astro 6’s Cloudflare adapter auto-provisions an ASSETS binding. Cloudflare Pages reserves that exact binding name. The two collide, and there is no workaround — the Vite plugin that ships with Astro 6 only supports Workers (see withastro/astro#16107).

Practical fix: use wrangler deploy, not wrangler pages deploy. Your wrangler.toml needs an [assets] section, never pages_build_output_dir.

2. Astro.locals.runtime.env is gone

Astro 6 removed the familiar Astro.locals.runtime.env access. Running any old code that touches it throws a runtime error pointing at the replacement:

import { env } from 'cloudflare:workers';

// in a .astro page or API route
const secret = env.RESEND_API_KEY;

Type it via an ambient interface Env in src/env.d.ts. No more App.Locals augmentation for env vars. During astro dev, .dev.vars is read as long as you have platformProxy: { enabled: true } in the adapter config.

3. Hybrid is dead — long live per-page prerender

output: 'hybrid' was removed. The new pattern:

// astro.config.mjs
export default defineConfig({ output: 'server' });

…and every static page explicitly opts in:

---
export const prerender = true;
---

No flag → server-rendered. Simple, but easy to miss on a migration.

Also worth knowing

  • compatibility_flags = ["nodejs_compat"] in wrangler.toml — required for Svelte SSR on Cloudflare.
  • Svelte 5 runes mode: onMount crashes with lifecycle_outside_component. Use $effect instead.
  • Scoped <style> blocks won’t match classes added via classList.add() — either use :global() or drive visibility with $state reactivity.

Nothing catastrophic. But the kind of papercuts you want to find on your own site, not the client’s.