You open someone's Astro or Next.js project and there's a file called [slug].astro or a folder called [slug] with a page.tsx in it. What is that? Why the square brackets? Why is there a word inside them?
This chapter pins it down. By the end you'll know exactly what "file-based routing" means, what a slug is (and the surprising place the word comes from), what the square brackets do, the three route shapes you'll meet in real projects, and how to read the URL params at runtime in both frameworks.
What "File-Based Routing" Actually Means
Before frameworks, a router was a separate file:
// Express, the old way
app.get("/about", handler1);
app.get("/blog/:slug", handler2);
app.get("/users/:id/posts/:postId", handler3);You wrote routes explicitly, then mapped them to handlers. Simple, but tedious — every new page is a new line in the router config.
File-based routing flips it: the file system is the router. The path of a file under a special directory (src/pages/ in Astro; app/ in Next.js's App Router) is its URL.
| File path | URL it serves |
|---|---|
Astro: src/pages/about.astro | /about |
Next.js: app/about/page.tsx | /about |
Astro: src/pages/blog/index.astro | /blog |
Next.js: app/blog/page.tsx | /blog |
Astro: src/pages/blog/launch.astro | /blog/launch |
Next.js: app/blog/launch/page.tsx | /blog/launch |
That's the whole convention for static routes — files whose URLs are known at build time. Drop a new .astro or page.tsx in the right spot, refresh the dev server, and the URL works. No router config touched.
So What Is a "Slug"?
Before we touch the brackets, let's name the word.
A slug is a short, URL-safe identifier for a resource — usually a hyphenated lowercase string derived from the resource's title. For example:
- Title: "How RDAP Replaced WHOIS in 2025"
- Slug:
how-rdap-replaced-whois-in-2025 - URL:
/articles/how-rdap-replaced-whois-in-2025
The word comes from 1900s newspaper composing rooms — a slug was the strip of metal type carrying a short headline ("the slug line") used to identify which article a galley belonged to. It became "the short ID for an article," then jumped into URLs in the early CMS era. Now every modern web framework uses it as a parameter name by convention.
A slug is a kind of route parameter. It's just a parameter that happens to identify a specific resource and is human-readable. [slug] and [id] work identically as far as the framework is concerned; we use slug when the value is a human-readable identifier and id when it's an opaque key.
The Brackets: Dynamic Routes
Static routes are fine when you have five pages. They break down the moment you have fifty articles and don't want to create one page.tsx per article. The framework's answer: one file that handles many URLs, with the variable part of the URL in square brackets in the filename:
| File path | Matches | Param available as |
|---|---|---|
Astro: src/pages/blog/[slug].astro | /blog/anything | Astro.params.slug |
Next.js: app/blog/[slug]/page.tsx | /blog/anything | params.slug |
The text inside the brackets — slug, id, articleId, whatever — is just the name the param will be called when you access it in code. You pick it; the framework uses it.
So when you see this file on this very site:
website-next/src/app/tutorials/web/[slug]/page.tsxThat one file is the renderer for every URL of the form /tutorials/web/<anything>. The framework takes whatever's in that <anything> slot, sets params.slug to it, and runs the file. Different <anything> → different content → different page. Twenty-one chapters in the web series, one page.tsx.
Reading the param — Astro
---
// src/pages/blog/[slug].astro
const { slug } = Astro.params;
const article = await loadArticle(slug);
---
<html>
<body>
<h1>{article.title}</h1>
<div set:html={article.body} />
</body>
</html>The --- block is server-only (it runs at build time or per-request depending on output mode). Astro.params.slug is the value of the bracketed segment.
Reading the param — Next.js (App Router)
// app/blog/[slug]/page.tsx
interface Props {
params: Promise<{ slug: string }>;
}
export default async function BlogPage({ params }: Props) {
const { slug } = await params;
const article = await loadArticle(slug);
return (
<article>
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.body }} />
</article>
);
}In modern Next.js (15+), params is a Promise you await — that's how the framework lets it lazily provide the values during streaming render. The shape is otherwise identical.
Catch-All Routes — [...slug]
Sometimes the variable part of the URL has multiple segments: /docs/v2/api/authentication. You don't want four nested [a]/[b]/[c]/[d] directories — you want one file that captures the whole tail. That's a catch-all route, with ... (three dots) inside the brackets:
| File path | Matches | Param shape |
|---|---|---|
Astro: src/pages/docs/[...slug].astro | /docs/anything/at/any/depth | Astro.params.slug = "anything/at/any/depth" |
Next.js: app/docs/[...slug]/page.tsx | /docs/anything/at/any/depth | params.slug = ["anything","at","any","depth"] |
Note the subtle difference in the runtime value: Astro hands you the joined string; Next.js hands you an array. (Both are reasonable; you just need to know which you're in.)
Catch-all is the right pick for docs sites, hierarchical content, and anywhere the URL depth varies.
Optional catch-all — Next.js only — [[...slug]]
If you want the same file to also match the base URL (no trailing segment), Next.js gives you double brackets:
app/docs/[[...slug]]/page.tsxNow /docs (no trailing slug) AND /docs/anything/at/any/depth both render through this file. params.slug is undefined for the base case and an array otherwise. Astro doesn't have an exact equivalent; you'd add a separate src/pages/docs/index.astro for the base case.
Multiple Params
You can have more than one bracket in a path. The route param names are taken from each bracket:
| File path | URL | Params |
|---|---|---|
app/blog/[year]/[month]/[slug]/page.tsx | /blog/2026/05/launch | {year:"2026", month:"05", slug:"launch"} |
app/users/[userId]/posts/[postId]/page.tsx | /users/u_123/posts/p_456 | {userId:"u_123", postId:"p_456"} |
Same in Astro — name your brackets, access by name. Use multiple params when each segment is its own meaningful identifier; use a single catch-all when it's hierarchical depth.
The Three Shapes — Decision Table
Figure 1 — Two questions, three answers. Almost every route you'll ever write is one of these.
How Does the Framework Know Which Slugs Exist?
There are two ways the framework decides what URLs to actually render:
- At build time (static). You tell the framework "here are all the slugs that exist" and it pre-renders one HTML file per slug. Fast, free, every CDN can serve them.
- On demand (server). When a request comes in for
/blog/never-seen-this, the framework runs your file with that slug. Slower (per-request work) but doesn't need a build for each new slug.
You usually want #1 for content sites and #2 for app routes. The next chapter walks through generateStaticParams (Next.js) and getStaticPaths (Astro) — the functions that tell the framework what URLs to build.
Naming Gotchas
Three things that bite people:
- No spaces, no underscores in URL parts. Use hyphens.
[my-slug]is fine;[my slug]is broken;[my_slug]works technically but URLs typically use hyphens. - Square brackets are reserved. A literal
[in a URL needs to be percent-encoded as%5B. Don't try to use brackets for anything other than dynamic params. - Route groups are a Next.js feature using
(group)parentheses to organise files without affecting the URL —app/(marketing)/about/page.tsxserves/about, not/(marketing)/about. Useful for nesting layouts without nesting URLs.
Where This Lands on This Site
To make it concrete, here's the actual routing on the site you're reading:
| File | What it renders |
|---|---|
app/page.tsx | / — the home page |
app/tutorials/page.tsx | /tutorials — the tutorials hub |
app/tutorials/web/[slug]/page.tsx | /tutorials/web/anything — every web-series chapter (22 URLs from 1 file) |
app/tutorials/cloudflare/[slug]/page.tsx | /tutorials/cloudflare/anything — every Cloudflare-series chapter |
app/tutorials/astro-and-nextjs/[slug]/page.tsx | This page and its three siblings |
So this very chapter you're reading is served by one page.tsx, which reads params.slug = "file-based-routing-and-slug", loads the corresponding MDX file, and renders it. Same file, different content, different URL.
Mental Model — Three Sentences
- The file system is the router — a file at
src/pages/about.astroorapp/about/page.tsxserves/about, with no router config to maintain. - Square brackets in a filename make the path part variable —
[slug]matches any single segment and exposes its value as a param (Astro.params.slugorparams.slug);[...slug]matches multiple segments and the value is a string (Astro) or array (Next.js). - A "slug" is just the name we give to a human-readable URL identifier — the framework treats
[slug]and[id]identically; the difference is convention, not code.
Try It Yourself (10 Minutes)
- In any Astro or Next.js project, create
src/pages/hello/[name].astro(orapp/hello/[name]/page.tsx) and renderHello, {Astro.params.name}!(orparams.name). Visit/hello/world, then/hello/everyone— same file, different output. - Add a second segment:
src/pages/hello/[name]/[mood].astro. Visit/hello/world/happy. Read both params. - Add a catch-all:
src/pages/wild/[...path].astrorendering the joined path. Visit/wild/a/b/c/d. Confirm Astro hands you"a/b/c/d"; in Next.js you'd get["a","b","c","d"]. - Add a real article: create
src/content/articles/my-first.mdx, load it fromsrc/pages/articles/[slug].astro(orapp/articles/[slug]/page.tsx). You've just shipped your first dynamic-route content page. - Open this site's repo and look at
src/app/tutorials/web/[slug]/page.tsx. Notice that ~50 lines of code render the 22 chapters of the web series. That's the whole power of file-based dynamic routing.
Where This Lands in the Series
You can now make any URL pattern you want. The next question is: when a URL has a param, what makes it the right URL — canonical, indexable, SEO-safe? And how do you tell the framework which slugs to build at build time?
Next chapter: Canonical URLs, Route Params & SEO — the <link rel="canonical"> tag and why every page needs one, route params vs query params (and the SEO consequences of mixing them up), generateStaticParams (Next.js) and getStaticPaths (Astro), trailing slashes (the eternal war), permanent vs temporary redirects, and Open Graph + sitemap basics.
Ship your apps faster
When you're ready to publish your Swift app to the App Store, Simple App Shipper handles metadata, screenshots, TestFlight, and submissions — all in one place.
Try Simple App Shipper