Astro Adapter Mismatch Between SSR and SSG Modes

Astro deploy fails or pages render blank because the adapter expects SSR but config says static, or vice versa — diagnose by aligning output, adapter, and route prerender flags.

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.mjs output value and any adapter: line.
  • Check whether ANY page in src/pages/ has export const prerender = ....
  • Confirm which deploy target you actually want (static-only, full SSR, or hybrid per-route).

Information to collect

  • Full astro.config.mjs content.
  • 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, no prerender = false anywhere. Deploys as plain HTML.
  • All-SSR: output: 'server', adapter installed and wired, optional prerender = true on truly static pages.
  • Mixed (Astro 5+): output: 'static' globally, export const prerender = false on 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.js single file.
  • Vercel Serverless: dist/.vercel/output/functions/ and dist/.vercel/output/static/.
  • All-static: dist/index.html and 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 build reports 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 available warnings in build log.

Long-term prevention

  • Pin the adapter version explicitly in package.json and bump it in lockstep with Astro major versions.
  • Add a CI check that grep-counts prerender directives and fails if they contradict the global output.
  • Document the deploy model in README.md so 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.toml checked 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 supports mode: 'advanced' — the build produces a _worker.js file Cloudflare cannot route.
  • Pasting export const prerender = false into a layout file (not a page) — has no effect, leads to false sense of fix.
  • Assuming output: 'static' + Astro.cookies works 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.

Tags: #Troubleshooting #Astro #adapter #ssr #ssg