Type something to search...
How to enable server-side rendering in Astro?

How to enable server-side rendering in Astro?

To enable server-side rendering in Astro, install an adapter for the runtime you deploy to, for example with npx astro add node, npx astro add vercel, npx astro add netlify or npx astro add cloudflare. Then choose which pages render on demand: add export const prerender = false to individual pages and endpoints, or set output: 'server' in astro.config.mjs to render every page on demand by default. Those pages can then read cookies and headers, set status codes, redirect and show live data on every request, while the rest of your site can stay static.

Astro calls this on-demand rendering, and the docs use it interchangeably with server-side rendering (SSR). In this article you'll go through the setup step by step: adding an adapter, opting pages in, the 'server' output mode, the features you can use once a page renders on demand, building and running the server, deploying to a host, and the security and performance details to know about. Every example was built and tested with curl using Astro 7.4.2 and @astrojs/node 11.1.9. If you're still deciding whether you need SSR at all, start with the post on static vs on-demand rendering.

Step 1: Add an Adapter

An adapter turns your on-demand routes into code that a specific runtime can execute. Astro maintains official adapters for Node.js, Netlify, Vercel and Cloudflare, and community adapters cover other platforms.

The astro add command installs the package and updates your config in one step:

npx astro add node

For Node, the resulting config is:

// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";

export default defineConfig({
  adapter: node({
    mode: "standalone",
  }),
});

mode: "standalone" builds a complete HTTP server that also serves your static files. The alternative, "middleware", exports a handler you can mount in Express, Fastify or another Node server.

If you skip this step and mark a page as on-demand, the build stops with:

[NoAdapterInstalled] Cannot use server-rendered pages without an adapter. Please install and configure the appropriate server adapter for your final deployment.

Choosing between adapters is covered in the post on Astro adapters.

Step 2: Opt Pages In to On-Demand Rendering

By default, every page is still prerendered after you add an adapter. To render a page per request, export prerender = false from its frontmatter:

---
// src/pages/now.astro
export const prerender = false;

const now = new Date().toISOString();
const userAgent = Astro.request.headers.get("user-agent") ?? "unknown";
---
<h1>Rendered on demand</h1>
<p>Time: {now}</p>
<p>User agent: {userAgent}</p>

Two requests a second apart returned different times, and the user agent reflected the request:

<h1>Rendered on demand</h1><p>Time: 2026-10-24T09:45:19.816Z</p><p>User agent: curl-test</p>
<h1>Rendered on demand</h1><p>Time: 2026-10-24T09:45:20.866Z</p><p>User agent: curl-test</p>

Endpoints work the same way:

// src/pages/api/random.ts
import type { APIRoute } from "astro";

export const prerender = false;

export const GET: APIRoute = () => {
  return Response.json({ number: Math.random() });
};
{"number":0.4731785053079889}

This per-page approach is best when most of your site is content and only a few routes need a server.

Step 3 (Optional): Make On-Demand the Default

For an application where most pages depend on the visitor, flip the default with output: 'server':

// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";

export default defineConfig({
  output: "server",
  adapter: node({
    mode: "standalone",
  }),
});

Now every page renders on demand without needing prerender = false. Pages that don't need a server can opt back in to prerendering:

---
// src/pages/about.astro
export const prerender = true;
---
<h1>About</h1>

With this config, the build log showed only that page being prerendered:

[build] output: "server"
[build] mode: "server"
  ├─ /about/index.html (+5ms)

output: 'server' doesn't unlock extra features. It only changes the default for pages that don't export prerender. Mixing the two modes deliberately is covered in the post on mixing prerendered and on-demand pages.

What You Can Do in an On-Demand Page

Once a page renders per request, it has access to the request and control over the response. These features don't work on prerendered pages.

Read the URL, Query String and Headers

---
// src/pages/search.astro
const query = Astro.url.searchParams.get("q")?.trim() ?? "";
const posts = ["Scoped CSS", "Global styles", "Dark mode", "Custom fonts"];
const results = query
  ? posts.filter((title) => title.toLowerCase().includes(query.toLowerCase()))
  : [];
---
<h1>Search</h1>
<form method="get">
  <input type="search" name="q" value={query} />
  <button>Search</button>
</form>
{query && <p>{results.length} result(s) for "{query}"</p>}
<ul>{results.map((title) => <li>{title}</li>)}</ul>

Requesting /search?q=css returned <p>1 result(s) for "css"</p><ul><li>Scoped CSS</li></ul>. On a prerendered page the query string isn't known at build time, so a page like this must render on demand. This example runs in output: 'server' mode, so it doesn't need a prerender export.

Reading Astro.request.headers in a prerendered page doesn't fail the build, but Astro warns you, which is a useful hint that the page should be on demand:

[WARN] `Astro.request.headers` was used when rendering the route `src/pages/static-headers.astro'`. `Astro.request.headers` is not available on prerendered pages.

Read and Set Cookies

---
// src/pages/visits.astro
export const prerender = false;

const previous = Astro.cookies.get("visits")?.number() ?? 0;
const visits = Number.isNaN(previous) ? 1 : previous + 1;

Astro.cookies.set("visits", String(visits), {
  path: "/",
  httpOnly: true,
  sameSite: "lax",
  maxAge: 60 * 60 * 24 * 30,
});
---
<h1>You have visited {visits} time{visits === 1 ? "" : "s"}</h1>

The first response included the cookie header, and a second request with the cookie jar counted up:

set-cookie: visits=1; Max-Age=2592000; Path=/; HttpOnly; SameSite=Lax
<h1>You have visited 1 time</h1>
<h1>You have visited 2 times</h1>

The full cookie API is covered in the post on cookies in Astro.

Set Status Codes, Headers and Redirects

---
// src/pages/product/[id].astro
export const prerender = false;

const products: Record<string, { name: string; available: boolean }> = {
  "1": { name: "Desk lamp", available: true },
  "2": { name: "Old chair", available: false },
};

const product = products[Astro.params.id ?? ""];

if (!product) {
  return new Response("Not found", { status: 404 });
}

if (!product.available) {
  return Astro.redirect("/products", 301);
}

Astro.response.headers.set("Cache-Control", "public, max-age=60");
---
<h1>{product.name}</h1>

/product/1 returned 200 OK with cache-control: public, max-age=60, /product/2 returned 301 Moved Permanently with location: /products, and /product/9 returned 404 Not Found. Notice there's no getStaticPaths(): an on-demand dynamic route reads Astro.params directly at request time.

One rule applies to all of these: response headers and status can only be changed in the page itself, not inside components or layouts. Astro streams the HTML, so by the time a component renders, the headers have already been sent.

Handle Form Submissions in an Endpoint

// src/pages/api/subscribe.ts
import type { APIRoute } from "astro";

export const POST: APIRoute = async ({ request }) => {
  const data = await request.formData();
  const email = data.get("email");

  if (typeof email !== "string" || !email.includes("@")) {
    return Response.json({ error: "A valid email is required" }, { status: 400 });
  }

  return Response.json({ ok: true, email });
};
curl -X POST -d "email=me@example.com" http://localhost:4321/api/subscribe
{"ok":true,"email":"me@example.com"}

An invalid value returned 400 Bad Request with the error message. More patterns are in the post on API endpoints in Astro; for forms with validation built in, Astro Actions are worth a look too.

Step 4: Build and Run the Server

Development works exactly as before: npm run dev renders on-demand pages on every request. To run a production build with the Node adapter:

npx astro build
node ./dist/server/entry.mjs
[@astrojs/node] Server listening on http://localhost:4321

The build output is split into dist/client/ (prerendered HTML and static assets) and dist/server/ (the server entry and route chunks). In standalone mode the server serves both. You can set the host and port with environment variables at runtime:

HOST=0.0.0.0 PORT=8080 node ./dist/server/entry.mjs

HOST=0.0.0.0 is what you want inside a Docker container, so the server accepts connections from outside it. npx astro preview also works with the Node adapter if you just want a quick local check of the build.

