Vercel Logo

Dynamic routes & static generation

Dynamic routes let you create pages programmatically without manually creating files. With generateStaticParams, Next.js pre-renders all product pages at build time. Result: instant page loads with zero server computation.

Outcome

Create dynamic product pages at /[productId] that display all reviews using the components from Lesson 1.3. Pre-render all pages at build time.

Fast Track

  1. Create app/[productId]/page.tsx with params: Promise<{ productId: string }> and await params
  2. Add generateStaticParams() returning products.map(p => ({ productId: p.slug }))
  3. Use getProduct(productId) with try/catch and notFound(), render <Reviews product={product} />

Hands-on Exercise 1.4

Build dynamic product pages with static generation:

Requirements:

  1. Create a dynamic route at app/[productId]/page.tsx
  2. Fetch product data using getProduct(slug) from Lesson 1.2
  3. Display product name, description, and reviews
  4. Implement generateStaticParams to pre-render all 3 products
  5. Handle 404s with Next.js notFound()

Implementation hints:

  • Params are now a Promise in Next.js 15+ (use await params)
  • generateStaticParams returns array of objects with route parameters
  • Use the Reviews component from Lesson 1.3
  • The page should be a Server Component (no "use client" needed)

Understanding Dynamic Routes

File structure:

app/
├── page.tsx              # Homepage (/)
└── [productId]/
    └── page.tsx          # Dynamic route (/:productId)

Routes created:

  • /app/page.tsx
  • /mowerapp/[productId]/page.tsx (productId = "mower")
  • /ecoBrightapp/[productId]/page.tsx (productId = "ecoBright")
  • /aquaHeatapp/[productId]/page.tsx (productId = "aquaHeat")

One file generates infinite routes.

Step 1: Create Dynamic Route

Create app/[productId]/page.tsx:

app/[productId]/page.tsx
import { notFound } from "next/navigation";
import { getProduct, getProducts } from "@/lib/sample-data";
import { Reviews } from "@/components/reviews";
 
export default async function ProductPage({
  params,
}: {
  params: Promise<{ productId: string }>;
}) {
  const { productId } = await params;
 
  let product;
  try {
    product = getProduct(productId);
  } catch {
    notFound();
  }
 
  return (
    <main className="min-h-screen p-8">
      <div className="max-w-4xl mx-auto space-y-8">
        {/* Product Header */}
        <div>
          <h1 className="text-4xl font-bold">{product.name}</h1>
          <p className="text-lg text-muted-foreground mt-2">
            {product.description}
          </p>
        </div>
 
        {/* Reviews */}
        <Reviews product={product} />
      </div>
    </main>
  );
}

Key features:

  • params is a Promise (Next.js 15+ change)
  • await params to get route parameters
  • try/catch handles invalid product IDs
  • notFound() renders 404 page

Step 2: Add Static Generation

Add generateStaticParams to the same file:

app/[productId]/page.tsx
import { notFound } from "next/navigation";
import { getProduct, getProducts } from "@/lib/sample-data";
import { Reviews } from "@/components/reviews";
 
export default async function ProductPage({
  params,
}: {
  params: Promise<{ productId: string }>;
}) {
  const { productId } = await params;
 
  let product;
  try {
    product = getProduct(productId);
  } catch {
    notFound();
  }
 
  return (
    <main className="min-h-screen p-8">
      <div className="max-w-4xl mx-auto space-y-8">
        <div>
          <h1 className="text-4xl font-bold">{product.name}</h1>
          <p className="text-lg text-muted-foreground mt-2">
            {product.description}
          </p>
        </div>
 
        <Reviews product={product} />
      </div>
    </main>
  );
}
 
export function generateStaticParams() {
  const products = getProducts();
 
  return products.map((product) => ({
    productId: product.slug,
  }));
}

What generateStaticParams does:

  • Runs at build time
  • Returns array of route parameters to pre-render
  • Next.js generates HTML for each route
  • Pages load instantly (no server rendering needed)

Step 3: Add Metadata

Add dynamic metadata for SEO:

app/[productId]/page.tsx
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { getProduct, getProducts } from "@/lib/sample-data";
import { Reviews } from "@/components/reviews";
 
