~/TechPurAI
~/tutorials/nextjs-from-scratch/dynamic-routes-and-generatestaticparams
intermediate·part 6 of 22·2 min read

Dynamic routes and generateStaticParams

Updated Aug 16, 2026JavaScript · Next.js

/posts/hello-app-router and /posts/server-components-explained shouldn't need two separate page.tsx files — they're the same template, rendering different data. A dynamic route is one file that matches every URL in that shape.

The [slug] folder

text
app/
    posts/
        page.tsx              →  /posts
        [slug]/
            page.tsx           →  /posts/hello-app-router, /posts/anything-here

Square brackets in a folder name mark it as a dynamic segment — [slug] matches literally any single path segment in that position, and whatever was actually in the URL gets passed to the page as a prop.

Reading the param

tsx
// app/posts/[slug]/page.tsx
import { getAllPosts } from "@/lib/posts";
import { notFound } from "next/navigation";

export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = getAllPosts().find((p) => p.slug === slug);

  if (!post) notFound();

  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.date}</time>
      <p>{post.content}</p>
    </article>
  );
}

params is a Promise (as of Next.js 15) that resolves to an object keyed by the dynamic segment's name — slug, matching the folder name [slug] exactly. notFound(), imported from next/navigation, immediately renders the nearest not-found.tsx (part 8 adds one) instead of continuing — the right response for a slug that matches nothing, rather than letting the page render with post as undefined and crashing on post.title.

Pre-rendering every post at build time

tsx
export async function generateStaticParams() {
  const posts = getAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

Without this, [slug] still works, but Next.js renders each post's page on demand, the first time it's requested. generateStaticParams tells Next.js every valid value of slug ahead of time, at build — so every post page gets pre-rendered to static HTML during next build, served instantly with no per-request rendering cost at all. The array it returns has to use the exact same key as the dynamic segment (slug here) for Next.js to match it up correctly.

Linking to it

tsx
// app/posts/page.tsx — already correct since part 5
<Link href={`/posts/${post.slug}`}>{post.title}</Link>

Nothing changes here — the list page has been linking to this exact URL shape since part 5, before the route that handles it even existed.

Common mistake

Forgetting generateStaticParams and assuming every page in the project gets pre-rendered automatically. Only generateStaticParams-covered dynamic routes are known at build time; without it, [slug] pages render on-demand per request instead — functional, but not pre-rendered, and worth knowing which one a given route is actually doing.

Next: what a visitor sees while a page is fetching data — right now, nothing at all until it's fully ready.

VK

Vijay Kumar

Founder of TechPurAI — writing hands-on tutorials and honest tool breakdowns.

LinkedIn ↗
← previous5. Fetching data in Server Componentsnext →7. Loading UI with loading.tsx and Suspense