Static folders cover pages you know in advance, but a blog with a thousand posts cannot have a thousand folders. Dynamic routes solve this: one folder pattern handles every post, product, or user profile. In this lesson you will build dynamic segments, read their parameters, pre-render them at build time, and handle missing content with 404s.
Wrap a folder name in square brackets to make it dynamic:
app/blog/[slug]/page.jsNow /blog/hello-world, /blog/nextjs-tips, and any other /blog/anything all render this page. The matched value arrives in the params prop:
// app/blog/[slug]/page.js
export default async function BlogPostPage({ params }) {
const { slug } = await params; // params is a Promise in Next.js 15
const post = await getPost(slug);
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}Note the await: since Next.js 15, params is asynchronous. In Next.js 14 you could read params.slug directly; awaiting works in both mindsets going forward and is the convention to learn.
When the slug does not match real content, show a 404 with notFound():
import { notFound } from 'next/navigation';
const post = await getPost(slug);
if (!post) notFound();This renders the nearest not-found.js file and sends a proper 404 status code - important for SEO, since you do not want search engines indexing empty pages.
By default a dynamic route renders on demand. If you know the possible values ahead of time, generate them at build time for maximum speed:
// app/blog/[slug]/page.js
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}At build time, Next.js calls this function, gets the list of slugs, and renders a static HTML page for each one. Visitors get CDN-fast pages, and slugs not in the list can still render on demand the first time they are requested.
Segments compose naturally. An e-commerce route might look like:
app/shop/[category]/[productId]/page.jsexport default async function ProductPage({ params }) {
const { category, productId } = await params;
// fetch product by category + id
}Need to match an unknown number of segments, like documentation paths? Use a catch-all with three dots:
app/docs/[[...parts]]/page.jsThis single file can power an entire documentation tree.
URL query parameters like ?page=2 arrive through the separate searchParams prop on pages:
export default async function BlogIndex({ searchParams }) {
const { page = '1' } = await searchParams;
const posts = await getPosts({ page: Number(page) });
// ...
}Using searchParams opts the page into dynamic rendering, since the server cannot know query values at build time.
Next up: Lesson 9 leaves pages behind and builds backend API endpoints with route handlers.