Dynamic routes and generateStaticParams
/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
app/
posts/
page.tsx → /posts
[slug]/
page.tsx → /posts/hello-app-router, /posts/anything-hereSquare 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
// 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
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
// 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.
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.