Skip to content

Dynamic Routes & SSR

Dynamic routes and SSR are two of the most powerful features in Astro, but they can be a bit tricky to understand at first. In this chapter, we’ll break them down into simple terms, with clear examples and step-by-step explanations.

When you want one page file to handle many different blog posts (like /blog/hello-world, /blog/my-trip, and so on), you use a special file name with three dots in front, like [...slug].astro. This lets you build nested or flexible dynamic routes.

---
// src/pages/blog/[...slug].astro
// "[...slug].astro" allows to create dynamic nested routes
import { getCollection } from "astro:content";
import type { CollectionEntry } from "astro:content";
export async function getStaticPaths() {
const posts: CollectionEntry<"blog">[] = await getCollection("blog");
return posts.map((post) => ({
params: { slug: post.data.slug },
props: { post },
}));
}
const { post } = Astro.props;
---
<article>
<h1>{post.data.title}</h1>
<time>{post.data.date.toDateString()}</time>
<div innerHTML={post.body} />
</article>

In plain words: getStaticPaths() tells Astro, “here is the full list of slugs that exist, please build one page for each of them, ahead of time.” This only works for static pages (the default mode in Astro), where everything is built once, in advance.

So far, everything we’ve built has been static. That means Astro builds all your HTML pages once, ahead of time (this is called build time), and then those same HTML files get served to every visitor.

But sometimes, you need a page to be built fresh, every single time someone visits it. For example:

  • A page that shows content only after checking if the user is logged in
  • A page that shows live, constantly-changing data
  • A page that handles form submissions and needs to respond based on what the user just typed

This is called SSR, which stands for Server-Side Rendering. In simple words: instead of Astro building the page once and reusing it for everyone, the page gets built again on the server, every time someone asks for it.

Static (SSG) vs Server-Rendered (SSR), Side by Side

Section titled “Static (SSG) vs Server-Rendered (SSR), Side by Side”
Static (the default)Server-Rendered (SSR)
When is the page built?Once, ahead of time, during the buildFresh, every time someone visits
Good forBlogs, docs, marketing pages, portfoliosLogins, live data, personalized pages, forms
SpeedVery fast (it’s just a saved HTML file)Slightly slower (the server has to build it on the spot)
Needs a live server?No, plain files are enoughYes, you need a server running

To use SSR, you need to do two things: pick an output mode, and add an adapter (this is the piece of code that lets Astro actually run on a real server, like Vercel or Node).

Astro gives you three choices for the output setting in astro.config.mjs:

  • "static" - this is the default. Every page is built ahead of time. No real server needed.
  • "server" - every page is rendered fresh on the server, by default. You can still mark specific pages as static if you want.
  • "hybrid" - the opposite of "server". Most pages stay static by default, and you only turn on server-rendering for the specific pages that actually need it.
// astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
output: "server", // enable SSR, it is by default "static"
});

SSR needs a real server to run on, so Astro requires you to install an “adapter” - this is just a small package that connects Astro to whichever hosting platform you’re using.

Terminal window
npx astro add vercel

You can swap vercel for netlify, node, cloudflare, or whichever platform you’re deploying to. Running this command installs the right package and updates your astro.config.mjs file automatically.

Step 3 (Optional): Mix Static and Server Pages

Section titled “Step 3 (Optional): Mix Static and Server Pages”

Even after turning on SSR, you can still choose, page by page, whether that specific page should be static or server-rendered. You do this with a special export at the top of the page file:

---
export const prerender = false; // this page will be rendered fresh, on the server
---
---
export const prerender = true; // this page will be built ahead of time, like a static page
---
  1. If your output is set to "server", every page is server-rendered by default. Add export const prerender = true only to the few pages you want to keep static.
  2. If your output is set to "hybrid", every page is static by default. Add export const prerender = false only to the few pages that need fresh, on-demand rendering.

SSR is useful for things like:

  • Login and authentication checks
  • Forms that need to process and respond to user input
  • Pages that show data that changes very often

Here’s a complete, corrected example of a page that loads one specific blog post by its slug, using SSR. This is useful when, for example, you want to check something (like a login) before showing the post, or when your content is too large or too dynamic to pre-build every single page in advance.

---
import { getEntry, render } from "astro:content";
const { slug } = Astro.params;
if (slug === undefined) {
throw new Error("Slug is required");
}
const post = await getEntry("blog", slug);
if (post === undefined) {
return Astro.redirect("/404");
}
const { Content } = await render(post);
---
<article>
<h1>{post.data.title}</h1>
<time>{post.data.date.toDateString()}</time>
<Content />
</article>

Let’s break this down, step by step, in plain words:

  1. getEntry("blog", slug) looks for one specific blog post, matching the slug value taken from the page’s URL.
  2. If there’s no slug in the URL at all, we stop and throw an error, since we can’t look anything up without it.
  3. If no post is found for that slug, we redirect the visitor to a 404 (“page not found”) page, instead of showing a broken page.
  4. render(post) is the correct, modern way to turn the Markdown body of a post into an actual <Content /> component that Astro can display. This render() function is imported directly from "astro:content".
  5. Finally, we just drop <Content /> into our HTML, exactly like any other component, and it shows the full body of the blog post.