Step 5: Deploy to Your Host

For serverless and edge hosts, you don't run entry.mjs yourself. The adapter writes output in the format the platform expects:

HostCommandWhat the adapter produces
Any Node.js server or containernpx astro add nodeA standalone server or middleware handler
Vercelnpx astro add vercelVercel functions plus static output
Netlifynpx astro add netlifyNetlify functions plus static output
Cloudflarenpx astro add cloudflareA Cloudflare Workers project

Connect the repository to the platform, keep its build command as astro build, and the adapter handles the rest. Each adapter has its own options and platform-specific details; for example, see the guide to deploying Astro to Vercel.

Security and Performance Notes

CSRF Protection Is On by Default

Astro's security.checkOrigin option defaults to true. For on-demand routes, it checks form-style POST, PUT, PATCH and DELETE requests against the browser's Sec-Fetch-Site header, falling back to Origin, and rejects cross-site requests. Sending the same form post with Origin: https://evil.example returned:

HTTP/1.1 403 Forbidden

Same-origin form posts from your own pages are unaffected.

Keep Secrets on the Server

Code in the frontmatter of an on-demand page runs on the server, so it can read secrets such as database credentials. Only what you render into the HTML reaches the browser. Use Astro's typed environment variables for secrets rather than hard-coding them.

Make Responses Fast

  • Prerender what you can: static pages are served without running any code.
  • Use streaming to your advantage: Astro streams on-demand pages, so put slow data fetching in components lower down the page rather than awaiting everything in the page frontmatter.
  • Cache: set Cache-Control headers for CDN caching, or use Astro 7's stable cache config and routeRules with Astro.cache, which work with an in-memory provider or CDN providers from the Netlify, Vercel and Cloudflare adapters.
  • Consider server islands: if only a small part of a page is dynamic, keep the page static and render just that component on demand with server:defer.

Shared logic for every on-demand request, such as authentication checks, belongs in middleware, covered in the post on Astro middleware.


FAQ: Server-Side Rendering in Astro

Yes. The Astro docs use on-demand rendering as the main term and describe it as also being known as server-side rendering. Both mean the HTML is generated per request by a server.

It was removed in Astro 5. The default static output now supports on-demand pages directly: add an adapter and export prerender = false from the pages that need a server.

No. With an adapter installed, any page or endpoint can render on demand by exporting prerender = false. Setting output to server only makes on-demand the default for all pages.

At least one route is set to render on demand, but no adapter is configured. Run npx astro add with your host's adapter, or remove prerender = false if the page can be static.

No. Headers and status codes can only be changed in the page or endpoint, because Astro has already started streaming the response by the time components and layouts render.

Yes. The dev server renders on-demand pages per request, so cookies, headers and redirects behave as they will in production. You still need the adapter configured for builds.


Conclusion

Enabling server-side rendering in Astro takes two decisions: which adapter matches your host, and which pages should render on demand. npx astro add installs and configures the adapter, export const prerender = false opts individual routes in, and output: 'server' flips the default when most of your site is dynamic.

From there, on-demand pages can read the URL, headers and cookies, return custom status codes and redirects, and handle form posts in endpoints, with CSRF protection already switched on. Keep the pages that don't need a server prerendered, cache what you can, and SSR becomes a targeted tool for the parts of your site that genuinely depend on the request.

Tags :
Share :

Related Posts

What are Astro Actions and how do they work?

What are Astro Actions and how do they work?

Astro Actions are type-safe backend functions that you define once on the server and then call from the browser as if they were ordinary async fu

Dive Deeper
What are Astro adapters and which one should you choose?

What are Astro adapters and which one should you choose?

An Astro adapter is an integration that builds the server part of your project for a specific runtime, such as Node.js, Vercel Functions, Netlify

Dive Deeper
How to build API endpoints in Astro?

How to build API endpoints in Astro?

To build an API endpoint in Astro, create a .js or .ts file in src/pages/ and export a function named after an HTTP method, such as GET or

Dive Deeper