export default async function ProductPage({
  params,
}: {
  params: Promise<{ productId: string }>;
}) {
  const { productId } = await params;
 
  let product;
  try {
    product = getProduct(productId);
  } catch {
    notFound();
  }
 
  return (
    <main className="min-h-screen p-8">
      <div className="max-w-4xl mx-auto space-y-8">
        <div>
          <h1 className="text-4xl font-bold">{product.name}</h1>
          <p className="text-lg text-muted-foreground mt-2">
            {product.description}
          </p>
        </div>
 
        <Reviews product={product} />
      </div>
    </main>
  );
}
 
export function generateStaticParams() {
  const products = getProducts();
 
  return products.map((product) => ({
    productId: product.slug,
  }));
}
 
export async function generateMetadata({
  params,
}: {
  params: Promise<{ productId: string }>;
}): Promise<Metadata> {
  const { productId } = await params;
 
  let product;
  try {
    product = getProduct(productId);
  } catch {
    return {
      title: "Product Not Found",
    };
  }
 
  return {
    title: `${product.name} - Customer Reviews`,
    description: product.description,
  };
}

Benefits:

  • Dynamic page titles (<title>Mower3000 - Customer Reviews</title>)
  • SEO-friendly descriptions
  • Falls back gracefully for 404s

Try It

  1. Visit the homepage at http://localhost:3000

  2. Click on a product card

  3. You should see:

    • Product name as heading
    • Product description
    • All reviews displayed with ratings, avatars, timestamps
    • Proper layout with separators
  4. Test all products:

  5. Test 404 handling:

Understanding Static Generation

Build time:

pnpm build

Output shows:

Route (app)
┌ ○ /
├ ○ /_not-found
└ ● /[productId]
  ├ /mower
  ├ /ecoBright
  └ /aquaHeat

○  (Static)  prerendered as static content
●  (SSG)     prerendered as static HTML (uses generateStaticParams)

The symbol means "SSG" - statically generated using generateStaticParams. All product pages are pre-rendered at build time.

Runtime: When a user visits /mower, Next.js serves pre-built HTML instantly. No database queries, no API calls, no computation.

Static Generation Benefits

MetricDynamic (SSR)Static (SSG)
Server CPUHigh (renders every request)Zero (pre-rendered)
Response Time~100-500ms~10ms
ScalabilityLimited (server bottleneck)Infinite (CDN)
CostHigh (always computing)Low (compute once)

Best for:

  • Product pages (content changes rarely)
  • Blog posts
  • Documentation
  • Marketing pages

Not ideal for:

  • User dashboards (personalized)
  • Real-time data
  • Frequently changing content

Dynamic Route Patterns

Single parameter:

[productId] → /mower, /ecoBright, /aquaHeat

Multiple parameters:

[category]/[productId] → /electronics/mower, /appliances/aquaHeat

Catch-all:

[...slug] → /any/nested/path/works

Optional catch-all:

[[...slug]] → / and /any/path both work

Extra Credit: Custom Not Found Page

Create app/[productId]/not-found.tsx.

app/[productId]/not-found.tsx
import Link from "next/link";
 
export default function NotFound() {
  return (
    <main className="min-h-screen p-8">
      <div className="max-w-2xl mx-auto text-center space-y-4">
        <h1 className="text-4xl font-bold">Product Not Found</h1>
        <p className="text-muted-foreground">
          The product you're looking for doesn't exist.
        </p>
        <Link
          href="/"
          className="inline-block mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
        >
          Back to Products
        </Link>
      </div>
    </main>
  );
}

Now invalid product URLs show a custom 404 with a link back home.

Done-When

  • Dynamic route created at app/[productId]/page.tsx
  • Product pages display name, description, and reviews
  • generateStaticParams pre-renders all 3 products
  • 404 handling works for invalid product IDs
  • Dynamic metadata sets page titles
  • All product links from homepage work

What's Next

Your app is functionally complete with product listings, individual product pages, and reviews display. In the next lesson, you'll deploy this to Vercel and see static generation in action on a production CDN.


Sources: