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
- Create
app/[productId]/page.tsxwithparams: Promise<{ productId: string }>andawait params - Add
generateStaticParams()returningproducts.map(p => ({ productId: p.slug })) - Use
getProduct(productId)with try/catch andnotFound(), render<Reviews product={product} />
Hands-on Exercise 1.4
Build dynamic product pages with static generation:
Requirements:
- Create a dynamic route at
app/[productId]/page.tsx - Fetch product data using
getProduct(slug)from Lesson 1.2 - Display product name, description, and reviews
- Implement
generateStaticParamsto pre-render all 3 products - Handle 404s with Next.js
notFound()
Implementation hints:
- Params are now a Promise in Next.js 15+ (use
await params) generateStaticParamsreturns 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/mower→app/[productId]/page.tsx(productId = "mower")/ecoBright→app/[productId]/page.tsx(productId = "ecoBright")/aquaHeat→app/[productId]/page.tsx(productId = "aquaHeat")
One file generates infinite routes.
Step 1: Create Dynamic Route
Create 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:
paramsis a Promise (Next.js 15+ change)await paramsto get route parameterstry/catchhandles invalid product IDsnotFound()renders 404 page
Step 2: Add Static Generation
Add generateStaticParams to the same file:
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:
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
-
Visit the homepage at http://localhost:3000
-
Click on a product card
-
You should see:
- Product name as heading
- Product description
- All reviews displayed with ratings, avatars, timestamps
- Proper layout with separators
-
Test all products:
-
Test 404 handling:
- Visit http://localhost:3000/invalid-product
- Should show Next.js 404 page
Understanding Static Generation
Build time:
pnpm buildOutput 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
| Metric | Dynamic (SSR) | Static (SSG) |
|---|---|---|
| Server CPU | High (renders every request) | Zero (pre-rendered) |
| Response Time | ~100-500ms | ~10ms |
| Scalability | Limited (server bottleneck) | Infinite (CDN) |
| Cost | High (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.
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
generateStaticParamspre-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:
Was this helpful?