
How does file-based routing work in Astro?
File-based routing in Astro means every supported file inside src/pages/ becomes a URL on your site, and the file's path decides what that URL is. src/pages/about.astro is served at /about, src/pages/blog/index.astro at /blog, and src/pages/blog/hello-world.md at /blog/hello-world. There is no router configuration file to maintain: add a file and the route exists, delete it and the route is gone. Square brackets in a file name, such as [slug].astro, turn a single file into a dynamic route that can produce many pages.
This convention is one of the reasons Astro projects stay easy to navigate. You can look at the folder tree and know the shape of the site. In this article you'll learn how file paths map to URLs, which file types can live in src/pages/, how to keep helper files out of the router, how dynamic routes fit in, what the build output looks like with each build.format option, how trailing slashes behave, and what the new src/fetch.ts entrypoint in Astro 7 changes (and doesn't change) about routing.
The Basic Mapping
Astro reads the src/pages/ directory and creates one route per file. The rules are simple:
- The path becomes the URL: folders become URL segments, and the file name becomes the last segment.
- index files map to their folder:
index.astroinside a folder answers at the folder's URL. - The extension is dropped:
.astro,.md,.mdxand.htmlextensions don't appear in the URL.
Here is a small project and the URLs it produces:
src/pages/index.astro -> /
src/pages/about.astro -> /about
src/pages/about/index.astro -> /about (same URL, pick one)
src/pages/blog/index.astro -> /blog
src/pages/blog/hello-world.md -> /blog/hello-world
src/pages/docs/guides/setup.astro -> /docs/guides/setup
Notice that about.astro and about/index.astro both claim /about. They're interchangeable. Use the folder version when you expect sibling pages under /about/ later, and the single file when you don't.
A Minimal Page
A page is just an Astro component that renders a full HTML document. Most real pages wrap their content in a layout so they don't repeat the <html> and <head> markup:
---
// src/pages/index.astro
import Base from "../layouts/Base.astro";
---
<Base title="Home">
<h1>Welcome</h1>
<p>Current path: {Astro.url.pathname}</p>
</Base>
The layout is an ordinary component outside src/pages/, so it never becomes a route itself:
---
// src/layouts/Base.astro
interface Props {
title: string;
}
const { title } = Astro.props;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{title}</title>
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/about/">About</a>
<a href="/blog/">Blog</a>
</nav>
<main>
<slot />
</main>
</body>
</html>
If layouts are new to you, the guide on creating reusable layouts in Astro covers them in detail.
One Astro 7 detail matters here: the Rust compiler is now the only compiler, and it requires every non-void element to be closed. An unclosed <p> or <li> that older versions silently accepted will now fail the build, so keep your page markup tidy.
Linking Between Pages
Astro has no special <Link> component. You navigate with plain <a> elements and root-relative URLs:
<a href="/blog/hello-world/">Read the first post</a>
Use a path that starts with /, not a relative file path like ../blog/hello-world.md. The link points at the URL the page is served from, not at the source file. If you configure a base in your Astro config (for example when deploying to a subfolder), include that base in your links.
Supported File Types in src/pages
Not every file in src/pages/ is treated the same way. Astro recognises these types:
| File type | What it becomes | Notes |
|---|---|---|
.astro | An HTML page | Full Astro component features |
.md | An HTML page | Use the layout frontmatter property to wrap it |
.mdx | An HTML page | Requires the @astrojs/mdx integration |
.html | An HTML page | Copied in as a page, with limited Astro features |
.js / .ts | An endpoint | Serves any data, such as JSON, XML or images |
Markdown pages are handy for one-off content like a privacy policy. The layout frontmatter property points to a layout component that wraps the rendered Markdown:
---
layout: ../../layouts/MarkdownPage.astro
title: Hello world
---
# Hello world
This page was written in **Markdown**.
The layout receives the file's frontmatter as a frontmatter prop, not as individual props, so read the title from there:
---
// src/layouts/MarkdownPage.astro
import Base from "./Base.astro";
const { frontmatter } = Astro.props;
---
<Base title={frontmatter.title}>
<article>
<slot />
</article>
</Base>
For a whole blog, though, a content collection is usually a better fit than a folder of Markdown pages, because you get schema validation and query helpers. See what content collections are in Astro for that approach.
Endpoints Live in the Same Tree
A .js or .ts file in src/pages/ is an endpoint rather than a page. Its name should include the extension of the file you want to produce, because Astro strips only the .ts part:
// src/pages/data.json.ts
import type { APIRoute } from "astro";
export const GET: APIRoute = () => {
return Response.json({ site: "example", pages: 4 });
};
This builds a file at /data.json. Since Astro 6, endpoints whose URL ends in a file extension can only be reached without a trailing slash, so link to /data.json, never /data.json/. Endpoints get their own article: how to build API endpoints in Astro.
Keeping Files Out of the Router
It's often convenient to keep a component or utility next to the page that uses it. Prefix the file or folder name with an underscore and Astro ignores it when building routes:
src/pages/
_drafts/
idea.astro (ignored)
_helpers.ts (ignored)
index.astro -> /
projects/
_ProjectCard.astro (ignored)
project-one.md -> /projects/project-one
Files with a leading _ aren't recognised by the router and don't end up in dist/. In a test build of the tree above, requesting /_drafts/idea in the dev server returned a 404, exactly as if the file didn't exist.
This is also a simple way to temporarily disable a page without deleting it: rename pricing.astro to _pricing.astro.
Static Routes and Dynamic Routes
Everything so far has been a static route: one file, one URL. Astro also supports dynamic routes, where part of the file name is a parameter in square brackets.
| Pattern | Example file | Matches |
|---|---|---|
| Static | blog/index.astro | /blog only |
| Named parameter | blog/[slug].astro | /blog/anything (one segment) |
| Several parameters | [lang]/[version]/info.astro | /en/v1/info |
| Rest parameter | docs/[...path].astro | /docs, /docs/a, /docs/a/b/c |
In the default static output, a dynamic page must export getStaticPaths() so Astro knows which values to build at build time:
---
// src/pages/dogs/[dog].astro
export function getStaticPaths() {
return [
{ params: { dog: "clifford" } },
{ params: { dog: "rover" } },
];
}
const { dog } = Astro.params;
---
<p>Good dog, {dog}!</p>
That one file generates /dogs/clifford and /dogs/rover. Pages rendered on demand skip getStaticPaths() and match any value at request time instead. Both topics have dedicated posts: dynamic routes with getStaticPaths and rest parameters for catch-all routes.
When a static route and a dynamic route could both match the same URL, Astro has a clear set of priority rules. posts/create.astro always beats posts/[id].astro for /posts/create, for example. The details are in route priority in Astro.
What the Build Output Looks Like
In static mode, each route becomes a file in dist/. The build.format option controls the shape of those files. I built the example project with each setting:
# build.format: 'directory' (default)
dist/404.html
dist/about/index.html
dist/blog/hello-world/index.html
dist/blog/index.html
dist/data.json
dist/index.html
dist/legacy/index.html
# build.format: 'file'
dist/404.html
dist/about.html
dist/blog.html
dist/blog/hello-world.html
dist/index.html
dist/legacy.html
# build.format: 'preserve'
dist/404.html
dist/about.html
dist/blog/hello-world.html
dist/blog/index.html
dist/index.html
dist/legacy.html
- directory wraps every page in its own folder with an
index.html, which gives clean URLs on almost any static host. - file writes
about.htmlstyle files. Some hosts serve these at/aboutautomatically; others need configuration. - preserve mirrors your source tree:
about.astrobecomesabout.html, whileblog/index.astrostaysblog/index.html.
The 404.astro page is always written as 404.html, which is the file most hosts look for. There's more on that in creating a custom 404 page in Astro.
Setting the Format
// astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
build: {
format: "file",
},
});
The format also affects Astro.url during the build. With directory, Astro.url.pathname includes a trailing slash (/about/). With file, it ends in .html. Keep that in mind if you build relative URLs with new URL("./something", Astro.url).
Trailing Slashes
Should /about and /about/ both work? The trailingSlash option decides how the dev server and on-demand pages match them:
- 'ignore' (default): both forms match the same route.
- 'always': only
/about/matches. In production, on-demand pages redirect the other form. - 'never': only
/aboutmatches, with the same redirect behaviour in production.
// astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
trailingSlash: "always",
build: {
format: "directory",
},
});
For prerendered pages, trailing slash handling is up to your hosting platform, because the host serves the files directly. Pairing trailingSlash: "always" with the directory format, or "never" with file, keeps development and production consistent.
Advanced Routing and src/fetch.ts in Astro 7
Astro 7 made advanced routing stable and enabled it by default. It doesn't replace file-based routing. Your src/pages/ tree still defines every route. What it adds is a hook into the request pipeline that sits in front of those routes.
By default, Astro handles each request with a built-in pipeline: trailing-slash normalisation, redirects, sessions, actions, your middleware, page rendering, i18n and caching. If you create src/fetch.ts, Astro uses it as the entrypoint instead. The simplest useful version runs the whole default pipeline with astro() and adds something around it:
// src/fetch.ts
import { FetchState, astro } from "astro/fetch";
export default {
async fetch(request: Request): Promise<Response> {
const state = new FetchState(request);
const response = await astro(state);
response.headers.set("X-Matched-Route", state.routeData?.route ?? "none");
return response;
},
};
FetchState resolves which file-based route matches the request, and state.routeData exposes it. Running this in the dev server shows the router at work:
curl -s -o /dev/null -D - http://localhost:4321/about/ | grep -i x-matched
curl -s -o /dev/null -D - http://localhost:4321/nope | grep -i x-matched
x-matched-route: /about
x-matched-route: /404
/about/ matched src/pages/about.astro, and an unknown URL fell through to the 404 route.
Three things are worth knowing before you reach for this file:
- src/fetch.ts is a reserved name: if your project already has a
src/fetch.tsfor something else, such as an API client, Astro will try to load it as the entrypoint. Rename your file, or setfetchFilein the config to another name or tonullto disable the feature. - It only runs for requests that reach the server: when I built the same project with the Node adapter, the custom header appeared on an on-demand page and on the 404 response, but not on prerendered pages, which the server sent as static files. A fully static site has no request pipeline in production at all.
- Don't authorise by pathname string: the docs warn that the incoming pathname isn't guaranteed to match the route Astro resolves internally, so a check like
pathname.startsWith("/dashboard")can be bypassed. Use middleware or a router that owns route matching for access control.
If you have an existing file you want to keep, the config change is one line:
// astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
fetchFile: null,
});
For most sites you won't need src/fetch.ts at all. File-based routing plus middleware covers the common cases, and the entrypoint is there for when you want full control over the order of things.
Common Mistakes
A few problems come up again and again with file-based routing:
- Pages outside src/pages: a component in
src/components/is never a route, no matter what it renders. - Linking to source files: write
/blog/hello-world/, not./hello-world.md. - Two files for one URL:
about.astroandabout/index.astrotogether create a conflict. Keep one. - Forgetting the underscore: a
utils.tsfile insrc/pages/is picked up as an endpoint route. Name it_utils.tsor move it tosrc/lib/. - Expecting dynamic routes to build without getStaticPaths(): in static mode, a
[param]page without it fails the build with aGetStaticPathsRequirederror, which also suggests addingexport const prerender = falseif you meant the page to be rendered on demand.
FAQ: File-Based Routing in Astro
No. Astro builds its routes from the files in src/pages. Adding a supported file creates a route and removing it deletes the route. Configuration only comes into play for things like redirects, i18n and trailing slash behaviour.
Pages always live in a pages folder inside your source directory. You can move the whole source directory with the srcDir option, but the pages folder name itself stays the same.
Prefix the file or folder name with an underscore, such as _Card.astro or _drafts/. Astro ignores those files when it builds routes, so they never appear in dist.
Both produce the /about URL. The folder version is convenient when you plan to add more pages under /about/, but you should only keep one of the two to avoid a conflict.
No. Your routes still come from src/pages. The fetch entrypoint lets you wrap or reorder the request pipeline that runs in front of those routes, and it only applies to requests the server actually handles.
The most common causes are trailing slash differences between dev and your host, and the build.format setting. Match trailingSlash to your format, and check how your host serves files like about.html versus about/index.html.
Conclusion
Astro's file-based routing turns your src/pages/ folder into your site map. Folders become URL segments, index files answer at their folder's URL, extensions disappear, and underscored files stay private. Markdown, MDX and HTML files become pages, while .js and .ts files become endpoints, all in the same tree.
Once the basics are clear, the rest builds on top: square brackets for dynamic routes, build.format and trailingSlash to shape your output, and, in Astro 7, an optional src/fetch.ts for taking control of the request pipeline. Start with a clean folder structure and most routing questions answer themselves.


