Next.js Dynamic Routes — Params and Slugs
Advertisement
Next.js Dynamic Routes — Params and Slugs
Dynamic routes let you create pages from a single template with URL parameters. Combined with static generation, this creates incredibly performant sites at scale.
- Next.js Dynamic Routes — Params and Slugs
- Creating Dynamic Routes
- Multiple Dynamic Segments
- Catch-All Routes
- Static Generation with generateStaticParams
- Incremental Static Regeneration
- Dynamic Metadata
- Real-World Blog Example
- Handling Missing Routes
- Optional Dynamic Segments
- Query Parameters
- FAQ
Creating Dynamic Routes
Dynamic route segments use square brackets:
// app/blog/[slug]/page.tsx
export default function BlogPostPage({ params }: { params: { slug: string } }) {
return <h1>Blog post: {params.slug}</h1>
}
A request to /blog/hello-world passes slug: "hello-world".
Multiple Dynamic Segments
Create nested dynamic segments:
// app/[category]/[subcategory]/page.tsx
export default function Page({
params,
}: {
params: { category: string; subcategory: string }
}) {
return (
<div>
<h1>{params.category}</h1>
<h2>{params.subcategory}</h2>
</div>
)
}
/products/electronics → category: "products", subcategory: "electronics"
Catch-All Routes
Capture all remaining segments:
// app/docs/[[...slug]]/page.tsx
export default function DocsPage({ params }: { params: { slug?: string[] } }) {
if (!params.slug) {
return <div>Documentation home</div>
}
const path = params.slug.join(' / ')
return <div>Reading: {path}</div>
}
/docs→slug: undefined/docs/guides/setup→slug: ["guides", "setup"]
Static Generation with generateStaticParams
Generate pages at build time for better performance:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await db.posts.findMany()
return posts.map((post) => ({
slug: post.slug,
}))
}
export default async function BlogPostPage({
params,
}: {
params: { slug: string }
}) {
const post = await db.posts.findUnique({ where: { slug: params.slug } })
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
This generates static HTML for every blog post at build time.
Incremental Static Regeneration
Combine static generation with automatic updates:
// app/blog/[slug]/page.tsx
export const revalidate = 3600 // Revalidate every hour
export async function generateStaticParams() {
// Generate popular posts statically
const posts = await db.posts.findMany({ take: 50 })
return posts.map((post) => ({ slug: post.slug }))
}
export default async function BlogPostPage({ params }) {
const post = await db.posts.findUnique({ where: { slug: params.slug } })
if (!post) {
return <div>Post not found</div>
}
return <article>{post.title}</article>
}
New posts not in generateStaticParams generate on-demand, then cache for 1 hour.
Dynamic Metadata
Generate metadata for dynamic routes:
// app/products/[id]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata({
params,
}: {
params: { id: string }
}): Promise<Metadata> {
const product = await db.products.findUnique({ where: { id: params.id } })
return {
title: product?.name || 'Product',
description: product?.description,
openGraph: {
title: product?.name,
images: [product?.image],
},
}
}
export default async function ProductPage({ params }) {
const product = await db.products.findUnique({ where: { id: params.id } })
return <div>{product?.name}</div>
}
Real-World Blog Example
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
async function getPost(slug: string) {
const post = await db.posts.findUnique({
where: { slug },
include: {
author: true,
tags: true,
relatedPosts: true,
},
})
return post
}
export async function generateStaticParams() {
const posts = await db.posts.findMany({
select: { slug: true },
})
return posts.map((post) => ({
slug: post.slug,
}))
}
export async function generateMetadata({
params,
}: {
params: { slug: string }
}): Promise<Metadata> {
const post = await getPost(params.slug)
if (!post) {
return { title: 'Not Found' }
}
return {
title: post.title,
description: post.excerpt,
openGraph: {
type: 'article',
publishedTime: post.publishedAt,
},
}
}
export default async function BlogPostPage({ params }) {
const post = await getPost(params.slug)
if (!post) {
notFound()
}
return (
<article>
<header>
<h1>{post.title}</h1>
<p>By {post.author.name}</p>
<time>{new Date(post.publishedAt).toLocaleDateString()}</time>
</header>
<div className="content">{post.content}</div>
<aside>
<h3>Related Posts</h3>
<ul>
{post.relatedPosts.map((related) => (
<li key={related.id}>
<a href={`/blog/${related.slug}`}>{related.title}</a>
</li>
))}
</ul>
</aside>
</article>
)
}
Handling Missing Routes
Use notFound() for missing pages:
import { notFound } from 'next/navigation'
export default async function UserPage({ params }) {
const user = await db.users.findUnique({ where: { id: params.id } })
if (!user) {
notFound() // Shows not-found.tsx
}
return <div>{user.name}</div>
}
Create not-found.tsx in the same directory:
// app/users/[id]/not-found.tsx
export default function UserNotFound() {
return (
<div>
<h1>User Not Found</h1>
<a href="/users">Back to Users</a>
</div>
)
}
Optional Dynamic Segments
Make segments optional with double brackets:
// app/[[...slug]]/page.tsx
export default function Page({ params }: { params: { slug?: string[] } }) {
if (!params.slug) {
return <div>Home</div>
}
return <div>Viewing: {params.slug.join('/')}</div>
}
Both / and /any/path/structure match this route.
Query Parameters
Access query parameters separately from route params:
'use client'
import { useSearchParams } from 'next/navigation'
export default function SearchPage({ params }) {
const searchParams = useSearchParams()
const query = searchParams.get('q')
const sort = searchParams.get('sort')
return (
<div>
<h1>Results for: {query}</h1>
<p>Sorted by: {sort}</p>
</div>
)
}
FAQ
Q: Should I use static generation or ISR? A: Use static generation for sites with finite content (blogs, docs). Use ISR when content changes frequently but you still want cache benefits.
Q: Can I have both static and dynamic params? A: Yes. Routes not in generateStaticParams generate on-demand (with ISR if revalidate is set).
Q: What happens if a user requests a route not in generateStaticParams? A: If revalidate is set, it generates on-demand and caches. If not set, it renders dynamically on each request.
Master dynamic routes for scalable, performant websites.
Advertisement