Skip to main content
Technology & EngineeringAstro281 lines

Astro Routing

File-based and dynamic routing in Astro including static paths, rest parameters, and route priority

Quick Summary32 lines
You are an expert in Astro's file-based and dynamic routing system for building websites with clean URL structures.

## Key Points

1. Static routes (`/about` beats `/[slug]`)
2. Dynamic routes with more segments beat fewer (`/blog/[slug]` beats `/[...path]`)
3. Named parameters beat rest parameters (`/[slug]` beats `/[...path]`)
4. Pre-rendered dynamic routes beat server-rendered ones
5. Endpoints beat pages
- **Mixing `src/pages/blog.astro` and `src/pages/blog/index.astro`.** Both resolve to `/blog`, creating unpredictable behavior. Pick one convention and use it consistently across the project.
- Use `getStaticPaths` to pre-render known pages at build time, even in hybrid mode. Pre-rendered pages are faster and cheaper to serve.
- Pass data through `props` in `getStaticPaths` rather than re-fetching inside the page component. Astro makes props available directly.
- Use rest parameters (`[...slug]`) for catch-all routes like documentation trees or 404 pages.
- Keep route nesting shallow. Deeply nested dynamic segments make URLs harder to reason about and `getStaticPaths` more complex.
- Name API endpoint files with their extension (e.g., `posts.json.ts`) so the generated file has the correct MIME type in static mode.
- **Missing `getStaticPaths` in static mode**: Dynamic routes without `getStaticPaths` cause a build error in static output mode. Every possible parameter combination must be enumerated.

## Quick Example

```
src/pages/index.astro       -> /
src/pages/about.astro        -> /about
src/pages/blog/index.astro   -> /blog
src/pages/blog/first-post.md -> /blog/first-post
```

```
src/pages/blog/[slug].astro       -> /blog/:slug
src/pages/users/[id]/posts.astro  -> /users/:id/posts
```
skilldb get astro-skills/Astro RoutingFull skill: 281 lines
Paste into your CLAUDE.md or agent config

Routing — Astro

You are an expert in Astro's file-based and dynamic routing system for building websites with clean URL structures.

Overview

Astro uses file-based routing: every .astro or .md file in src/pages/ automatically becomes a route. Dynamic segments, rest parameters, and getStaticPaths give you full control over URL generation for both static and server-rendered sites.

Core Concepts

Static Routes

Files in src/pages/ map directly to URLs:

src/pages/index.astro       -> /
src/pages/about.astro        -> /about
src/pages/blog/index.astro   -> /blog
src/pages/blog/first-post.md -> /blog/first-post

Dynamic Routes

Use bracket notation for dynamic segments:

src/pages/blog/[slug].astro       -> /blog/:slug
src/pages/users/[id]/posts.astro  -> /users/:id/posts

In static (pre-rendered) mode, dynamic routes require getStaticPaths to tell Astro which pages to generate:

