app/users/[id]/page.tsx → /users/:id
1export default async function UserPage({ params }: { params: Promise<{ id: string }> }) {
2 const { id } = await params;
3 const user = await db.user.findUnique({ where: { id } });
4 if (!user) notFound();
5 return <h1>{user.name}</h1>;
6}app/blog/[year]/[slug]/page.tsx → /blog/2026/hello-world
1type Params = Promise<{ year: string; slug: string }>;
2
3export default async function Post({ params }: { params: Params }) {
4 const { year, slug } = await params;
5 // …
6}app/docs/[...path]/page.tsx → /docs/a, /docs/a/b, /docs/a/b/c
params.path is a string array:
1const { path } = await params;
2// /docs/a/b/c → path = ["a", "b", "c"]Wrap the brackets: ...path. Matches both /docs (no segments) and /docs/a/b.
| URL | params.path |
|---|---|
/docs | undefined |
/docs/intro | ["intro"] |
/docs/api/auth | ["api", "auth"] |
generateStaticParams¶Tell Next which dynamic routes to prerender at build time:
1// app/blog/[slug]/page.tsx
2export async function generateStaticParams() {
3 const posts = await db.post.findMany({ select: { slug: true } });
4 return posts.map((p) => ({ slug: p.slug }));
5}
6
7// Optionally: error on unknown slugs (no on-demand SSG)
8export const dynamicParams = false;1import type { Metadata } from "next";
2
3export async function generateMetadata(
4 { params }: { params: Promise<{ slug: string }> }
5): Promise<Metadata> {
6 const { slug } = await params;
7 const post = await getPost(slug);
8 return {
9 title: post.title,
10 description: post.excerpt,
11 openGraph: { images: [post.coverImage] },
12 };
13}With experimental.typedRoutes: true and template literals:
1<Link href={`/blog/${post.slug}`} /> // ✅ inferred
2<Link href={`/blogs/${post.slug}`} /> // ❌ compile errorawait on params — TypeScript catches this; runtime error otherwise.[id] and [...path] in the same folder shadow each other. Be explicit.null from generateStaticParams — return an empty array to opt out of prerender, not null.