Your Astro site builds locally with output: 'static' and ships to Cloudflare or Vercel. Deploy succeeds. Half the pages 404, the other half render blank with no console errors. Or the reverse: you set output: 'server', added the adapter, and now astro build fails with Cannot find adapter for output mode "static" or [config] Cannot use prerender = false without an adapter. That is the classic Astro adapter-mismatch trap.
Fastest fix: pick one deploy model and align three things to it — the output value in astro.config.mjs, whether an adapter: is wired in, and every per-route export const prerender = ... flag. A single contradicting prerender line on one page is the most common cause of “one route 404s in prod, works in dev.”
One thing that trips up older guides: as of Astro 5 (and unchanged in Astro 6, June 2026), output: 'hybrid' no longer exists. The default output: 'static' already supports per-route export const prerender = false, so “hybrid” is just static plus opt-out routes plus an adapter. If your config still says hybrid, that is your first bug.
Common causes
Ordered by how often we see each on real Cloudflare and Vercel deploys.
1. Config still uses the removed output: 'hybrid'
Astro 5 removed output: 'hybrid' entirely (it was not just deprecated). The two valid values are now output: 'static' (default; statically generates everything, with optional export const prerender = false per route to opt that route into on-demand rendering) and output: 'server' (on-demand renders everything, with optional export const prerender = true per route).
How to spot it: on Astro 5.x or 6.x, astro.config.mjs has output: 'hybrid' and the build errors with Invalid value for "output" or warns that the value is unknown.
2. Adapter installed but not wired into astro.config.mjs
npm install @astrojs/cloudflare installs the package; you still need import cloudflare from "@astrojs/cloudflare" and adapter: cloudflare() in the config. The cleaner path is npx astro add cloudflare, which installs the package, wires the import, sets output, and scaffolds config in one step. Without the adapter: line, any route with prerender = false errors at build time with Cannot use server-rendered pages without an adapter.
How to spot it: build log shows zero on-demand-rendered routes even though you have endpoint files, or it fails the moment a prerender = false route is reached.
3. Per-route export const prerender contradicts the global mode
A route file has export const prerender = false while output: 'static' and no adapter is installed. The build fails. Or you have an adapter but expect that page to be static (it is not — it now needs a runtime the static host does not provide), so it 404s in production while rendering fine in dev.
How to spot it: one specific page 404s in prod but renders in dev. grep -rn "prerender" src/pages/ finds the contradicting line. Note: Astro 5+ only accepts the literal values true / false for prerender — dynamic expressions are no longer allowed.
4. Wrong adapter major version for your Astro version
The Cloudflare adapter API has changed substantially across majors. As of June 2026, @astrojs/cloudflare is on v13.x; Astro 6 requires v13+, and the latest Astro 5 line wants the matching recent major. Mixing an old adapter with a new Astro (or vice versa) either fails to build or produces a non-functional bundle.
How to spot it: build error mentions is not a function, Adapter API changed, an unknown adapter option, or peer astro@... required. Run npm ls astro @astrojs/cloudflare and compare.
5. Deploying the Cloudflare adapter to Cloudflare Pages (no longer supported)
This is the big one for anyone following a pre-2026 tutorial. @astrojs/cloudflare dropped Cloudflare Pages support and now targets Cloudflare Workers only (Workers with static-asset hosting). Pointing a Pages project at the new adapter’s output produces a build that deploys but 404s, because Pages cannot route the dist/_worker.js/ Worker bundle the way wrangler deploy to Workers does.
How to spot it: you deploy via the Pages build/Git integration, build succeeds, every dynamic route 404s. Older config options like mode: 'directory', mode: 'advanced', functionPerRoute, and cloudflareModules are gone — if your config still passes them, that confirms you are on an old mental model.
6. output: 'server' but the deploy uses the wrong entry / wrong host wiring
For Cloudflare Workers, the build emits dist/_worker.js/ plus static assets, and you deploy with wrangler deploy using a wrangler.jsonc/wrangler.toml that points main at the worker and sets the ASSETS binding. For Vercel, the adapter emits the .vercel/output Build Output API layout. Use the wrong entry or skip the wrangler config and every page 404s.
How to spot it: build succeeds; deploy “works”; every route returns 404. dist/_worker.js/ exists but wrangler.jsonc has no main or no assets block, or you uploaded dist/ to Pages instead of running wrangler deploy.
7. SSR-only APIs used in a statically prerendered page
Astro.request, Astro.cookies, Astro.redirect, and request headers require on-demand rendering. Used in a route that is statically prerendered (default output: 'static' with no prerender = false), they resolve at build time, not per request — so cookies/headers read as empty or stale in production.
How to spot it: a page that reads cookies/headers shows empty or build-time values in production despite logging fine in dev. The fix is export const prerender = false on that route (plus an adapter).
8. Deploy target’s runtime missing the compatibility flag
The Cloudflare Workers runtime needs nodejs_compat to import Node built-ins (node:buffer, node:crypto, node:path) that your code or a transitive dependency pulls in. On Vercel, the Node runtime version is controlled by your project/package.json engines rather than a magic adapter flag.
How to spot it: runtime error Cannot find package 'node:buffer', No such module "node:...", or Compatibility flag required in wrangler tail / the Workers logs.
Which bucket are you in
| Symptom | Most likely cause | Jump to |
|---|---|---|
astro build errors on output value | Removed hybrid (cause 1) | Step 2 |
| Build fails: “cannot use prerender = false without an adapter” | Adapter not wired (cause 2) | Step 2 |
| One route 404s in prod, fine in dev | Contradicting prerender flag (cause 3) | Step 3 |
Build error: is not a function / unknown option | Adapter/Astro version mismatch (causes 4, 5) | Step 4 |
| Deploy succeeds, EVERY dynamic route 404s | Pages-vs-Workers mismatch or wrong entry (causes 5, 6) | Step 5 |
| Cookies/headers empty only in prod | SSR API on a static page (cause 7) | Step 3 |
Cannot find package 'node:...' at runtime | Missing compat flag (cause 8) | Step 6 |
Before you start
- Capture the exact Astro version:
npx astro --version. - Capture the exact adapter package + version:
npm ls @astrojs/cloudflare(or@astrojs/vercel,@astrojs/node,@astrojs/netlify). - Note your
astro.config.mjsoutputvalue and anyadapter:line. - Check whether ANY route in
src/pages/hasexport const prerender = ...:grep -rn "prerender" src/pages/. - Confirm which deploy model you actually want (all-static, all-SSR, or static-with-opt-out routes) and which exact host product (Cloudflare Workers vs Pages, Vercel Functions, Netlify, a Node server).
Step-by-step fix
Ordered by ROI.
Step 1: Decide your deploy model first, then align everything
Pick one explicitly:
- All-static:
output: 'static', no adapter, noprerender = falseanywhere. Deploys as plain HTML to any static host. - Mixed (most common):
output: 'static'globally,export const prerender = falseon the routes that need a request at runtime, adapter installed and wired. This is what used to be called “hybrid.” - All-SSR:
output: 'server', adapter installed and wired, optionalexport const prerender = trueon truly static pages.
Everything below is guesswork until this is decided.
Step 2: Audit and reconcile astro.config.mjs
If you see output: 'hybrid', change it to output: 'static' and rely on per-route prerender = false (Step 3). For mixed mode on Cloudflare Workers (Astro 5/6, June 2026):
// astro.config.mjs
import { defineConfig } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
export default defineConfig({
output: "static",
adapter: cloudflare(),
site: "https://example.com",
});
For all-SSR on Vercel (note the import path is bare @astrojs/vercel, not @astrojs/vercel/serverless — the subpath exports were removed):
import { defineConfig } from "astro/config";
import vercel from "@astrojs/vercel";
export default defineConfig({
output: "server",
adapter: vercel(),
});
The safest way to add or repair an adapter is npx astro add cloudflare (or vercel, node, netlify), which sets output, wires the import, and installs the matching adapter major for your Astro version.
Step 3: Find and clean up contradictory prerender flags
grep -rn "prerender" src/pages/
For every match, decide whether that route needs a request at runtime:
- Global
static, this route needs SSR -> keepexport const prerender = falseand make sure an adapter is wired. - Global
static, this route is also static -> delete the line. - Global
server, this route can be prebuilt -> useexport const prerender = true.
Remember prerender must be a literal true/false in Astro 5+ (no variables), and putting it in a layout or component instead of a page/endpoint file does nothing.
Step 4: Match adapter major to Astro major
npm ls astro
npm ls @astrojs/cloudflare
As of June 2026: @astrojs/cloudflare is on v13.x (Astro 6 requires v13+); @astrojs/vercel is on v10.x. Reinstall the latest matching adapter rather than guessing a number:
npm install @astrojs/cloudflare@latest
Read the adapter CHANGELOG for breaking changes before bumping a major — the Cloudflare adapter replaced the old runtime options with platformProxy, removed mode/functionPerRoute/cloudflareModules, and dropped Pages support. See astro deploy page not found for the 404 fallout this causes.
Step 5: Verify the build output and wire the host correctly
npm run build
ls dist/
Expected per target as of June 2026:
| Target | Expected dist/ layout | How you deploy |
|---|---|---|
| Cloudflare Workers | dist/_worker.js/ (a directory) + static assets in dist/ | npx wrangler deploy with a wrangler.jsonc |
| Vercel | dist/.vercel/output/ (Build Output API: functions/ + static/) | Vercel build/Git integration |
| All-static | dist/index.html and friends, NO _worker.js | upload dist/ to any static host |
For Cloudflare Workers your wrangler.jsonc must point at the worker and bind the assets:
{
"name": "my-astro-site",
"main": "./dist/_worker.js/index.js",
"compatibility_date": "2026-06-18",
"compatibility_flags": ["nodejs_compat"],
"assets": { "binding": "ASSETS", "directory": "./dist" }
}
If you were previously on Cloudflare Pages, that is now the mismatch: the modern adapter targets Workers. Migrate by adding the wrangler.jsonc above and deploying with wrangler deploy instead of the Pages Git build. If you cannot move off Pages yet, pin the last adapter major that still supported Pages and freeze it — but plan the Workers migration.
Step 6: Set the Cloudflare compatibility flag in the repo
Keep nodejs_compat in checked-in wrangler.jsonc/wrangler.toml (under compatibility_flags), not only in a dashboard, so every deploy and every contributor gets it:
"compatibility_flags": ["nodejs_compat"]
Without it the deployed Worker cannot import the Node built-ins your code or its transitive deps expect, and you get No such module "node:..." at request time.
Step 7: Deploy a minimal probe route to confirm the model end-to-end
Create one on-demand endpoint:
// src/pages/probe.json.ts
export const prerender = false;
export const GET = ({ request }) => {
return new Response(
JSON.stringify({ ok: true, url: request.url }),
{ headers: { "content-type": "application/json" } },
);
};
After deploy, hit /probe.json. Live JSON means SSR routing works. A 404 means the adapter/host wiring is still wrong — go back to Step 5 and check the dist/ layout and wrangler.jsonc.
How to confirm it’s fixed
npm run buildreports a non-zero count of on-demand routes when you expect SSR, and nooutput/prerendererrors.dist/layout matches the deploy target’s expected layout in the Step 5 table exactly.- The probe route returns live JSON in production (not a 404, not build-time-cached values).
- All previously-404 routes resolve.
- Cookie/header-reading routes return live per-request values in production, not empty strings.
wrangler tail(Cloudflare) shows noNo such module "node:..."errors.
Long-term prevention
- Pin the adapter major in
package.jsonand bump it in lockstep with Astro majors; read the adapter CHANGELOG before each major bump. - Add a CI check that greps
prerenderdirectives and fails if any contradict the globaloutput(or if a layout/component contains one). - Keep one probe route deployed at all times so a routing regression is visible immediately.
- For Cloudflare Workers, commit
wrangler.jsoncwithcompatibility_flagsandcompatibility_dateto the repo — never rely only on the dashboard. - Document the deploy model (and the host product — Workers, not Pages) in
README.mdso contributors do not flip flags casually. - Do not reach for
output: 'hybrid'; it no longer exists in Astro 5/6.
Common pitfalls
- Installing the adapter package but forgetting
adapter: cloudflare()in the config — anyprerender = falseroute then fails the build. - Following a pre-2026 tutorial and deploying the modern Cloudflare adapter to Cloudflare Pages — it targets Workers now and the dynamic routes 404.
- Passing removed options (
mode: 'directory',functionPerRoute,cloudflareModules) and assuming they still do something — they are silently ignored or error. - Importing
@astrojs/vercel/serverless— the subpath exports were removed; import bare@astrojs/vercel. - Pasting
export const prerender = falseinto a layout or component (not a page/endpoint) — it has no effect, giving a false sense of a fix. - Assuming
output: 'static'+Astro.cookiesworks because dev shows it working — dev always runs on-demand. See static site blank page for the symptom.
FAQ
Q: My config still says output: 'hybrid'. Will it build on Astro 5 or 6?
No. Astro 5 removed hybrid (it was not merely deprecated). Use output: 'static' and add export const prerender = false to the routes that need on-demand rendering, plus an adapter. That combination is exactly what “hybrid” used to mean.
Q: My Cloudflare Pages deploy 404s on every dynamic route. Did something change?
Yes. The @astrojs/cloudflare adapter dropped Cloudflare Pages support and now targets Cloudflare Workers. Add a wrangler.jsonc that points main at ./dist/_worker.js/index.js, binds ASSETS to ./dist, sets nodejs_compat, and deploy with wrangler deploy instead of the Pages Git build.
Q: Can I switch adapters without changing my code?
Mostly, for portable APIs like Astro.request / Astro.cookies. Platform-specific access differs — Astro.locals.runtime.env on Cloudflare vs process.env on Node — and requires code changes. The adapter docs list these.
Q: Build succeeds but every route 404s in production. Where do I start?
Check the dist/ layout against the Step 5 table first. If it does not match the host’s expectation, the adapter target or your wrangler.jsonc/Vercel wiring is wrong. See astro deploy page not found for the full 404 fault tree.
Q: Why does npm run dev show no problems even when the deploy is broken?
Astro dev runs everything on demand via Vite, effectively ignoring output and prerender flags. Production builds enforce them. Always test against npm run preview (or a deploy preview) before merging.
Related
- Deploy Preview URLs Got Indexed by Google
- GitHub Actions Deploy Step Times Out After 6 Hours
- Monorepo Deploy Only Ships One App Out of Several
- Netlify Function Cold Start Times Out at 10s
- Service Worker Serves Stale Bundle After Deploy
- Vercel Build Exceeds 45-Minute Limit and Cancels
- Next.js ISR Revalidation Stuck Serving Stale Pages
Tags: #Troubleshooting #Astro #adapter #ssr #ssg