Rendering & Output Modes
Two ways to produce a page’s HTML
Section titled “Two ways to produce a page’s HTML”Astro can render a route’s HTML at two different times, and this choice is central to how you deploy:
- Static (SSG) — the HTML is built once, at build time, and served as a plain file. Fast, cacheable, deployable to any static host or CDN. Perfect for content that’s the same for everyone.
- On-demand (SSR) — the HTML is built per request, on a server, so it can use request-time data (the logged-in user, cookies, query params, live data). Requires a runtime, which means an adapter.
flowchart TB
subgraph static["Static (SSG)"]
b["build time"] --> f["HTML file"] --> cdn["served from CDN"]
end
subgraph ssr["On-demand (SSR)"]
r["each request"] --> srv["server renders HTML"] --> resp["response"]
endThe default is static. You opt a project (or a single route) into on-demand rendering when a page genuinely needs per-request data.
Per-route control with prerender
Section titled “Per-route control with prerender”You don’t choose one mode for the whole site. In a server-enabled project (output: 'static' with an adapter configured, or output: 'server'), each route decides for itself with export const prerender:
---// A mostly-static site with one dynamic route:export const prerender = false; // render THIS page on demand, per requestconst user = await getUser(Astro.request); // needs the incoming request---<p>Welcome, {user.name}</p>export const prerender = true→ build this route to a static file (even in a server project).export const prerender = false→ render this route on demand (even in a mostly-static project).
This is the powerful middle ground: a site that is 95% static files with a handful of on-demand routes (a dashboard, a form handler), each rendered only when needed. Astro 4.x called this combination hybrid output; today it’s just what 'static' does when at least one route opts out with prerender = false (see the callout above).
Proof: which routes actually get prerendered
Section titled “Proof: which routes actually get prerendered”A real astro build prints exactly which routes it prerendered to static files — anything not in that list, in a server-enabled project, is being rendered on demand instead. Three routes: one explicitly static, one explicitly on-demand, one default:
---export const prerender = true;---<html><body><h1>Static about page</h1></body></html>---export const prerender = false;const cookieHeader = Astro.request.headers.get('cookie');---<html><body><h1>Dashboard for request with cookie: {cookieHeader}</h1></body></html>$ npx astro build --outDir dist prerendering static routes ├─ /mixed/static-about/index.html (+13ms) ├─ /index.html (+1ms)✓ Completed in 27ms.
[build] Rearranging server assets...[build] Server built in 467ms[build] Complete!/mixed/static-about/index.html is in the “prerendering static routes” list — a real HTML file written to dist/. /mixed/dashboard never appears there at all: it was compiled into the server entry instead, to be rendered fresh on every request. The build log itself is the proof of which mode each route actually got, not just what the source file declares.
Adapters: how on-demand runs
Section titled “Adapters: how on-demand runs”Static output needs nothing but a file host. On-demand rendering needs a runtime to execute your server code per request — and that’s where an adapter comes in. You install the adapter for your target platform and set it in the config:
// astro.config.mjs — enabling on-demand rendering on Cloudflareimport { defineConfig } from 'astro/config';import cloudflare from '@astrojs/cloudflare';
export default defineConfig({ adapter: cloudflare(), // or node(), vercel(), netlify()...});The adapter translates Astro’s server rendering into the platform’s format (a Cloudflare Worker, a Node server, a Vercel function). Without an adapter, on-demand features (SSR routes, server islands, Actions endpoints) can’t run — that’s a common first-deploy error: “an adapter is required.” The SSR & adapters lesson covers choosing and configuring one in depth.
What the build produces
Section titled “What the build produces”astro build runs your component scripts, renders the HTML, bundles the CSS and any island JavaScript, and writes the output to dist/. For a static site that’s a folder of .html files and assets you can drop on any CDN. For a server-enabled site, dist/ also contains the server entry the adapter runs. Either way, the browser receives finished HTML plus only the island JS — the foundational promise, delivered by the build.
What Astro.request/cookies/session do on a prerendered page
Section titled “What Astro.request/cookies/session do on a prerendered page”A prerendered route runs once, at build time — there is no live incoming request to read anything request-specific from. Astro.cookies, Astro.session, and reading Astro.request.headers all depend on a real request, so using any of them on a page still set to prerender = true (or in a 'static' project with no per-route override) produces a real, provable warning at build time — not a silent no-op:
---export const prerender = true;const views = await Astro.session?.get('views');---<html><body><p>{views}</p></body></html>$ npx astro build --outDir dist[WARN] `Astro.request.headers` was used when rendering the route `src/pages/mixed/static-session.astro'`. `Astro.request.headers` is not available on prerendered pages. If you need access to request headers, make sure that the page is server-rendered using `export const prerender = false;` or by setting `output` to `"server"` in your Astro config to make all your pages server-rendered by default.That’s a real build warning, reproduced against Astro.session specifically (both Astro.session and Astro.cookies read the request’s headers under the hood, so both surface the identical warning). The value itself comes back undefined at build time — there’s no request to have a session or a cookie. The fix is exactly what the warning says: export const prerender = false on that one route, moving it into the on-demand set proven above.
Reading AI-generated code
Section titled “Reading AI-generated code”A generated snippet about rendering modes is worth checking against what’s actually still true in Astro 7:
- Does it set
output: 'hybrid'? That’s a real config error on 7.3.5 — removed in v5, proven above with the exact upgrade-guide text.'static'(the default) already covers it. - Does a page read
Astro.cookies,Astro.session, orAstro.request.headerswith noexport const prerender = falseanywhere near it, and no adapter/output: 'server'in the config? Proven above: that’s a real build-time warning (“not available on prerendered pages”), and the value comes backundefined— not the request-time data the snippet assumes it will get. - Does it claim a route “becomes on-demand automatically” because it imports something request-related? Rendering mode is controlled only by
export const prerender(or the project-wideoutput) — importingAstro.cookiesdoesn’t flip the mode, it just warns (and returns nothing useful) if the mode wasn’t already on-demand. - Does it treat “static” and “on-demand” as an all-or-nothing site-wide choice? Proven above: a single project can mix both, decided per route.
🐛 Spot the bug
---const user = Astro.cookies.get('session_id');// no export const prerender anywhere in this file---<h1>Settings for {user?.value}</h1>Show the answer
With no export const prerender = false in this file, and nothing in the project config forcing output: 'server', this route prerenders by default — a build-time-only render with no request to read a cookie from. It’s exactly the Astro.request.headers-on-a-prerendered-page warning proven above (Astro.cookies.get reads request headers internally), and user comes back undefined for every visitor, at build time, once, forever — not per-visitor as the code implies. The fix: add export const prerender = false to make this route render per-request, so Astro.cookies has a real request to read from.
Mental Model
Section titled “Mental Model”A route’s rendering mode answers one question: “does this page’s HTML depend on who’s asking, or is it the same file for everyone?” If it’s the same for everyone, prerender it once and serve a file forever — that’s what the default does. The moment a page needs to know about the specific incoming request (a cookie, a session, a query param that changes the answer), it needs to run during that request, not once beforehand — that’s prerender = false. The proofs above show both directions of the mistake: prerendering something that needed the request (silent undefined, a warning) and not realizing static output can still carry a handful of on-demand routes right alongside it.
Trade-offs
Section titled “Trade-offs”Prerendered (prerender = true / static default) |
On-demand (prerender = false) |
|
|---|---|---|
| When it runs | Once, at build time | Every request |
Can read Astro.cookies/Astro.session/request headers |
No — warns and returns nothing, proven above | Yes |
| Hosting | Any static host/CDN | Needs an adapter + runtime |
| Latency | Fastest — a file | Depends on server/edge cold start, DB calls, etc. |
| Best for | Content that’s the same for every visitor | Per-user or per-request data |
Common Misconceptions
Section titled “Common Misconceptions”- “
output: 'hybrid'is still how you mix static and dynamic routes.” It was removed in Astro v5 — proven above with the exact upgrade-guide quote.'static'plus per-routeprerender = falseis the current (and only) way. - “Reading
Astro.cookieson a static page just returnsundefinedquietly.” Proven above: it also emits a real build-time warning naming the exact route and explaining the fix — it’s loud, not silent, if you’re looking at build output. - “A project is either a static site or a server-rendered site, not both.” Proven above with a real build log: one project produced both a prerendered
.htmlfile and an on-demand route compiled into the server entry, side by side. - “Setting
prerender = falseis the only way an adapter gets involved.” Server islands and Actions also require an adapter even on an otherwise fully static (prerender = trueeverywhere) site.