Type something to search...
How to create dynamic routes with getStaticPaths in Astro?

How to create dynamic routes with getStaticPaths in Astro?

To create a dynamic route in Astro, name a page file with a parameter in square brackets, such as src/pages/blog/[id].astro, and export a getStaticPaths() function from it. That function returns an array of objects, each with a params object that tells Astro which value to put in the brackets. Astro builds one HTML page per object at build time, and the page reads its value from Astro.params. You can also attach a props object to each entry to pass the full data for that page, so the template doesn't need to look it up again.

Dynamic routes are how a single template becomes a whole blog, a product catalogue or a set of documentation pages. In this article you'll learn the basic pattern, the difference between params and props, how to generate pages from a content collection, how to use several parameters in one path, how to type everything with TypeScript, the rules that trip people up (strings only, isolated scope), and what changes when the route is rendered on demand instead of at build time.

The Basic Pattern

A dynamic page has two parts: a file name with a bracketed parameter, and a getStaticPaths() export that lists the values.

---
// src/pages/products/[id].astro
export function getStaticPaths() {
  return [1, 2, 3].map((n) => ({ params: { id: String(n) } }));
}

const { id } = Astro.params;
---
<p>Product {id}</p>

The name inside the brackets, id, must match the key in each params object. Running astro build on this file produced three pages:

/products/1/index.html
/products/2/index.html
/products/3/index.html

Any other URL under /products/ doesn't exist in the built site. That's the key idea of static mode: every page must be known at build time, and getStaticPaths() is how you tell Astro what they are.

If you haven't worked with Astro's routing before, start with how file-based routing works in Astro. Dynamic routes are the same system with placeholders.

What Happens Without getStaticPaths()

In the default static output, a bracketed page without getStaticPaths() stops the build. Astro reports a GetStaticPathsRequired error and suggests adding export const prerender = false if you intended the page to be rendered on demand. That suggestion is a good hint about the two modes, which we'll come back to later.

params vs props

Each object returned by getStaticPaths() can have two keys:

KeyRequiredEnds up inAllowed values
paramsYesThe URL, and Astro.paramsStrings or undefined
propsNoAstro.props onlyAny serialisable data

params decides the URL. props is extra data handed to the page for that URL. A common mistake is to pass only an ID in params and then search for the item again in the template. It works, but props is cleaner because the work happens once:

// src/data/products.ts
export const products = [
  { sku: "mug-01", category: "kitchen", name: "Enamel mug", price: 14 },
  { sku: "lamp-02", category: "living", name: "Desk lamp", price: 49 },
  { sku: "towel-03", category: "kitchen", name: "Linen towel", price: 12 },
];
---
// src/pages/shop/[category]/[sku].astro
import type { GetStaticPaths } from "astro";
import { products } from "../../../data/products";

export const getStaticPaths = (() => {
  return products.map((product) => ({
    params: { category: product.category, sku: product.sku },
    props: { product },
  }));
}) satisfies GetStaticPaths;

const { category, sku } = Astro.params;
const { product } = Astro.props;
---
<html lang="en">
  <head>
    <title>{product.name}</title>
  </head>
  <body>
    <p>{category} / {sku}</p>
    <h1>{product.name}</h1>
    <p>£{product.price}</p>
  </body>
</html>

The build output for one of those pages:

<!DOCTYPE html><html lang="en"><head><title>Enamel mug</title></head><body><p>kitchen / mug-01</p><h1>Enamel mug</h1><p>£14</p></body></html>

Multiple Parameters in One Path

The example above already uses two parameters, each in its own folder: [category]/[sku].astro. Every parameter in the path must appear in every params object. The three products produced:

/shop/kitchen/mug-01/index.html
/shop/living/lamp-02/index.html
/shop/kitchen/towel-03/index.html

Parameters can also share a single segment. A file named [lang]-[version]/info.astro with params: { lang: "en", version: "v1" } builds /en-v1/info. That's useful for compact URLs, but separate folders are usually easier to read and reason about.

When you need a parameter that can span several segments, such as docs/guides/setup, use a rest parameter like [...path].astro instead. That's covered in rest parameters for catch-all routes.

Generating Pages from a Content Collection

The most common real-world use of getStaticPaths() is turning a content collection into pages. In Astro 6 and later, collections are defined in src/content.config.ts with a loader:

// src/content.config.ts
import { defineCollection } from "astro:content";
import { glob } from "astro/loaders";
import { z } from "astro/zod";

const blog = defineCollection({
  loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
  schema: z.object({
    title: z.string(),
    pubDate: z.coerce.date(),
    draft: z.boolean().default(false),
  }),
});

export const collections = { blog };

Then a single dynamic page renders every published post:

---
// src/pages/blog/[id].astro
import { getCollection, render } from "astro:content";
import type { GetStaticPaths } from "astro";

export const getStaticPaths = (async () => {
  const posts = await getCollection("blog", ({ data }) => !data.draft);
  return posts.map((post) => ({
    params: { id: post.id },
    props: { post },
  }));
}) satisfies GetStaticPaths;

const { post } = Astro.props;
const { Content } = await render(post);
---
<html lang="en">
  <head>
    <title>{post.data.title}</title>
  </head>
  <body>
    <h1>{post.data.title}</h1>
    <time datetime={post.data.pubDate.toISOString()}>
      {post.data.pubDate.toDateString()}
    </time>
    <Content />
  </body>
</html>

A few details reflect current Astro rather than older tutorials:

  • post.id, not post.slug: entries from the glob() loader have an id derived from the file path, such as first-post for first-post.md. The old slug property belonged to legacy collections, which Astro 6 removed.
  • render(post), not post.render(): import render from astro:content and pass it the entry.
  • z from astro/zod: the schema helper comes from astro/zod, which is Zod 4.
  • Filtering drafts: the second argument to getCollection() filters entries. Here the draft post was skipped, so the build produced only /blog/first-post/ and /blog/second-post/.

The rendered page from the build looked like this:

<!DOCTYPE html><html lang="en"><head><title>My first post</title></head><body><h1>My first post</h1><time datetime="2026-09-01T00:00:00.000Z">Tue Sep 01 2026</time><p>Hello from the <strong>first</strong> post.</p>
</body></html>

For more on defining and querying collections, see what content collections are in Astro.

Using Remote Data

getStaticPaths() can be async, so you can fetch from an API or CMS at build time:

---
// src/pages/posts/[id].astro
export async function getStaticPaths() {
  const response = await fetch("https://example.com/api/posts");
  const posts: { id: string; title: string }[] = await response.json();

  return posts.map((post) => ({
    params: { id: post.id },
    props: { post },
  }));
}

const { post } = Astro.props;
---
<h1>{post.title}</h1>

The request runs once during the build. If the API returns a new post later, you need a new build to get a new page.

Typing getStaticPaths() with TypeScript

Wrapping the function in satisfies GetStaticPaths does two jobs: it checks the return shape, and it lets Astro infer the types of Astro.params and Astro.props from what you return. I confirmed this with astro check by referencing a property that didn't exist on the product:

src/pages/shop/[category]/[sku].astro:25:40 - error ts(2339): Property 'nope' does not exist on type '{ sku: string; category: string; name: string; price: number; }'.

If you want named types to reuse elsewhere, Astro also exports two helpers:

---
import type {
  GetStaticPaths,
  InferGetStaticParamsType,
  InferGetStaticPropsType,
} from "astro";

export const getStaticPaths = (() => {
  return [{ params: { id: "a" }, props: { label: "First" } }];
}) satisfies GetStaticPaths;

type Params = InferGetStaticParamsType<typeof getStaticPaths>;
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
---

Remember that astro build doesn't type-check. Run astro check (it needs @astrojs/check and typescript installed) in CI or before building if you want type errors to stop a deploy. The TypeScript props guide goes further into typing components.

Rules That Trip People Up

Params Must Be Strings

Since Astro 6, params values must be strings or undefined. Numbers are no longer converted for you. I tried returning { params: { n: 1 } } and the build failed:

[GetStaticPathsInvalidRouteParam] Invalid `getStaticPaths()` route parameter for `n`. Expected a string or undefined, received `number` (`1`).

Wrap numeric IDs in String() or use a template literal.

getStaticPaths() Runs in Its Own Scope

The function executes once, before any page renders, in an isolated scope. You can use imports, but you can't use variables declared elsewhere in the frontmatter:

---
import { products } from "../data/products"; // fine: an import

const featured = ["mug-01"]; // not visible inside getStaticPaths()

export function getStaticPaths() {
  return products.map((p) => ({ params: { sku: p.sku } }));
}
---

When I tried using featured inside getStaticPaths(), the build stopped with featured is not defined. Move shared values into a separate module and import them.

Don't Use Astro Inside getStaticPaths()

The Astro object inside getStaticPaths() is deprecated since Astro 6. Using Astro.site there logs a warning:

Astro.site inside getStaticPaths is deprecated and will be removed in a future major version of Astro. Use import.meta.env.SITE instead

Use import.meta.env.SITE if you need your configured site URL. If you need the route pattern, getStaticPaths() receives it as an option: getStaticPaths({ routePattern }) gave me /t/[n] for a test file at src/pages/t/[n].astro.

Params Aren't Decoded

Values you return in params are used as they are. If you need to decode a value, call decodeURI() yourself.

Dynamic Routes Rendered on Demand

Everything above assumes static output. With an adapter installed, you can render a dynamic route on demand instead. The file name works the same way, but there's no getStaticPaths(): any value that matches the pattern reaches the page, and you look up the data at request time.

// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";

export default defineConfig({
  adapter: node({ mode: "standalone" }),
});
---
// src/pages/live/[sku].astro
export const prerender = false;

import { products } from "../../data/products";

const { sku } = Astro.params;
const product = products.find((p) => p.sku === sku);

if (!product) {
  return new Response(null, { status: 404, statusText: "Not found" });
}
---
<html lang="en">
  <head>
    <title>{product.name}</title>
  </head>
  <body>
    <h1>{product.name}</h1>
  </body>
</html>

Testing the built Node server with curl:

curl -s http://localhost:4323/live/lamp-02
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:4323/live/nothing
<!DOCTYPE html><html lang="en"><head><title>Desk lamp</title></head><body><h1>Desk lamp</h1></body></html>
404

Because nothing is generated ahead of time, you must handle unknown values yourself, as the 404 response does here. Choosing between the two approaches is mostly about how often your data changes:

getStaticPaths() (prerendered)On-demand
When pages are madeAt build timeOn each request
Unknown valuesDon't exist, host serves 404Reach your code, you decide
Passing propsYes, via propsNo, look up data in the page
Needs an adapterNoYes
New content appearsAfter a rebuildImmediately

The wider trade-offs are covered in static vs on-demand rendering in Astro.


FAQ: Dynamic Routes and getStaticPaths

Only prerendered ones. In the default static output, a page with brackets in its name must export getStaticPaths() or the build fails. Pages that set prerender to false and run on a server with an adapter don't use it.

Params define the URL and must be strings or undefined. Props are any extra data you want the page to receive through Astro.props, such as a full content entry. Props aren't part of the URL.

Since Astro 6, getStaticPaths() must return string or undefined values in params. Convert numbers with String() or a template literal before returning them.

Yes. Make the function async and await your fetch call. It runs once at build time, so new data only appears after the next build.

Use post.id. Entries loaded with the glob() loader have an id based on the file path, and the slug property from legacy content collections no longer exists in Astro 6 and later.

Write getStaticPaths() with satisfies GetStaticPaths and Astro infers params and props for you. You can also use InferGetStaticParamsType and InferGetStaticPropsType to create named types.


Conclusion

Dynamic routes in Astro come down to two pieces: a bracketed file name that defines the pattern, and a getStaticPaths() function that lists the values to build. Use params for the parts of the URL and props for the data each page needs, and you can generate any number of pages from one template, whether the data comes from a local file, a content collection or a remote API.

Keep the modern rules in mind: params are strings only, the function runs in its own scope, import.meta.env.SITE replaces Astro.site inside it, and collection entries use id with render(). When your data changes too often for rebuilds, switch the route to on-demand rendering and handle unknown values in the page itself.

Tags :
Share :

Related Posts

What are Astro Actions and how do they work?

What are Astro Actions and how do they work?

Astro Actions are type-safe backend functions that you define once on the server and then call from the browser as if they were ordinary async fu

Dive Deeper
What are Astro adapters and which one should you choose?

What are Astro adapters and which one should you choose?

An Astro adapter is an integration that builds the server part of your project for a specific runtime, such as Node.js, Vercel Functions, Netlify

Dive Deeper
How to build API endpoints in Astro?

How to build API endpoints in Astro?

To build an API endpoint in Astro, create a .js or .ts file in src/pages/ and export a function named after an HTTP method, such as GET or

Dive Deeper