Astro routing is file-based. Every .astro, .md, .mdx, .html, .ts or .js file inside src/pages/ becomes a route, and its path in the folder becomes the URL. src/pages/about.astro serves /about/, src/pages/blog/[slug].astro serves one page per blog post, and src/pages/rss.xml.ts serves an XML feed. There is no router config to maintain.
This guide maps every kind of route Astro 7 supports to the file that produces it, then covers getStaticPaths(), pagination, endpoints, redirects, 404 pages, route priority and i18n.
Key Takeaways
- The file path under
src/pages/is the URL.index.astromaps to the folder’s root.- Square brackets make a route dynamic:
[slug].astrofor one segment,[...path].astrofor any depth.- Static dynamic routes must export
getStaticPaths(), which returns everyparamscombination to build..tsfiles insrc/pages/are endpoints that return aResponse, useful for JSON, RSS,robots.txtand images.- When patterns overlap, static routes beat dynamic ones and named params beat rest params.
The route map
| File | URL | Notes |
|---|---|---|
src/pages/index.astro |
/ |
Home page |
src/pages/about.astro |
/about/ |
Static page |
src/pages/blog/index.astro |
/blog/ |
Folder index |
src/pages/blog/[slug].astro |
/blog/any-post/ |
One dynamic segment |
src/pages/[lang]/[slug].astro |
/en/pricing/ |
Multiple params |
src/pages/docs/[...path].astro |
/docs/, /docs/a/b/c/ |
Rest param, any depth |
src/pages/blog/[page].astro |
/blog/2/ |
Pagination with paginate() |
src/pages/rss.xml.ts |
/rss.xml |
Endpoint |
src/pages/404.astro |
Not-found page | Served by most hosts automatically |
src/pages/_draft.astro |
No route | Underscore prefix excludes a file |
Files and folders starting with _ are ignored by the router, which is useful for page-local components or work in progress.
Whether /about or /about/ is canonical depends on trailingSlash and build.format in your config; the trailing slash tip explains the combinations.
Static pages
The simplest route is a file:
---
// src/pages/contact.astro
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout title="Contact" description="Get in touch.">
<h1>Contact</h1>
</BaseLayout>
Markdown files in src/pages/ become pages too, rendered with the layout named in their layout frontmatter. For anything repeated, use a content collection and a dynamic route instead; it gives you validation and a single template.
Dynamic routes and getStaticPaths()
A dynamic route has a parameter in brackets. In static mode (the default), Astro has to know every value of that parameter at build time, so the page exports getStaticPaths():
---
// 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 />
Each object in the returned array produces one page:
paramsfills the brackets in the file name. Every param named in the file must be present, as a string (orundefinedfor an optional rest param).propsis passed to the page asAstro.props. Passing the whole entry here saves a second lookup.
getStaticPaths() runs once, in isolation, before pages render. It can’t use variables from the rest of the frontmatter, but it can import and await anything.
Multiple parameters
---
// src/pages/[category]/[slug].astro
export async function getStaticPaths() {
const products = await getCollection('products');
return products.map((p) => ({
params: { category: p.data.category, slug: p.id },
props: { product: p },
}));
}
---
Rest parameters
[...path] matches any number of segments, including none. It suits docs trees where ids contain slashes:
---
// src/pages/docs/[...path].astro
export async function getStaticPaths() {
const docs = await getCollection('docs');
return docs.map((doc) => ({
params: { path: doc.id === 'index' ? undefined : doc.id },
props: { doc },
}));
}
---
Passing undefined for the rest param makes this file also serve /docs/.
Dynamic routes on demand
If a page is rendered on request (with an adapter and export const prerender = false), you skip getStaticPaths() and read params directly:
---
export const prerender = false;
const { slug } = Astro.params;
const product = await fetch(`https://api.example.com/products/${slug}`).then((r) => r.json());
if (!product) return Astro.redirect('/404/');
---
This is how you route to data that changes too often to rebuild for. The Astro SSR guide covers adapters and mixed rendering.
Pagination with paginate()
getStaticPaths() receives a paginate helper that splits an array into pages and generates the routes:
---
// src/pages/blog/[page].astro
import type { GetStaticPaths, Page } from 'astro';
import type { CollectionEntry } from 'astro:content';
import { getCollection } from 'astro:content';
export const getStaticPaths = (async ({ paginate }) => {
const posts = (await getCollection('blog'))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
return paginate(posts, { pageSize: 10 });
}) satisfies GetStaticPaths;
interface Props { page: Page<CollectionEntry<'blog'>> }
const { page } = Astro.props;
---
<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}>Newer</a>}
<span>Page {page.currentPage} of {page.lastPage}</span>
{page.url.next && <a href={page.url.next}>Older</a>}
</nav>
With [page].astro, page one lives at /blog/1/. Name the file [...page].astro to put page one at /blog/ and the rest at /blog/2/, /blog/3/ and so on.
page also exposes total, start, end, size, url.first and url.last. For tag archives, combine params and pagination by returning paginate() per tag with params: { tag }.
Endpoints: routes that return data
A .ts or .js file in src/pages/ is an endpoint. Export a function named after the HTTP method, returning a Response. The file name, extension included, sets the URL:
// src/pages/search-index.json.ts
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';
export const GET: APIRoute = async () => {
const posts = await getCollection('blog');
const index = posts.map((p) => ({ id: p.id, title: p.data.title, tags: p.data.tags }));
return new Response(JSON.stringify(index), {
headers: { 'Content-Type': 'application/json' },
});
};
In a static build, endpoints run once and their output is written to dist/ as a file. That is how Astro sites generate rss.xml, robots.txt, sitemap files, Open Graph images and JSON search indexes. Dynamic endpoints such as src/pages/api/[id].json.ts also use getStaticPaths().
On demand, endpoints become real API routes that can handle POST, read request.json() or form data, and set cookies. For form handling specifically, Astro Actions are usually less code. The endpoints docs list everything an endpoint receives.
Redirects
Permanent moves go in config:
// astro.config.mjs
export default defineConfig({
redirects: {
'/old-pricing/': '/pricing/',
'/blog/[slug]/': '/articles/[slug]/',
'/legacy/': { status: 302, destination: '/' },
},
});
On a static site without an adapter, Astro writes an HTML page with a meta refresh for each redirect, which works on every host but can’t send a real 301. Hosts like Netlify, Vercel and Cloudflare Pages can serve true HTTP redirects through their adapter or a _redirects file. Inside an on-demand page, return Astro.redirect('/login/', 302).
The redirects tip compares config, host files and runtime redirects.
Rewrites
A redirect sends the browser to a new URL. A rewrite serves another route’s content while keeping the URL the visitor asked for. In Astro, return Astro.rewrite() from a page:
---
// src/pages/products/[id].astro (on-demand)
export const prerender = false;
const product = await getProduct(Astro.params.id);
if (!product) return Astro.rewrite('/404/');
---
The visitor stays on /products/abc123/ and sees your 404 page. Rewrites also work in middleware through context.rewrite() (for A/B tests or locale fallbacks, for example) and in endpoints. In static builds, rewrites happen at build time, so they can only target routes that exist when the site is built.
Use a redirect when the content has moved and the URL should change. Use a rewrite when the URL is right but the content should come from somewhere else.
Reading the current route
A few values help components behave differently per page:
Astro.url.pathnamefor the current path, such as for active navigation links.Astro.paramsfor the dynamic segments of the current route.Astro.routePatternfor the route’s pattern, such as/blog/[slug], which is handy for analytics or debugging.
---
// src/components/NavLink.astro
const { href } = Astro.props;
const path = Astro.url.pathname;
const isActive = href === '/' ? path === '/' : path.startsWith(href);
---
<a href={href} aria-current={isActive ? 'page' : undefined} class:list={['nav-link', { active: isActive }]}>
<slot />
</a>
Setting aria-current="page" tells screen readers which link is the current page, and gives you a styling hook that doesn’t depend on a class.
404 and error pages
src/pages/404.astro builds to 404.html, which Netlify, Vercel, Cloudflare Pages and GitHub Pages serve for unknown URLs. For on-demand sites, src/pages/500.astro renders on server errors. The custom 404 page tip has a template with search and links back into the site.
Route priority
Several routes can match the same URL. Astro picks one with these rules, in order:
- Routes with more path segments win over less specific ones.
- Static routes (no params) beat dynamic routes:
/blog/featured.astrowins over/blog/[slug].astrofor/blog/featured/. - Named params beat rest params:
[slug].astrobeats[...path].astro. - Prerendered routes beat on-demand routes.
- Endpoints beat pages.
- File-based routes beat redirects.
- If all else ties, alphabetical order decides.
In practice you only meet this when a static page shares a folder with a dynamic one, and rule 2 does what you’d expect.
Multilingual routing with i18n
Astro has built-in i18n routing. Configure locales once:
// astro.config.mjs
export default defineConfig({
i18n: {
locales: ['en', 'fr', 'de'],
defaultLocale: 'en',
routing: { prefixDefaultLocale: false },
},
});
Then mirror pages into locale folders: src/pages/about.astro for English at /about/, src/pages/fr/about.astro for /fr/about/. Astro.currentLocale tells a component which locale is rendering, and getRelativeLocaleUrl('fr', 'about') from astro:i18n builds links without hard-coding prefixes. Fallbacks can serve the default language when a translation is missing.
Base paths
If the site lives in a subfolder (GitHub Pages project sites, for example), set base: '/my-repo' in config. Astro prefixes its own assets, but links you write by hand need it too. Use import.meta.env.BASE_URL to build them.
Worked example: blog with tag pages and pagination
Most content sites need the same four routes: a paginated index, a post page, a page per tag, and pagination within each tag. Here’s the full set.
src/pages/blog/
├── [...page].astro # /blog/, /blog/2/, /blog/3/
├── [slug].astro # /blog/my-post/
└── tags/
└── [tag]/
└── [...page].astro # /blog/tags/astro/, /blog/tags/astro/2/
The paginated index:
---
// src/pages/blog/[...page].astro
import type { GetStaticPaths } from 'astro';
import { getCollection } from 'astro:content';
export const getStaticPaths = (async ({ paginate }) => {
const posts = (await getCollection('blog', ({ data }) => !data.draft))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
return paginate(posts, { pageSize: 12 });
}) satisfies GetStaticPaths;
const { page } = Astro.props;
---
Tag pages paginate once per tag, by returning several paginate() results flattened into one array:
---
// src/pages/blog/tags/[tag]/[...page].astro
import type { GetStaticPaths } from 'astro';
import { getCollection } from 'astro:content';
export const getStaticPaths = (async ({ paginate }) => {
const posts = (await getCollection('blog', ({ data }) => !data.draft))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
const tags = [...new Set(posts.flatMap((p) => p.data.tags))];
return tags.flatMap((tag) =>
paginate(
posts.filter((p) => p.data.tags.includes(tag)),
{ params: { tag: tag.toLowerCase().replace(/\s+/g, '-') }, props: { tag }, pageSize: 12 },
),
);
}) satisfies GetStaticPaths;
const { page, tag } = Astro.props;
---
<h1>Posts tagged “{tag}”</h1>
Slugify tag names in params (spaces and capitals make awkward URLs) and pass the original name through props for display. Every page here is static, generated once per build.
Common routing mistakes
Numbers in params. params values must be strings or undefined. Return { id: String(item.id) }, not { id: item.id }.
Forgetting a param. If the file is [category]/[slug].astro, every object from getStaticPaths() needs both category and slug.
Ids with slashes in a single-segment route. Content in subfolders gets ids like guides/setup. A [slug].astro route can’t match a slash; use [...slug].astro.
Doing expensive work outside getStaticPaths(). Code in the page frontmatter runs once per generated page. Fetch the whole collection once in getStaticPaths() and pass what each page needs as props.
Hard-coded links that ignore base. On a site deployed under a subpath, /about/ skips the base. Build links with import.meta.env.BASE_URL.
Two files claiming the same URL. src/pages/about.astro and src/pages/about/index.astro both produce /about/. Astro warns about the collision; delete one.
Routing in a theme
Komal themes keep routing predictable: one file per page type in src/pages/, collections for repeated content, and dynamic routes only where content is repeated. The pages docs show how to add or remove a page in a theme. To see real route structures for different site types, browse the themes; each preview lists the pages it includes.
FAQ
Do I need getStaticPaths() for every dynamic route?
Only for prerendered (static) routes. On-demand routes read Astro.params at request time and don’t export it.
How do I make page one of my pagination live at /blog/?
Name the file [...page].astro instead of [page].astro. Page one is then served at the parent path.
Can I generate non-HTML files like JSON or XML?
Yes. Create an endpoint such as src/pages/feed.json.ts that exports GET and returns a Response. In static builds the output is written as a file.
Why is my dynamic route returning 404 in production?
In a static build, only params returned from getStaticPaths() exist. Check that the value you’re visiting is in that list, that it’s a string, and that any trailing slash matches your trailingSlash setting.
How do I exclude a file in src/pages from routing?
Prefix the file or folder name with an underscore, like _components/ or _draft.astro.