Your Astro site builds locally with output: 'static' and ships to Cloudflare Pages 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 Node adapter, and now astro build fails with Cannot find adapter for output mode "static". This is the classic Astro adapter-mismatch trap. Astro 4/5 made the model more orthogonal — output: 'static' | 'server' | 'hybrid' (or unified output: 'static' | 'server' with per-route prerender = true in Astro 5) — but the deploy target’s adapter must match, and individual route prerender flags can silently override your global mode.
Common causes
Ordered by how often we see each on Cloudflare Pages and Vercel.
1. Astro 5 unified output model still mixed with old hybrid
Astro 5 deprecated output: 'hybrid'. The replacement is output: 'static' with export const prerender = false on dynamic routes (or output: 'server' with prerender = true on static routes). Some configs still set hybrid and the build silently picks the wrong mode.
How to spot it: astro.config.mjs has output: 'hybrid' on Astro 5.x. Build log shows a deprecation warning that gets buried.
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. Without it, Astro builds with the static defaults and the SSR routes become 404s.
How to spot it: Build log says (SSR routes) count is zero, but you have endpoint routes. Cloudflare Pages Function tab shows no functions.
3. Per-route export const prerender = true/false contradicts the global mode
A route file accidentally has export const prerender = false while output: 'static'. Astro happily generates the page as SSR — but the static-site deploy target has no SSR runtime, so the page 404s in production.
How to spot it: One specific page 404s in prod but renders fine in dev. grep -r "prerender" src/pages/ finds the contradicting line.
4. Wrong adapter version for your Astro version
@astrojs/cloudflare@9 and @astrojs/cloudflare@12 have different APIs. Astro 5 requires the newer adapter; Astro 4 requires the older. Mixed versions either fail to build or build a non-functional bundle.
How to spot it: Build error mentions is not a function, Adapter API changed, or expected APIContext to have .... Check npm ls @astrojs/cloudflare against your Astro version.
5. output: 'server' but no entry function generated for the target
Some adapters require mode: 'directory' or mode: 'advanced' (Cloudflare), or edge: true / false (Vercel). Default mode generates the wrong file layout for what the platform expects.
How to spot it: Build succeeds; Cloudflare Pages deploys; every page returns 404. dist/_worker.js is missing or dist/functions/[[path]].js is missing depending on the mode you needed.
6. Mixing Astro.cookies / Astro.request in static-only pages
These APIs require SSR. Used in an output: 'static' page, the build silently outputs nothing useful for them — the page renders but the values are undefined.
How to spot it: A page that reads cookies/headers shows empty values in production despite logging fine in dev. Build log warns Astro.request is not available in static pages.
7. Deploy target’s runtime version below adapter’s minimum
The Cloudflare adapter needs Workers compatibility flags nodejs_compat; the Vercel adapter for Node 20 needs runtime set in adapter options. Missing → adapter generates output the platform cannot execute.
How to spot it: Deploy log shows Cannot find package 'node:buffer' or Compatibility flag required.
Before you start
- Capture exact Astro version:
npx astro --version. - Capture exact adapter package + version:
npm ls @astrojs/cloudflare(or vercel/node/netlify). - Note your
astro.config.mjsoutputvalue and anyadapter:line. - Check whether ANY page in
src/pages/hasexport const prerender = .... - Confirm which deploy target you actually want (static-only, full SSR, or hybrid per-route).
Information to collect
- Full
astro.config.mjscontent. - Output of
find src/pages -name "*.astro" -o -name "*.ts" | xargs grep -l "prerender". - Build log final summary: count of static routes vs. SSR routes vs. endpoints.
- Contents of
dist/after a local build: does it have_worker.js,server/,functions/, or just static HTML? - Deploy platform: Cloudflare Pages (Functions or Workers?), Vercel (Serverless or Edge?), Netlify, Node host.
- Any runtime compatibility flags set on the platform side.
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. - All-SSR:
output: 'server', adapter installed and wired, optionalprerender = trueon truly static pages. - Mixed (Astro 5+):
output: 'static'globally,export const prerender = falseon SSR pages, adapter installed and wired.
Without this decision the rest is guesswork.
Step 2: Audit and reconcile astro.config.mjs
For Astro 5 mixed mode on Cloudflare Pages:
// astro.config.mjs
import { defineConfig } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
export default defineConfig({
output: "static",
adapter: cloudflare({ mode: "directory" }),
site: "https://example.com",
});
For all-SSR on Vercel Serverless:
import { defineConfig } from "astro/config";
import vercel from "@astrojs/vercel/serverless";
export default defineConfig({
output: "server",
adapter: vercel(),
});
Step 3: Find and clean up contradictory prerender flags
grep -rn "prerender" src/pages/
For every match, decide: does this page need SSR? If global is static and you want this one SSR, keep export const prerender = false. If global is static and the page is also static, remove the line entirely. If global is server and you want this one prerendered, use export const prerender = true.
Step 4: Match adapter version to Astro version
npm ls astro
npm ls @astrojs/cloudflare
For Astro 5.x, install adapter 12.x:
npm install @astrojs/cloudflare@latest
Read the adapter’s CHANGELOG for breaking changes (mode options, runtime flags). See astro deploy page not found for the most common 404 fallout from this.
Step 5: Verify the build output matches what the platform expects
npm run build
ls dist/
Expected per target:
- Cloudflare Pages,
mode: 'directory':dist/_worker.js/directory +dist/_routes.json. - Cloudflare Pages,
mode: 'advanced':dist/_worker.jssingle file. - Vercel Serverless:
dist/.vercel/output/functions/anddist/.vercel/output/static/. - All-static:
dist/index.htmland friends, NO_worker.js.
If your dist/ does not look like the expected layout, the adapter is misconfigured.
Step 6: Set platform-side runtime compatibility flags
Cloudflare Pages → Settings → Functions → Compatibility flags:
nodejs_compat
For Vercel Node adapter, in adapter options:
vercel({
runtime: "nodejs20.x",
webAnalytics: { enabled: false },
});
Without these, the deployed worker / function cannot import Node built-ins your code (or transitive deps) expect.
Step 7: Deploy a minimal probe route to confirm the model end-to-end
Create a single dynamic route:
// 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. If you get JSON, SSR routing works. If 404, the adapter wiring is still wrong — go back to step 5.
Verify
npm run buildreports a non-zero count of SSR routes when SSR is expected.dist/layout matches the deploy target’s expected layout exactly.- The probe route returns live JSON in production.
- All previously-404 routes resolve correctly.
- No
Astro.request is not availablewarnings in build log.
Long-term prevention
- Pin the adapter version explicitly in
package.jsonand bump it in lockstep with Astro major versions. - Add a CI check that grep-counts
prerenderdirectives and fails if they contradict the globaloutput. - Document the deploy model in
README.mdso future contributors do not flip flags casually. - Keep one probe route deployed at all times so a regression is visible the moment it happens.
- For Cloudflare Pages, always set compatibility flags in
wrangler.tomlchecked into the repo, not just in the dashboard. - Avoid
output: 'hybrid'on Astro 5+; it is deprecated and the migration is mechanical.
Common pitfalls
- Installing the adapter package but forgetting
adapter: cloudflare()in the config — Astro builds as static and silently drops SSR routes. - Setting
mode: 'directory'on Cloudflare with an Astro version that only supportsmode: 'advanced'— the build produces a_worker.jsfile Cloudflare cannot route. - Pasting
export const prerender = falseinto a layout file (not a page) — has no effect, leads to false sense of fix. - Assuming
output: 'static'+Astro.cookiesworks because dev shows it working — dev is always SSR; production static drops the API. See static site blank page for the symptom. - Deploying to Vercel with the Cloudflare adapter “because they both serve at the edge” — the output layouts are completely different.
FAQ
Q: I used output: 'hybrid' on Astro 5 and it builds. Is it fine?
It works for now as an alias to static mode with per-route prerender opt-outs. But it is officially deprecated; future minors will remove it. Migrate to output: 'static' + explicit prerender = false on SSR routes.
Q: Can I switch adapters without changing my code?
Mostly yes for Astro.request / Astro.cookies usage. Platform-specific APIs (context.locals.runtime.env on Cloudflare vs. process.env on Node) do require code changes. The adapter docs list these.
Q: Build succeeds but every route 404s in production. Where to start?
Check dist/ layout first. If it does not match what the platform expects, your adapter mode 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 in SSR via Vite, 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