Tutorials Astro & Next.js Series Chapter 2

File-Based Routing + What `[slug]` Actually Means

Astro + Next.jsChapter 2 of the Astro & Next.js Series24 minMay 31, 2026Beginner

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 pathURL 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:

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 pathMatchesParam available as
Astro: src/pages/blog/[slug].astro/blog/anythingAstro.params.slug
Next.js: app/blog/[slug]/page.tsx/blog/anythingparams.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.tsx

That 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 pathMatchesParam shape
Astro: src/pages/docs/[...slug].astro/docs/anything/at/any/depthAstro.params.slug = "anything/at/any/depth"
Next.js: app/docs/[...slug]/page.tsx/docs/anything/at/any/depthparams.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.tsx

Now /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 pathURLParams
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

Loading diagram…

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:

  1. 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.
  2. 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:

Where This Lands on This Site

To make it concrete, here's the actual routing on the site you're reading:

FileWhat 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.tsxThis 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

  1. The file system is the router — a file at src/pages/about.astro or app/about/page.tsx serves /about, with no router config to maintain.
  2. Square brackets in a filename make the path part variable[slug] matches any single segment and exposes its value as a param (Astro.params.slug or params.slug); [...slug] matches multiple segments and the value is a string (Astro) or array (Next.js).
  3. 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)

  1. In any Astro or Next.js project, create src/pages/hello/[name].astro (or app/hello/[name]/page.tsx) and render Hello, {Astro.params.name}! (or params.name). Visit /hello/world, then /hello/everyone — same file, different output.
  2. Add a second segment: src/pages/hello/[name]/[mood].astro. Visit /hello/world/happy. Read both params.
  3. Add a catch-all: src/pages/wild/[...path].astro rendering 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"].
  4. Add a real article: create src/content/articles/my-first.mdx, load it from src/pages/articles/[slug].astro (or app/articles/[slug]/page.tsx). You've just shipped your first dynamic-route content page.
  5. 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.

Ch 1: Why Use a Framework?Ch 3: Canonical URLs, Route Params & SEO
WebUltimate Web Development SeriesWeb development tutorials for HTML, CSS, JavaScript, Next.js, Workers, databases, and production shipping.Production WebProduction Web Apps SeriesProduction patterns for web apps: caching, rate limiting, webhooks, queues, cron jobs, and idempotency.CloudflareCloudflare Feature FocusFocused Cloudflare tutorials for Workers, R2, Stream, Durable Objects, and edge deployment.

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
5 free articles remainingSubscribe for unlimited access