Dynamic routes
Every post has its own address: /posts/hello-nextjs, /posts/server-components. You cannot create a folder for each post, so you use a dynamic segment — a folder whose name is in square brackets. It matches any value in that part of the address.
The blog data
First let us move the posts into a separate file. Create lib/posts.ts in the project root:
// lib/posts.ts
export type Post = {
slug: string;
title: string;
body: string;
};
const posts: Post[] = [
{ slug: 'hello-nextjs', title: 'Hello, Next.js', body: 'The first post of the blog.' },
{ slug: 'server-components', title: 'Server components', body: 'How they work.' },
];
export async function getPosts(): Promise<Post[]> {
return posts;
}
export async function getPost(slug: string): Promise<Post | undefined> {
return posts.find((post) => post.slug === slug);
}The functions are asynchronous although they only read an array for now: later they will query a database, and the page code will not change. slug is the short name of a post used in the address.
The [slug] folder
Create the app/posts/[slug] folder with a page.tsx inside:
// app/posts/[slug]/page.tsx
import { getPost } from '@/lib/posts';
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
return <p>Post not found</p>;
}
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}Open /posts/hello-nextjs — the heading "Hello, Next.js" appears. The address /posts/abc shows "Post not found".
How params works
Next.js passes the page a params prop — an object with parts of the address. The key name matches the folder name: the [slug] folder gives params.slug. In Next.js 16 params is a promise, so you read it with await and declare the page component async. The server components lesson explains why a page can be asynchronous.
Instead of a hand-written type you can use the ready-made PageProps<'/posts/[slug]'> helper: Next.js generates it itself during npm run dev and npm run build, and it needs no import. The course writes the type by hand so you can see what is inside.
The list with links
The list page now takes data from the same file:
// app/posts/page.tsx
import Link from 'next/link';
import { getPosts } from '@/lib/posts';
export default async function PostsPage() {
const posts = await getPosts();
return (
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link href={`/posts/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
);
}The links on /posts now lead to real posts.
Other kinds of segments
| Folder | Matches | Value |
|---|---|---|
[slug] | /posts/a | 'a' |
[...slug] | /docs/a/b | ['a', 'b'] |
[[...slug]] | /docs and /docs/a/b | undefined or an array |
Common mistake
Reading params.slug without await, as in articles about Next.js 14. In Next.js 15 this still worked with a warning, but Next.js 16 removed synchronous access: params.slug is undefined, and the page cannot find the post. Always write const { slug } = await params.