Photo by PJH on Unsplash

On this page

This chapter follows Islands and client directives in Astro.

Static pages are fast and cheap to host, but they are the same for every visitor and only change when you rebuild. A login page, a search results page, or a form that writes to a database needs code that runs per request. Astro lets you add that route by route instead of turning the whole site into a server application.

Prerendered and on-demand routes

Astro has two output modes, and both allow mixing:

Config Default for each route Opt a route out
output: 'static' (default) Prerendered at build time export const prerender = false
output: 'server' Rendered on demand export const prerender = true

Pick the mode that matches most of your routes. A content site keeps static and marks the few dynamic pages; an application uses server and marks the marketing pages as prerendered.

src/pages/account.astro
---
export const prerender = false;
const user = Astro.cookies.get('user')?.value;
if (!user) return Astro.redirect('/login/');
---
<h1>Welcome back, {user}</h1>

An on-demand dynamic route such as src/pages/products/[id].astro does not need getStaticPaths; Astro.params.id comes from the request URL and the page can look the product up at request time.

Adapters

On-demand routes need a runtime to answer requests. An adapter builds the server part of your site for a specific host:

Terminal window
npx astro add node
npx astro add cloudflare
npx astro add vercel
npx astro add netlify

@astrojs/node produces a standalone Node.js server or middleware for an existing one. The hosting adapters produce functions in the format each platform expects. Prerendered routes are still emitted as static files and served without invoking the server. This knowledgebase has no adapter: every page is prerendered, so the build output is plain files.

Request data, cookies, and responses

Inside an on-demand page or endpoint, the Astro global exposes the request:

API Gives you
Astro.request The standard Request: method, headers, body
Astro.url The parsed URL, including searchParams
Astro.params Values from dynamic route segments
Astro.cookies get, set, has, and delete with cookie options
Astro.redirect(path, status) A redirect response
Astro.response Status and headers for the page response
Astro.locals Data placed there by middleware
src/pages/search.astro
---
export const prerender = false;
const query = Astro.url.searchParams.get('q') ?? '';
const results = query ? await searchArticles(query) : [];
if (query && results.length === 0) Astro.response.status = 404;
---
<form><input name="q" value={query} /></form>
<p>{results.length} results</p>

In a prerendered page these values reflect the build, not a visitor: there are no request headers or cookies at build time.

Middleware

src/middleware.ts runs before every route is rendered. It can read and modify the request, short-circuit with a response, and pass data to pages through context.locals:

src/middleware.ts
import { defineMiddleware, sequence } from 'astro:middleware';
const auth = defineMiddleware(async (context, next) => {
const token = context.cookies.get('session')?.value;
context.locals.user = token ? await lookupUser(token) : null;
if (context.url.pathname.startsWith('/admin/') && !context.locals.user) {
return context.redirect('/login/');
}
return next();
});
const timing = defineMiddleware(async (_context, next) => {
const started = performance.now();
const response = await next();
response.headers.set('Server-Timing', `render;dur=${performance.now() - started}`);
return response;
});
export const onRequest = sequence(auth, timing);

Type locals once in src/env.d.ts by extending App.Locals, and every page reading Astro.locals.user is checked. Middleware also runs for prerendered pages during the build, which is useful for shared setup but means request-specific logic only matters for on-demand routes.

Actions

Actions are type-safe server functions with input validation, callable from forms and from client code. Define them in src/actions/index.ts:

src/actions/index.ts
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';
export const server = {
subscribe: defineAction({
accept: 'form',
input: z.object({
email: z.string().email(),
}),
handler: async ({ email }) => {
await addSubscriber(email);
return { email };
},
}),
};

An HTML form can post to the action directly, which works without any client JavaScript:

src/pages/newsletter.astro
---
import { actions } from 'astro:actions';
export const prerender = false;
const result = Astro.getActionResult(actions.subscribe);
---
{result?.data && <p>Subscribed {result.data.email}</p>}
{result?.error && <p>Please enter a valid email address.</p>}
<form method="POST" action={actions.subscribe}>
<input type="email" name="email" required />
<button>Subscribe</button>
</form>

From an island, call await actions.subscribe(formData) and receive { data, error }; isInputError(error) tells validation failures apart from other errors. Actions replace most hand-written API endpoints, because validation and types are shared between caller and server.

Environment variables

import.meta.env works as in Vite: variables prefixed PUBLIC_ are exposed to client code, everything else stays on the server. For validated, typed variables, declare a schema with envField:

astro.config.mjs
import { defineConfig, envField } from 'astro/config';
export default defineConfig({
env: {
schema: {
API_URL: envField.string({ context: 'client', access: 'public' }),
API_SECRET: envField.string({ context: 'server', access: 'secret' }),
},
},
});

Read them from astro:env/client or astro:env/server. Astro fails the build when a required variable is missing, and refuses to import a secret into client code.

Choosing static or on demand

Prerender everything that is the same for all visitors, even pages that change daily; a scheduled rebuild is cheaper and faster than rendering per request. Use on-demand rendering when the response depends on the visitor or the request: authentication, personalized data, search parameters, and form submissions. For a mostly static page with one personal fragment, reach for a server island before making the whole page dynamic.

This is the last chapter in the Astro series. For a compact reference, see the Astro cheat sheet.

Check your understanding

Question 1 of 7

0 of 7 checked

  1. 1. With the default output: 'static', how do you render one route per request?

Search articles

Type a word to search published articles.

Open the search page