Astro SSR
Server-side rendering in Astro with adapters for Node, Vercel, Netlify, Cloudflare, and Deno
You are an expert in Astro server-side rendering, output modes, and adapter configuration for dynamic websites.
## Key Points
- Use hybrid rendering: pre-render as many pages as possible and server-render only pages that truly need request-time data (auth, personalization, real-time data).
- Add `export const prerender = true` to pages like marketing pages, docs, and blog posts that do not depend on request context.
- Use `Astro.cookies` instead of manually parsing the `Cookie` header. The helper handles serialization and provides a cleaner API.
- Return proper HTTP status codes. Use `Astro.redirect()` for redirects and `new Response(null, { status: 404 })` for not-found pages.
- Choose the `standalone` adapter mode for Docker deployments and `middleware` mode when integrating Astro into an existing Express or Fastify server.
- **Forgetting to install an adapter**: SSR mode without an adapter causes a build error. Every server-rendered Astro project needs an adapter for the target platform.
- **Using `Astro.request` in static pages**: Request data like headers, cookies, and client address are only available in server-rendered pages. Accessing them in a pre-rendered page throws an error.
- **Cold start latency**: Serverless adapters (Vercel, Netlify, Cloudflare) may have cold start delays. Keep SSR pages lightweight and avoid heavy initialization logic at module scope.skilldb get astro-skills/Astro SSRFull skill: 359 linesServer-Side Rendering — Astro
You are an expert in Astro server-side rendering, output modes, and adapter configuration for dynamic websites.
Overview
Astro supports three output modes: static (default), server, and hybrid. SSR enables dynamic pages that render on each request, giving you access to request headers, cookies, authentication, and real-time data without rebuilding the entire site.
Core Concepts
Output Modes
Configure the output mode in astro.config.mjs:
import { defineConfig } from 'astro/config';
// Static (default) — all pages pre-rendered at build time
export default defineConfig({
output: 'static',
});
// Server — all pages rendered on demand by default
export default defineConfig({
output: 'server',
});
Hybrid Rendering
In server mode, you can opt individual pages into pre-rendering:
---
// This page is pre-rendered at build time, even in server mode
export const prerender = true;
---
<h1>This page is static</h1>
In static mode, you can opt individual pages into server rendering:
---
// This page is server-rendered, even in static mode
export const prerender = false;
---
<h1>This page is dynamic</h1>
Adapters
Adapters connect Astro's SSR output to a hosting platform's runtime:
# Node.js (self-hosted, Docker)
npx astro add node
# Vercel (serverless or edge)
npx astro add vercel
# Netlify (serverless or edge)
npx astro add netlify
# Cloudflare (Workers / Pages)
npx astro add cloudflare
# Deno
npx astro add deno
// astro.config.mjs — Node adapter example
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({
mode: 'standalone', // or 'middleware'
}),
});
The Astro Global in SSR
Server-rendered pages have access to the full Astro global with request information:
---
// Available in SSR pages and endpoints
const url = Astro.url; // Full URL object
const params = Astro.params; // Dynamic route params
const request = Astro.request; // Standard Request object
const headers = Astro.request.headers; // Request headers
const cookies = Astro.cookies; // Cookie helper
const clientAddress = Astro.clientAddress; // Client IP address
const locals = Astro.locals; // Data from middleware
---
Cookies
---
// Reading cookies
const sessionId = Astro.cookies.get('session')?.value;
// Setting cookies
Astro.cookies.set('theme', 'dark', {
path: '/',
maxAge: 60 * 60 * 24 * 365, // 1 year
httpOnly: true,
secure: true,
sameSite: 'lax',
});
// Deleting cookies
Astro.cookies.delete('session');
// Checking existence
if (Astro.cookies.has('session')) {
// handle authenticated user
}
---
Redirects and Status Codes
---
const user = await getUser(Astro.cookies.get('session')?.value);
if (!user) {
return Astro.redirect('/login', 302);
}
// Return a 404
if (!user.hasAccess) {
return new Response('Not found', { status: 404 });
}
---
<h1>Welcome, {user.name}</h1>
Implementation Patterns
Authentication Flow
---
// src/pages/dashboard.astro
const token = Astro.cookies.get('auth_token')?.value;
if (!token) {
return Astro.redirect('/login');
}
let user;
try {
const res = await fetch('https://api.example.com/me', {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error('Unauthorized');
user = await res.json();
} catch {
Astro.cookies.delete('auth_token');
return Astro.redirect('/login');
}
---
<h1>Dashboard</h1>
<p>Hello, {user.name}</p>
Form Handling
---
// src/pages/contact.astro
let message = '';
let errors: Record<string, string> = {};
if (Astro.request.method === 'POST') {
const formData = await Astro.request.formData();
const name = formData.get('name')?.toString() ?? '';
const email = formData.get('email')?.toString() ?? '';
const body = formData.get('message')?.toString() ?? '';
if (!name) errors.name = 'Name is required';
if (!email.includes('@')) errors.email = 'Valid email required';
if (!body) errors.message = 'Message is required';
if (Object.keys(errors).length === 0) {
await fetch('https://api.example.com/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, message: body }),
});
return Astro.redirect('/thank-you');
}
message = 'Please fix the errors below.';
}
---
<h1>Contact Us</h1>
{message && <p class="error">{message}</p>}
<form method="POST">
<label>
Name
<input name="name" type="text" />
{errors.name && <span class="error">{errors.name}</span>}
</label>
<label>
Email
<input name="email" type="email" />
{errors.email && <span class="error">{errors.email}</span>}
</label>
<label>
Message
<textarea name="message"></textarea>
{errors.message && <span class="error">{errors.message}</span>}
</label>
<button type="submit">Send</button>
</form>
Server-Side API Endpoints
// src/pages/api/users/[id].ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ params, request }) => {
const user = await db.users.findById(params.id);
if (!user) {
return new Response(JSON.stringify({ error: 'Not found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' },
});
}
return new Response(JSON.stringify(user), {
headers: { 'Content-Type': 'application/json' },
});
};
export const PUT: APIRoute = async ({ params, request }) => {
const body = await request.json();
const updated = await db.users.update(params.id, body);
return new Response(JSON.stringify(updated), {
headers: { 'Content-Type': 'application/json' },
});
};
export const DELETE: APIRoute = async ({ params }) => {
await db.users.delete(params.id);
return new Response(null, { status: 204 });
};
Node Adapter with Express Middleware
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'middleware' }),
});
// server.mjs
import express from 'express';
import { handler as ssrHandler } from './dist/server/entry.mjs';
const app = express();
// Custom Express middleware
app.use('/api/legacy', legacyApiRouter);
// Astro SSR handler
app.use(ssrHandler);
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
Response Streaming
Astro supports response streaming for faster time-to-first-byte:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
// Streaming is enabled by default in Astro
});
Core Philosophy
Server-side rendering in Astro is an intentional escalation, not a default mode. Astro's design treats pre-rendered static pages as the gold standard for performance and reliability, and SSR exists specifically for the cases where request-time data is genuinely necessary: authentication, personalization, real-time content, and user-specific responses. This inversion makes every SSR page a conscious architectural decision rather than an unexamined default.
The hybrid rendering model reflects Astro's pragmatic approach to performance trade-offs. Within a single project, marketing pages and blog posts can be pre-rendered for maximum speed while dashboards and account pages are server-rendered for freshness. The prerender export makes this page-by-page decision explicit in the code, so anyone reading a route file immediately understands its rendering strategy without consulting a configuration file.
Astro's adapter system cleanly separates the rendering model from the deployment target. Your SSR code is written against the standard Web Fetch API (Request, Response, Headers, URL), and the adapter translates this to the platform's native format. This means changing hosting providers requires swapping one adapter, not rewriting route logic. The portability is real, but you must still respect each platform's constraints around available APIs, cold start characteristics, and execution time limits.
Anti-Patterns
-
Defaulting every page to server rendering when most content is static. If your site is 90% blog posts and marketing pages, setting
output: 'server'globally wastes compute on pages that could be served from a CDN. Use hybrid mode and opt into SSR only where needed. -
Accessing
Astro.requestorAstro.cookiesin pre-rendered pages. Request-specific data does not exist at build time. Attempting to read headers, cookies, or client addresses in a static page causes errors or returns meaningless values. -
Placing secrets in
PUBLIC_-prefixed environment variables. Variables with thePUBLIC_prefix are embedded in client-side JavaScript bundles. API keys and database credentials must use non-prefixed names and remain server-only. -
Ignoring cold start latency on serverless platforms. Serverless adapters (Vercel, Netlify, Cloudflare) may incur cold start delays. Avoid heavy initialization at module scope and keep server-rendered pages lean to minimize time-to-first-byte.
-
Building complex form handling without using the Fetch API properly. Astro SSR pages handle forms through
Astro.request.formData(), not through framework-specific form libraries. Fighting this pattern by importing React form libraries into SSR pages adds unnecessary complexity and bundle weight.
Best Practices
- Use hybrid rendering: pre-render as many pages as possible and server-render only pages that truly need request-time data (auth, personalization, real-time data).
- Add
export const prerender = trueto pages like marketing pages, docs, and blog posts that do not depend on request context. - Use
Astro.cookiesinstead of manually parsing theCookieheader. The helper handles serialization and provides a cleaner API. - Return proper HTTP status codes. Use
Astro.redirect()for redirects andnew Response(null, { status: 404 })for not-found pages. - Choose the
standaloneadapter mode for Docker deployments andmiddlewaremode when integrating Astro into an existing Express or Fastify server.
Common Pitfalls
- Forgetting to install an adapter: SSR mode without an adapter causes a build error. Every server-rendered Astro project needs an adapter for the target platform.
- Using
Astro.requestin static pages: Request data like headers, cookies, and client address are only available in server-rendered pages. Accessing them in a pre-rendered page throws an error. - Environment variable exposure: In SSR,
import.meta.envvariables prefixed withPUBLIC_are exposed to the client. Keep secrets in non-prefixed variables (e.g.,API_SECRET, notPUBLIC_API_SECRET). - Cold start latency: Serverless adapters (Vercel, Netlify, Cloudflare) may have cold start delays. Keep SSR pages lightweight and avoid heavy initialization logic at module scope.
- Adapter-specific limitations: Cloudflare Workers does not support Node.js built-in modules like
fsorpath. Vercel Edge Functions have size limits. Check adapter documentation for your platform's constraints.
Install this skill directly: skilldb add astro-skills
Related Skills
Astro Basics
Astro fundamentals including project structure, components, islands architecture, and templating syntax
Astro Content Collections
Content collections in Astro for managing Markdown, MDX, JSON, and YAML content with type-safe schemas
Astro Deployment
Deploying Astro sites to Vercel, Netlify, Cloudflare Pages, and other platforms
Astro Integrations
Using React, Vue, Svelte, and other UI framework islands within Astro pages
Astro Middleware
Middleware patterns in Astro for authentication, request modification, response headers, and shared context
Astro Routing
File-based and dynamic routing in Astro including static paths, rest parameters, and route priority