
How to mix prerendered and on-demand pages in Astro?
To mix prerendered and on-demand pages in Astro, install a server adapter and then control each route with the prerender export. In the default static output, every page is built to HTML at build time, and you add export const prerender = false to the pages or endpoints that must run on the server for each request. If most of your site is dynamic, set output: 'server' instead, which flips the default, and add export const prerender = true to the pages that can stay static. There is no separate "hybrid" mode to switch on: an Astro project with an adapter is hybrid as soon as it has at least one route of each kind.
This is one of the most useful things about Astro. A marketing site, blog and documentation can be plain HTML served from a CDN, while a dashboard, a search endpoint or a form handler on the same domain runs on a server. In this article you'll learn how the two defaults work, how to opt individual routes in and out, how to set prerendering for many routes at once with an integration hook, how to check what your build actually produced, and which mistakes cause the most confusion.
How Astro Decides How a Route Is Rendered
Every page and endpoint in src/pages/ ends up in one of two buckets:
- Prerendered: rendered once during
astro buildand written to disk as an HTML file (or a static file for endpoints). The same output is served to every visitor. - On-demand rendered: compiled into server code that runs for each request. It can read cookies, request headers, the query string and the request body, and it can return a different response to each visitor.
Which bucket a route falls into depends on two things: the project-wide output setting and an optional prerender export in the route file. The route's own export always wins.
output setting | Default for routes | How to override one route |
|---|---|---|
'static' (the default) | Prerendered | export const prerender = false |
'server' | On-demand rendered | export const prerender = true |
The Astro documentation describes output: 'server' as not adding any extra functionality. It only changes the default. Both modes can produce exactly the same site; you simply pick the one that means writing fewer prerender lines. The difference between the two rendering styles is covered in more depth in static vs on-demand rendering.
Step 1: Add an Adapter
On-demand rendering needs a runtime to run on, and Astro reaches that runtime through an adapter. Official adapters exist for Node.js, Vercel, Netlify and Cloudflare. Add one with astro add, which installs the package and edits your config in one step:
npx astro add node
For the Node adapter, the resulting config looks like this:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
adapter: node({
mode: 'standalone',
}),
});
Notice there's no output key. The project is still in the default static mode, so every page is still prerendered. The adapter only makes on-demand rendering available. Choosing between adapters is a topic of its own, covered in Astro adapters and which one to choose.
If you skip this step and mark a page with prerender = false, the build stops with a clear error:
[NoAdapterInstalled] Cannot use server-rendered pages without an adapter. Please install and configure the appropriate server adapter for your final deployment.
Step 2: Opt Individual Routes Into On-Demand Rendering
With the default static output, add export const prerender = false to the frontmatter of any page that needs request-time data. This dashboard reads a cookie, which is only possible on the server at request time:
---
// src/pages/dashboard.astro
export const prerender = false;
const user = Astro.cookies.get('user')?.value ?? 'guest';
const renderedAt = new Date().toISOString();
---
<html lang="en">
<head><title>Dashboard</title></head>
<body>
<h1>Hello, {user}</h1>
<p>Rendered on demand at {renderedAt}.</p>
</body>
</html>
Endpoints work the same way. In a .ts or .js file, export the constant at module level:
// src/pages/api/time.ts
import type { APIRoute } from 'astro';
export const prerender = false;
export const GET: APIRoute = () => {
return Response.json({ now: new Date().toISOString() });
};
Everything else stays static. A normal page like this one is rendered once at build time, so the timestamp is frozen:
---
// src/pages/about.astro
const builtAt = new Date().toISOString();
---
<html lang="en">
<head><title>About</title></head>
<body>
<h1>About us</h1>
<p>This page was built at {builtAt}.</p>
</body>
</html>
Dynamic routes that use getStaticPaths() also stay static by default, so a src/pages/blog/[slug].astro file generates one HTML file per path it returns. For more on that pattern, see dynamic routes with getStaticPaths.
Step 3 (Alternative): Make Server Rendering the Default
If almost every route in your project is personalised, such as an app behind a login, writing prerender = false everywhere is tedious. Set output: 'server' and do the opposite:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
});
Now every route renders on demand unless it says otherwise. Pages that never change, such as a privacy policy or an about page, can opt back into prerendering:
---
// src/pages/about.astro
export const prerender = true;
const builtAt = new Date().toISOString();
---
<html lang="en">
<head><title>About</title></head>
<body>
<h1>About us</h1>
<p>This page was built at {builtAt}.</p>
</body>
</html>
The Astro docs recommend starting with the default static mode until you're sure that most or all of your pages need a server. Prerendered pages are served as files, so they're fast and cheap, and they keep working even if your server function has a problem.
What Happens to getStaticPaths() in Server Mode
In server mode, a dynamic route without prerender = true is rendered per request, so getStaticPaths() has nothing to do. Astro ignores it and tells you so during the build:
[WARN] [router] getStaticPaths() ignored in dynamic page /src/pages/blog/[slug].astro. Add `export const prerender = true;` to prerender the page as static HTML during the build process.
If you want those blog posts as static files, add export const prerender = true to the route. If you want them rendered on demand, remove getStaticPaths() and look the post up from Astro.params instead, returning a 404 response when it doesn't exist.
Setting Prerendering for Many Routes at Once
Sometimes you want a rule rather than a line in every file, for example "everything under src/pages/api/ runs on the server". Astro's integration API has an astro:route:setup hook that runs for each route before bundling and lets you set route.prerender. You can write a tiny inline integration in your config:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
function apiOnDemand() {
return {
name: 'api-on-demand',
hooks: {
'astro:route:setup': ({ route }) => {
if (route.component.startsWith('src/pages/api/')) {
route.prerender = false;
}
},
},
};
}
export default defineConfig({
adapter: node({ mode: 'standalone' }),
integrations: [apiOnDemand()],
});
With this in place, the prerender export can be removed from src/pages/api/time.ts and the endpoint is still built as server code. route.component is the path of the route file, relative to the project root. If a file has its own export const prerender, that value is the starting point the hook sees, and if every hook leaves the value undefined, the route falls back to the default for your output mode.
Checking What Your Build Produced
The build log tells you exactly which routes were prerendered. With the dashboard and endpoint set to on-demand, and two blog posts from getStaticPaths(), the relevant part of astro build looks like this:
[build] output: "static"
[build] mode: "server"
[build] adapter: @astrojs/node
prerendering static routes
├─ /about/index.html (+7ms)
├─ /blog/hello/index.html (+1ms)
├─ /blog/world/index.html (+1ms)
├─ /index.html (+10ms)
✓ Completed in 29ms.
Two lines are worth reading carefully. output: "static" is your config setting, while mode: "server" means Astro detected at least one on-demand route and built a server. Routes missing from the "prerendering static routes" list, here /dashboard and /api/time, are the on-demand ones.
On disk, the Node adapter splits the output into two folders:
dist/client/about/index.html
dist/client/blog/hello/index.html
dist/client/blog/world/index.html
dist/client/index.html
dist/server/entry.mjs
dist/server/chunks/dashboard_mnysE5wV.mjs
dist/server/chunks/time_C_zo6Q6k.mjs
Prerendered HTML goes into dist/client/, and on-demand routes become chunks under dist/server/. Other adapters use their own folder layout, but the split is the same idea.
You can confirm the behaviour by running the standalone server and requesting each route:
PORT=4400 node dist/server/entry.mjs
curl -s localhost:4400/dashboard -H 'Cookie: user=Sajjad'
curl -s localhost:4400/api/time
<h1>Hello, Sajjad</h1><p>Rendered on demand at 2026-10-24T05:01:34.823Z.</p>
{"now":"2026-10-24T05:01:34.842Z"}
Run the requests again and both timestamps change, while /about/ keeps showing the time of the build.
Choosing Which Pages to Prerender
A simple rule: prerender anything that is the same for every visitor and can wait for the next deploy to change. Render on demand anything that depends on the request.
- Good candidates for prerendering: home and landing pages, blog posts, documentation, legal pages, RSS feeds, sitemaps and any endpoint that serves data fixed at build time.
- Good candidates for on-demand rendering: account pages, dashboards, pages that read cookies or sessions, search results that use the query string, form handlers that accept
POSTrequests, and pages showing data that changes too often to rebuild for. - In between: a product page whose price changes hourly can be rendered on demand and then cached by your host or CDN, which gives you fresh data without paying the full rendering cost on every request.
If a page is mostly static with one small personalised part, such as an avatar in the header, you don't have to make the whole page dynamic. Server islands let you keep the page prerendered and defer just that component to the server with server:defer. They also need an adapter.
Common Mistakes
Reading Request Data on a Prerendered Page
A prerendered page runs once at build time, when there is no real visitor. Request headers and the query string don't exist at that point. Astro warns you instead of failing:
[WARN] `Astro.request.headers` was used when rendering the route `src/pages/static-headers.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.
The page still builds, but the values are empty. The same goes for Astro.url.searchParams, which has no query string to read at build time. If you see this warning, the fix is to render the route on demand.
Expecting Middleware to Run on Every Request
Middleware runs for prerendered pages too, but only once, during the build. Whatever it does to a static page happens at that moment and isn't repeated when a visitor arrives, so a cookie check in middleware can't protect a prerendered page. If you need request-time logic, such as an authentication check, on a route, that route must be on-demand rendered. Some adapters can run middleware at the edge in front of static files; check your adapter's documentation. Middleware itself is covered in What is Astro middleware.
Using a Non-Literal prerender Value
Write the prerender export as a literal true or false. Astro reads it from the file before running your code, and a value computed from a variable doesn't work: in a quick test, export const prerender = flag (with flag defined just above it) broke the build with ReferenceError: flag is not defined. If you need the decision to depend on something else, such as an environment variable or a folder name, use the astro:route:setup hook shown earlier, which runs in your config where you have full access to Node.js.
Setting Response Headers From a Component
On on-demand pages, Astro streams HTML to the browser as each component renders. Headers are sent before your components run, so Astro.response.headers and Astro.cookies.set() only work in the page's own frontmatter, not inside a layout or child component.
FAQ: Mixing Prerendered and On-Demand Pages
No. Older versions of Astro had an output: 'hybrid' option, but it was merged into the default static mode. Today you get the same behaviour by adding an adapter and setting export const prerender = false on the routes that need a server.
No. A fully static Astro site needs no adapter and can be hosted anywhere that serves files. You need an adapter as soon as one route uses on-demand rendering, or when you use features such as server islands or host-specific services like an image CDN.
Keep the default static output if most pages are the same for every visitor, and opt the few dynamic routes out of prerendering. Switch to output: 'server' when most routes are dynamic, then mark the static ones with export const prerender = true. Both modes produce the same kind of build.
Read the prerendering static routes section of the astro build log. Every route listed there became a static file, and any route missing from it is rendered on demand. With the Node adapter, prerendered HTML is in dist/client and on-demand routes are in dist/server.
Yes. Endpoints follow exactly the same rules as pages. In output: 'server' mode, add export const prerender = true to an endpoint such as an RSS feed and it will be written to a static file at build time.
Because the route is rendered on demand, usually after switching to output: 'server'. Add export const prerender = true to the route to build its paths statically, or remove getStaticPaths() and read the route parameters at request time instead.
Conclusion
Mixing static and on-demand pages in Astro comes down to one adapter and one export. Add an adapter so a server runtime is available, keep the default static output, and mark the routes that need request data with export const prerender = false. If your project is mostly dynamic, set output: 'server' and mark the static routes with prerender = true instead. For rules that cover whole folders, the astro:route:setup hook sets prerender from your config.
Once it's set up, check the build log to see which routes were prerendered, and watch for warnings about request headers or ignored getStaticPaths() calls, which almost always mean a route is in the wrong bucket. Keep as much as possible prerendered, render on demand only what truly depends on the request, and reach for server islands when only a small part of a page needs to be dynamic.


