Dynamic Routes
Most real sites can’t enumerate every URL by hand. Blog posts, product pages, user profiles, and docs articles all share a single template but differ by an identifier in the path. Astro solves this with dynamic routes: file names wrapped in square brackets, like [slug].astro, where the bracketed segment becomes a named parameter you read at build time. Because Astro is static-first, these routes are pre-rendered to plain HTML with zero JavaScript shipped by default — you get the ergonomics of templated routing without the runtime cost.
Bracket syntax
Any .astro page (or API route) inside src/pages/ whose file or folder name is wrapped in [ ] becomes a dynamic route. The text between the brackets is the parameter name.
src/pages/
├── blog/
│ └── [slug].astro → /blog/:slug
├── products/
│ └── [id].astro → /products/:id
└── [category]/
└── [page].astro → /:category/:page
A file like [slug].astro matches a single path segment. Folder brackets work the same way, letting you compose nested parameters such as /electronics/laptops from [category]/[page].astro.
Brackets capture exactly one segment. To match an arbitrary number of segments (e.g.
/docs/a/b/c), use a rest parameter[...path].astroinstead.
Reading params from Astro.params
Inside the component script (the --- fence), the captured values are available on Astro.params. Each key matches a bracket name, and each value is the matching URL segment as a string.
---
// src/pages/blog/[slug].astro
const { slug } = Astro.params;
---
<h1>Reading post: {slug}</h1>
<p>The URL segment was "{slug}".</p>
Visiting /blog/hello-world renders:
Output:
Reading post: hello-world
The URL segment was "hello-world".
getStaticPaths for static output
In Astro’s default static build, the framework needs to know which parameter values exist so it can pre-render a page for each one. You provide that list by exporting a getStaticPaths() function. It returns an array of objects, each with a params key that maps every bracket name to a concrete value.
---
// src/pages/blog/[slug].astro
export async function getStaticPaths() {
const posts = await Astro.glob('../../content/posts/*.md');
return posts.map((post) => ({
params: { slug: post.frontmatter.slug },
props: { post },
}));
}
const { slug } = Astro.params;
const { post } = Astro.props;
const { Content } = post;
---
<article>
<h1>{post.frontmatter.title}</h1>
<p>Permalink: /blog/{slug}</p>
<Content />
</article>
The props key is the companion to params: anything you place there is passed to the page via Astro.props, so you can hand over the full data record alongside the identifier and avoid a second lookup.
params vs props
It’s worth being precise about the difference, since both flow out of getStaticPaths().
| Concept | Source | Type | Appears in URL? |
|---|---|---|---|
Astro.params | bracket segments / params key | string (or undefined) | Yes |
Astro.props | props key in getStaticPaths | any serializable value | No |
Params are always strings because they come from the URL. If you need a number, coerce it explicitly with Number(id).
On-demand rendering
When a route is server-rendered (a prerender = false page on an adapter, or fully on-demand output), Astro can’t enumerate paths ahead of time, so getStaticPaths() is not used. Instead, Astro.params is populated live from the incoming request URL.
---
// src/pages/products/[id].astro
export const prerender = false;
const { id } = Astro.params;
const res = await fetch(`https://api.example.com/products/${id}`);
if (!res.ok) {
return Astro.redirect('/404');
}
const product = await res.json();
---
<h1>{product.name}</h1>
<p>${product.price}</p>
This mode trades pre-rendering for flexibility — ideal when the set of valid IDs is huge or changes constantly.
Best practices
- Prefer static
getStaticPaths()whenever the set of pages is known at build time — it ships the fastest, cheapest pages with zero JS. - Keep bracket names descriptive (
[slug],[productId]) soAstro.paramsdestructuring reads clearly. - Always coerce params before numeric or boolean logic; remember they arrive as strings.
- Pass full records through
propsingetStaticPathsto avoid redundant data fetches inside the template. - For server-rendered dynamic routes, validate the param and redirect or return a 404 when the resource is missing.
- Reach for rest parameters (
[...path]) only when you genuinely need to match multiple, variable-length segments.