Dynamic Routes
Routes with a variable part
Section titled “Routes with a variable part”You have a collection of 50 blog posts and want a page for each — but you’re not going to write 50 files. A dynamic route is one file whose name has a variable segment in square brackets, and it stands in for many URLs:
src/pages/blog/[slug].astro→ matches/blog/anythingsrc/pages/[category]/[id].astro→ two variablessrc/pages/docs/[...path].astro→ a rest param, matching/docs/a/b/c(any depth)
The value in the brackets arrives at runtime on Astro.params (Astro.params.slug). But for a static site, Astro also needs to know which slugs exist so it can build a file for each — that’s what getStaticPaths() is for.
getStaticPaths(): one page per entry
Section titled “getStaticPaths(): one page per entry”In a static build, a dynamic route file exports getStaticPaths(), which returns the list of pages to generate. Each item is { params, props } — params fills the bracketed segment, props passes data to the page:
---import { getCollection, render } from 'astro:content';
export async function getStaticPaths() { const posts = await getCollection('blog'); return posts.map((post) => ({ params: { slug: post.id }, // fills [slug] — one page per post props: { post }, // hand the whole entry to the page }));}
const { post } = Astro.props;const { Content } = await render(post);---<article> <h1>{post.data.title}</h1> <time>{post.data.pubDate.toDateString()}</time> <Content /></article>At build time Astro calls getStaticPaths(), sees (say) 50 entries, and writes 50 HTML files — /blog/hello-world, /blog/second-post, and so on. The params key must match the bracket name (slug here); the props are your escape hatch to pass the already-loaded entry so the page body needn’t re-query.
flowchart LR
coll["getCollection('blog') — 50 entries"] --> map["map each to params + props"]
map --> paths["getStaticPaths returns 50 paths"]
paths --> build["build writes 50 HTML files"] Pagination with paginate()
Section titled “Pagination with paginate()”For a long list you want numbered pages — /blog/1, /blog/2. getStaticPaths receives a paginate helper that slices a list into pages and generates the routes for you. Name the route file with the page param, usually src/pages/blog/[page].astro:
---import { getCollection } from 'astro:content';
export async function getStaticPaths({ paginate }) { const posts = await getCollection('blog'); return paginate(posts, { pageSize: 10 }); // 10 posts per page}
const { page } = Astro.props; // paginate injects a `page` prop---<ul> {page.data.map((post) => <li>{post.data.title}</li>)}</ul><nav> {page.url.prev && <a href={page.url.prev}>Previous</a>} <span>Page {page.currentPage} of {page.lastPage}</span> {page.url.next && <a href={page.url.next}>Next</a>}</nav>paginate hands each page a page prop with data (this page’s slice), currentPage, lastPage, and url.prev/url.next — everything you need for the list and the prev/next links.
Static params vs on-demand params
Section titled “Static params vs on-demand params”There are two ways a dynamic route learns its params, and they mirror the two rendering modes:
- Static: you enumerate every param ahead of time with
getStaticPaths(), and Astro builds a file per path. Fast, cacheable, but the set of pages is fixed at build. - On-demand: set
export const prerender = falseand dropgetStaticPaths()— instead readAstro.paramslive and query on the fly. This handles URLs you can’t know at build time (a user id, a search term):
---// src/pages/blog/[slug].astro — on-demand versionexport const prerender = false;import { getEntry, render } from 'astro:content';
const { slug } = Astro.params;const post = await getEntry('blog', slug);if (!post) return Astro.redirect('/404');
const { Content } = await render(post);---<h1>{post.data.title}</h1><Content />Rule of thumb: a known, finite set of pages (all your blog posts) → getStaticPaths() and stay static. An open-ended set (any user profile, arbitrary search) → prerender = false and read Astro.params per request.