Next.js Dynamic Routes — Params and Slugs

Sanjeev SharmaSanjeev Sharma
5 min read

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.

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/electronicscategory: "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>
}
  • /docsslug: undefined
  • /docs/guides/setupslug: ["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

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro