Course outline · 0% complete

0/29 lessons0%

Course overview →

[slug] routes and params

lesson 5-1 · ~10 min · 15/29

Lesson Async server components answered the data-fetching question, and the shape of that answer matters for dynamic routes.

A server component waits for data by being declared async and awaiting the fetch directly in its body. Server components are allowed to be async functions, so they simply await a fetch call or a database query inline, with no hooks involved. The useEffect dance from the React course is only needed in client components, which cannot be async.

Dynamic routes lean on this heavily, because the thing a dynamic page awaits first is the URL itself.

One file, a thousand pages

A blog has one post layout but thousands of post URLs. Creating a folder per post is obviously impossible. Instead you create one dynamic segment by wrapping a folder name in square brackets:

app/blog/[slug]/page.js

This single file serves /blog/hello-world, /blog/why-frameworks-win, and every other /blog/<anything> URL. The changing part is handed to your component as the params prop.

This should feel familiar. In Backend with Node.js you wrote app.get("/posts/:id") and read req.params.id. It is the same idea, spelled with brackets instead of a colon, and derived from the file system instead of a registration call.

app/blog/[slug]/page.js/blog/hello-world/blog/why-frameworks-win/blog/ship-itone fileevery matching URL
A dynamic segment: the single [slug] page file answers every /blog/<slug> URL, and params tells it which one was requested.

Reading params (await it!)

// app/blog/[slug]/page.js
export default async function PostPage({ params }) {
  const { slug } = await params;
  const res = await fetch(`https://api.example.com/posts/${slug}`);
  const post = await res.json();
  return <h1>{post.title}</h1>;
}

Decode it:

  • In current Next.js, params is a Promise, so you must await it before destructuring. Older tutorials show plain params.slug, which is the outdated API.
  • Visiting /blog/hello-world gives slug === "hello-world", which the component uses to fetch exactly that post.
  • One file, one fetch, infinitely many URLs.

What params resolves to

With app/shop/[productId]/page.js deployed, a user visits /shop/42. Awaiting params gives you { productId: "42" }.

Two details in that small object are worth spelling out:

  • The key matches the bracket folder name exactly. The folder is [productId], so the key is productId. Rename the folder and the key changes with it.
  • The value is always a string, taken verbatim from the URL. Even though 42 looks like a number, URLs carry text, so you get "42". Convert it yourself with Number(productId) when you need arithmetic.

slugify: making titles URL-safe

Blog posts need URL-safe slugs like the ones that fill a [slug] route. slugify(title) builds one: it lowercases the title, removes every character that is not a lowercase letter, digit, or space, trims the ends, and turns each run of spaces into a single hyphen. The regex work here is the same kind you met in Advanced JavaScript.

function slugify(title) {
  return title
    .toLowerCase()
    .replace(/[^a-z0-9 ]/g, "")
    .trim()
    .replace(/ +/g, "-");
}

console.log(slugify("Hello, Next.js!"));
console.log(slugify("  Server   Components 101  "));
console.log(slugify("10 Tips & Tricks"));

Output

hello-nextjs
server-components-101
10-tips-tricks

Why the order of steps matters

  • Lowercasing happens first so the character filter only has to allow a-z, not both cases.
  • .replace(/[^a-z0-9 ]/g, "") deletes punctuation such as the comma, the period in Next.js, and the ampersand. Note that deleting & from Tips & Tricks leaves a double space behind.
  • .trim() removes the leading and trailing whitespace that would otherwise become stray hyphens.
  • The + in / +/g collapses runs of spaces into one hyphen, which cleans up exactly the double space the ampersand removal created.