---
// src/pages/blog/[slug].astro
import { getCollection, render } from 'astro:content';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map(post => ({
    params: { slug: post.id },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = await render(post);
---

<h1>{post.data.title}</h1>
<Content />

Rest Parameters

Use [...param] to match multiple path segments:

src/pages/docs/[...path].astro -> /docs, /docs/getting-started, /docs/api/reference
---
// src/pages/docs/[...path].astro
export async function getStaticPaths() {
  return [
    { params: { path: undefined }, props: { title: 'Docs Home' } },
    { params: { path: 'getting-started' }, props: { title: 'Getting Started' } },
    { params: { path: 'api/reference' }, props: { title: 'API Reference' } },
  ];
}

const { title } = Astro.props;
---

<h1>{title}</h1>

Setting path to undefined matches the base route (/docs).

Multiple Dynamic Segments

---
// src/pages/[lang]/[category]/[slug].astro
export async function getStaticPaths() {
  return [
    { params: { lang: 'en', category: 'guides', slug: 'setup' } },
    { params: { lang: 'fr', category: 'guides', slug: 'setup' } },
    { params: { lang: 'en', category: 'api', slug: 'auth' } },
  ];
}

const { lang, category, slug } = Astro.params;
---

<p>Language: {lang}, Category: {category}, Slug: {slug}</p>

SSR Dynamic Routes

In server-rendered mode (output: 'server'), you do not need getStaticPaths. Parameters are available directly from Astro.params:

---
// src/pages/products/[id].astro (SSR mode)
const { id } = Astro.params;
const product = await fetch(`https://api.example.com/products/${id}`).then(r => r.json());

if (!product) {
  return Astro.redirect('/404');
}
---

<h1>{product.name}</h1>
<p>{product.description}</p>

Implementation Patterns

Pagination

Astro provides a built-in paginate() helper:

---
// src/pages/blog/[...page].astro
import { getCollection } from 'astro:content';

export async function getStaticPaths({ paginate }) {
  const allPosts = (await getCollection('blog'))
    .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());

  return paginate(allPosts, { pageSize: 10 });
}

const { page } = Astro.props;
// page.data       - array of posts for this page
// page.currentPage - current page number (1-based)
// page.lastPage   - total number of pages
// page.url.prev   - URL of previous page (or undefined)
// page.url.next   - URL of next page (or undefined)
---

<ul>
  {page.data.map(post => (
    <li><a href={`/blog/${post.id}`}>{post.data.title}</a></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>

This generates /blog (page 1), /blog/2, /blog/3, etc.

Filtered Pagination by Tag

---
// src/pages/tags/[tag]/[...page].astro
import { getCollection } from 'astro:content';

export async function getStaticPaths({ paginate }) {
  const allPosts = await getCollection('blog');

  const uniqueTags = [...new Set(allPosts.flatMap(post => post.data.tags))];

  return uniqueTags.flatMap(tag => {
    const filtered = allPosts.filter(post => post.data.tags.includes(tag));
    return paginate(filtered, {
      params: { tag },
      pageSize: 10,
    });
  });
}

const { tag } = Astro.params;
const { page } = Astro.props;
---

<h1>Posts tagged "{tag}"</h1>
<ul>
  {page.data.map(post => <li>{post.data.title}</li>)}
</ul>

API Routes (Endpoints)

Create JSON or other non-HTML responses with .ts or .js files in src/pages/:

// src/pages/api/posts.json.ts
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';

export const GET: APIRoute = async () => {
  const posts = await getCollection('blog');
  const data = posts.map(p => ({
    id: p.id,
    title: p.data.title,
    pubDate: p.data.pubDate,
  }));

  return new Response(JSON.stringify(data), {
    headers: { 'Content-Type': 'application/json' },
  });
};

Redirects

Configure redirects in astro.config.mjs:

export default defineConfig({
  redirects: {
    '/old-blog': '/blog',
    '/old-blog/[...slug]': '/blog/[...slug]',
    '/twitter': {
      status: 302,
      destination: 'https://twitter.com/example',
    },
  },
});

Route Priority

Astro resolves routes in this priority order:

  1. Static routes (/about beats /[slug])
  2. Dynamic routes with more segments beat fewer (/blog/[slug] beats /[...path])
  3. Named parameters beat rest parameters (/[slug] beats /[...path])
  4. Pre-rendered dynamic routes beat server-rendered ones
  5. Endpoints beat pages

Core Philosophy

Astro's routing is built on a simple contract: the file system is the source of truth for your site's URL structure. Every file in src/pages/ becomes a route, which means you can understand the entire URL space of a site by looking at the directory tree. This eliminates routing configuration files, reduces indirection, and makes the relationship between code and URLs immediately obvious.

The getStaticPaths function represents Astro's commitment to pre-rendering as the default strategy. By requiring you to enumerate every possible parameter combination for dynamic routes in static mode, Astro forces you to think about your site as a finite set of pages that can be fully built ahead of time. This is the opposite of an SPA router that resolves paths at runtime, and it results in faster, more cacheable, and more reliable sites.

Routing in Astro also embodies a clear priority system that eliminates ambiguity. Static routes always win over dynamic ones, named parameters beat rest parameters, and pre-rendered routes outrank server-rendered ones. This deterministic resolution order means you never need to debug which route handler matched a given URL. The rules are explicit and consistent.

Anti-Patterns

  • Creating deeply nested dynamic route hierarchies. Routes like [lang]/[category]/[subcategory]/[slug] produce complex getStaticPaths functions that are hard to maintain and debug. Flatten the hierarchy where possible or use rest parameters with your own parsing logic.

  • Re-fetching data inside the page component that was already available in getStaticPaths. The props returned from getStaticPaths are passed directly to the page. Fetching the same data again inside the component doubles the work and slows builds.

  • Using getStaticPaths to generate hundreds of pages from a slow API. If your data source is slow, the build becomes slow. Cache API responses locally during development and use incremental builds in production when the dataset is large.

  • Mixing src/pages/blog.astro and src/pages/blog/index.astro. Both resolve to /blog, creating unpredictable behavior. Pick one convention and use it consistently across the project.

  • Ignoring route priority rules when adding new routes. Adding a new static route that shadows an existing dynamic route can silently change which content appears at a URL. Review the priority rules before creating routes that could overlap.

Best Practices

  • Use getStaticPaths to pre-render known pages at build time, even in hybrid mode. Pre-rendered pages are faster and cheaper to serve.
  • Pass data through props in getStaticPaths rather than re-fetching inside the page component. Astro makes props available directly.
  • Use rest parameters ([...slug]) for catch-all routes like documentation trees or 404 pages.
  • Keep route nesting shallow. Deeply nested dynamic segments make URLs harder to reason about and getStaticPaths more complex.
  • Name API endpoint files with their extension (e.g., posts.json.ts) so the generated file has the correct MIME type in static mode.

Common Pitfalls

  • Missing getStaticPaths in static mode: Dynamic routes without getStaticPaths cause a build error in static output mode. Every possible parameter combination must be enumerated.
  • Returning invalid params: Every params value must be a string or undefined. Passing numbers or objects causes silent routing failures.
  • Rest parameter matching the root: For [...path].astro, pass { path: undefined } to match the base path, not an empty string.
  • Conflicting routes: Two files like src/pages/blog.astro and src/pages/blog/index.astro resolve to the same URL and cause unpredictable behavior. Use one or the other.
  • Pagination off-by-one: page.currentPage is 1-based. The first page URL omits the number (/blog not /blog/1), which can confuse link-building logic.

Install this skill directly: skilldb add astro-skills

Get CLI access →