Next.js Dynamic Routing & Parameter Handling Complete Guide: From Basics to Type Safety
Introduction
Last week while refactoring a Next.js project, I ran into a frustrating problem—I wrote the dynamic route exactly as the documentation said, but clicking into it gave me a 404. The console was completely silent, no error messages at all, and I was completely confused. Later I discovered that Next.js 14’s App Router changed how routing parameters are accessed, and I was still using the old Pages Router approach.
Honestly, this wasn’t my first stumble with Next.js routing. From Pages Router’s getStaticPaths to App Router’s generateStaticParams, every upgrade requires relearning. When should you use dynamic routes? When should you use catch-all routes? What about optional parameters? These concepts mixed together can really get confusing.
If you’re also confused about Next.js dynamic routing, or migrating from Pages Router to App Router, this article is for you. I’ll start from the most basic dynamic routing, all the way to type safety practice techniques, using extensive real code examples to help clarify things.
What will you get after learning? A complete dynamic routing knowledge system, knowing which route type to use in various scenarios, how to correctly access parameters, and how to use TypeScript to give routing parameters type hints. No fluff—just solid code and solutions. Let’s begin.
Chapter 1: Dynamic Routing Basics (Starting from the Simplest)
What Is Dynamic Routing?
Let’s start with the most common scenario: you have a blog website, each article’s URL is /blog/articleID. If you use static routes, you’d need to create a separate page file for each article, which is obviously impractical. This is where dynamic routing comes in—one page file handles all article details.
In Next.js App Router, dynamic routing is implemented through bracket-named folders. Sounds a bit convoluted, let’s see an example:
app/
├── blog/
│ └── [slug]/
│ └── page.tsx ← This is a dynamic route
This structure matches all /blog/* paths, for example:
/blog/hello-world→slug = "hello-world"/blog/nextjs-guide→slug = "nextjs-guide"/blog/123→slug = "123"
Simplest Dynamic Route Implementation
Create app/blog/[slug]/page.tsx, write this code:
// app/blog/[slug]/page.tsx
export default function BlogPost({
params
}: {
params: { slug: string }
}) {
return (
<div>
<h1>Article Details</h1>
<p>Current article slug: {params.slug}</p>
</div>
)
}
That’s it! When a user visits /blog/hello-world, params.slug will be "hello-world".
Common beginner mistakes:
- ❌ Using filename
[slug].tsx(App Router needs folders) - ❌ Accessing
props.slugdirectly (must get throughparamsobject) - ❌ Forgetting brackets in folder name (without brackets it’s a static route)
Pages Router vs App Router Comparison
If you’ve used Pages Router before, you might find it strange: “Didn’t we write in pages/blog/[slug].tsx before?” Yes, App Router changed quite a bit:
| Feature | Pages Router | App Router |
|---|---|---|
| File Location | pages/blog/[slug].tsx | app/blog/[slug]/page.tsx |
| Parameter Access | router.query.slug or getStaticProps | params.slug |
| Type Definition | Manual definition required | Inferred through props types |
| Static Generation | getStaticPaths | generateStaticParams |
When I first migrated, the parameter access method was the hardest to get used to. Pages Router can use useRouter hook, but App Router’s Server Components can’t use hooks—only through params prop. This is because Server Components render on the server by default, there’s no client-side router object.
Practical Case: E-commerce Product Detail Page
Let’s say you’re building an e-commerce site, product detail page URL is /products/productID. Complete implementation looks like this:
// app/products/[id]/page.tsx
interface Product {
id: string
name: string
price: number
description: string
}
// Simulate fetching product from database
async function getProduct(id: string): Promise<Product | null> {
// In real projects this would be a database query or API call
const products: Product[] = [
{ id: '1', name: 'TypeScript入门书', price: 99, description: '适合初学者' },
{ id: '2', name: 'React 实战指南', price: 129, description: '从零到项目上线' }
]
return products.find(p => p.id === id) || null
}
export default async function ProductPage({
params
}: {
params: { id: string }
}) {
const product = await getProduct(params.id)
if (!product) {
return <div>Product not found</div>
}
return (
<div>
<h1>{product.name}</h1>
<p className="price">¥{product.price}</p>
<p>{product.description}</p>
</div>
)
}
Note these details:
- Component uses
asyncbecause Server Components support async - Fetch data first, then decide what to render based on results
- Handled product not found case (404 scenario)
If you want to return a real 404 page, use Next.js’s notFound function:
import { notFound } from 'next/navigation'
export default async function ProductPage({
params
}: {
params: { id: string }
}) {
const product = await getProduct(params.id)
if (!product) {
notFound() // Return 404 page
}
return (
<div>
<h1>{product.name}</h1>
{/* ... */}
</div>
)
}
This way, when users visit a non-existent product, they’ll see your custom not-found.tsx page—better user experience.
At this point, you’ve mastered basic dynamic routing. But this is just the tip of the iceberg. Next, let’s see more complex scenarios—what to do when you need to match multi-level paths.
Chapter 2: Catch-All Routes & Optional Parameters (Handling Complex Paths)
When Do You Need Catch-All Routes?
Let’s say you’re building a documentation site, URL structure is like this:
/docs/getting-started/docs/api/authentication/docs/api/database/queries/docs/guides/deployment/vercel
Path levels aren’t fixed—could be 2 levels, could be 3 or more. Regular dynamic routes can’t handle this, so you need Catch-All routes.
Catch-All Route: [...slug]
Name the folder [...slug] (three dots), can match paths of any level:
app/
├── docs/
│ └── [...slug]/
│ └── page.tsx ← Matches all paths under /docs/*
This will match:
/docs/getting-started→slug = ["getting-started"]/docs/api/authentication→slug = ["api", "authentication"]/docs/guides/deployment/vercel→slug = ["guides", "deployment", "vercel"]
Note: The slug parameter is an array, not a string!
Code Implementation: Documentation System
// app/docs/[...slug]/page.tsx
interface Doc {
title: string
content: string
}
// Get document based on path array
async function getDoc(slugArray: string[]): Promise<Doc | null> {
// Join array into path, e.g., ["api", "auth"] → "api/auth"
const path = slugArray.join('/')
// In real projects, read from file system or database
const docs: Record<string, Doc> = {
'getting-started': {
title: '快速开始',
content: '欢迎使用我们的产品...'
},
'api/authentication': {
title: 'API 认证',
content: '我们使用 JWT 进行认证...'
},
'api/database/queries': {
title: '数据库查询',
content: '使用 Prisma 查询数据库...'
}
}
return docs[path] || null
}
export default async function DocsPage({
params
}: {
params: { slug: string[] }
}) {
const doc = await getDoc(params.slug)
if (!doc) {
return <div>Document not found</div>
}
return (
<article>
<h1>{doc.title}</h1>
<div dangerouslySetInnerHTML={{ __html: doc.content }} />
{/* Breadcrumb navigation */}
<nav>
<a href="/docs">Docs</a>
{params.slug.map((segment, i) => {
const href = `/docs/${params.slug.slice(0, i + 1).join('/')}`
return (
<span key={i}>
{' / '}
<a href={href}>{segment}</a>
</span>
)
})}
</nav>
</article>
)
}
Highlights of this code:
- Use
slugArray.join('/')to join path array into string - Implemented breadcrumb navigation using
sliceto extract path prefixes - Type annotation
params: { slug: string[] }, TypeScript will check you didn’t write it wrong
Optional Catch-All Route: [[...slug]]
Sometimes you want to match both /docs and /docs/*. Regular Catch-All routes won’t match /docs (no parameters), so use Optional Catch-All route:
app/
├── docs/
│ └── [[...slug]]/
│ └── page.tsx ← Note the double brackets
This will match:
/docs→slug = undefined/docs/getting-started→slug = ["getting-started"]/docs/api/auth→slug = ["api", "auth"]
In code, you need to handle slug possibly being undefined:
// app/docs/[[...slug]]/page.tsx
export default async function DocsPage({
params
}: {
params: { slug?: string[] } // Note slug is optional
}) {
// If it's /docs homepage
if (!params.slug) {
return <div>Welcome to Documentation Center</div>
}
// Handle sub-paths
const doc = await getDoc(params.slug)
// ...
}
Common Beginner Pitfalls
Pitfall 1: Forgetting slug is an array
// ❌ Wrong approach
<h1>Current path: {params.slug}</h1> // Will display "api,authentication"
// ✅ Correct approach
<h1>Current path: {params.slug.join('/')}</h1> // Displays "api/authentication"
Pitfall 2: Wrong data structure in static generation
// ❌ Wrong approach
export function generateStaticParams() {
return [
{ slug: 'api/auth' } // This is a string, not an array!
]
}
// ✅ Correct approach
export function generateStaticParams() {
return [
{ slug: ['api', 'auth'] } // Array form
]
}
Pitfall 3: Confusing regular dynamic routes and Catch-All routes
| Route Type | Folder Naming | Match Range | Parameter Type |
|---|---|---|---|
| Dynamic Route | [slug] | /blog/123 | string |
| Catch-All | [...slug] | /docs/a/b/c (excluding /docs) | string[] |
| Optional Catch-All | [[...slug]] | /docs and /docs/a/b/c | string[] | undefined |
I mixed these three together, routes worked sometimes and didn’t other times—took me a while to discover the folder naming was wrong.
Practical Tip: Handling Special Characters
If URLs contain Chinese or special characters, remember to encode/decode:
export default async function Page({
params
}: {
params: { slug: string[] }
}) {
// URLs are automatically encoded, need to decode for normal display
const decodedSlug = params.slug.map(s => decodeURIComponent(s))
console.log(params.slug) // ["api", "%E8%AE%A4%E8%AF%81"]
console.log(decodedSlug) // ["api", "认证"]
// ...
}
At this point, you can handle various complex path structures. But there’s still a key question: when are these dynamic pages generated? Render on every request, or pre-generate at build time? This is what the next chapter covers—generateStaticParams.
Chapter 3: generateStaticParams Deep Dive (When to Use, How to Use)
Why Do You Need generateStaticParams?
Let’s say your blog has 100 articles, each article is a dynamic route /blog/[slug]. Without optimization, every user visit requires:
- Query database to get article content
- Server-side render HTML
- Return to user
This is slow and puts pressure on the server. Next.js provides a better solution—pre-render all article pages at build time, generating static HTML. This is what generateStaticParams does.
Basic Usage: Static Generation of Blog Articles
// app/blog/[slug]/page.tsx
interface Post {
slug: string
title: string
content: string
}
// Get all article slugs
export async function generateStaticParams() {
// Get all articles from database or CMS
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
// Return all possible parameter combinations
return posts.map((post: Post) => ({
slug: post.slug
}))
}
// Render article details
export default async function BlogPost({
params
}: {
params: { slug: string }
}) {
// Get article content based on slug
const post = await fetch(`https://api.example.com/posts/${params.slug}`)
.then(r => r.json())
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
What does this code do?
generateStaticParamsruns at build time, returns all article slugs- Next.js pre-renders a static HTML file for each slug
- When users visit, directly return static file—super fast
Build output:
.next/server/app/blog/
├── hello-world.html
├── nextjs-guide.html
└── typescript-tips.html
When to Use generateStaticParams?
This is the question I get asked most. Simple judgment:
✅ Scenarios suitable for generateStaticParams:
- Blog articles, news details (content relatively fixed)
- Product detail pages (limited product count, e.g., < 10000)
- Documentation pages, help centers
- User profile pages (if user count isn’t huge)
❌ Scenarios not suitable:
- Search results pages (infinite parameter combinations)
- Real-time data (stock quotes, sports scores)
- Massive UGC platforms (can’t pre-render all user pages)
- Pages that need to display different content based on login status
Advanced Usage 1: Static Generation for Catch-All Routes
For [...slug] routes, returned parameters must be arrays:
// app/docs/[...slug]/page.tsx
export async function generateStaticParams() {
// All documentation paths
const docPaths = [
['getting-started'],
['api', 'authentication'],
['api', 'database', 'queries'],
['guides', 'deployment', 'vercel']
]
return docPaths.map(slug => ({ slug }))
}
export default async function DocsPage({
params
}: {
params: { slug: string[] }
}) {
// ...
}
Note: Return format is { slug: ['api', 'auth'] }, not { slug: 'api/auth' }.
Advanced Usage 2: Multi-Parameter Routes
If a route has multiple dynamic parameters, like /shop/[category]/[productId]:
app/
├── shop/
│ └── [category]/
│ └── [productId]/
│ └── page.tsx
Write generateStaticParams like this:
// app/shop/[category]/[productId]/page.tsx
export async function generateStaticParams() {
const products = [
{ category: 'electronics', productId: 'iphone-15' },
{ category: 'electronics', productId: 'macbook-pro' },
{ category: 'books', productId: 'clean-code' },
{ category: 'books', productId: 'refactoring' }
]
return products.map(p => ({
category: p.category,
productId: p.productId
}))
}
export default async function ProductPage({
params
}: {
params: { category: string; productId: string }
}) {
return (
<div>
<h1>Category: {params.category}</h1>
<p>Product ID: {params.productId}</p>
</div>
)
}
Advanced Usage 3: On-Demand Generation (Fallback Mode)
If you have too much content (like 100,000 articles), pre-rendering everything isn’t realistic. You can only generate popular content, generate the rest on-demand:
// app/blog/[slug]/page.tsx
export const dynamicParams = true // Allow dynamic generation of non-pre-rendered pages
export async function generateStaticParams() {
// Only pre-render top 100 popular articles
const topPosts = await fetchTopPosts(100)
return topPosts.map(post => ({
slug: post.slug
}))
}
export default async function BlogPost({
params
}: {
params: { slug: string }
}) {
// Even if not pre-rendered, first visit will generate page and cache it
const post = await fetchPost(params.slug)
if (!post) {
notFound()
}
return <article>{/* ... */}</article>
}
After setting dynamicParams = true:
- Pre-rendered pages: Return immediately (fastest)
- Non-pre-rendered pages: Generate on first request, subsequent visits reuse cache
- Non-existent pages: Return 404
Common Beginner Stumbling Blocks
Question 1: When does generateStaticParams run?
Only at build time (npm run build), not on every request. So you won’t see the effect in development (npm run dev), must build first to see statically generated files.
Question 2: What if data updates?
After static generation, content is fixed. If data updates, need to rebuild and redeploy. Solutions:
- Use ISR (Incremental Static Regeneration) for scheduled updates
- Combine with
dynamicParams = truefor on-demand updates - Use
revalidateto set cache expiration time
// Regenerate page every 60 seconds
export const revalidate = 60
export default async function Page() {
// ...
}
Question 3: Why did build time get longer?
The more paths generateStaticParams returns, the longer the build time. If build times out:
- Reduce number of pre-rendered pages (only render popular content)
- Use incremental builds (Vercel/Netlify support)
- Consider on-demand generation (
dynamicParams = true)
At this point, you’ve mastered Next.js dynamic routing core usage. Last chapter, we solve a problem that troubles many people—how to give routing parameters TypeScript type hints?
Chapter 4: Routing Parameter Type Safety Practices (Say Goodbye to any)
Why Do You Need Type Safety?
Look at this code, can you spot the problem?
export default async function Page({
params
}: {
params: { slug: string }
}) {
// Assume this is a numeric ID, but type definition is string
const id = parseInt(params.slug)
if (isNaN(id)) {
// Only discover type mismatch at runtime!
return <div>Invalid ID</div>
}
// ...
}
The problem: params.slug type is string, but you actually need a number. This type mismatch can’t be caught at compile time, only fails at runtime.
Basic Type Constraints
Next.js’s params object defaults all parameters to string or string[]. You can customize types to enhance constraints:
// app/blog/[slug]/page.tsx
interface BlogParams {
slug: string
}
export default async function BlogPost({
params
}: {
params: BlogParams
}) {
// TypeScript knows params.slug is string
const post = await fetchPost(params.slug)
// ...
}
This doesn’t seem very useful, but when you have more parameters it becomes valuable:
// app/shop/[category]/[productId]/page.tsx
interface ShopParams {
category: 'electronics' | 'books' | 'clothing' // Limited to specific values
productId: string
}
export default async function ProductPage({
params
}: {
params: ShopParams
}) {
// TypeScript will check if category is an allowed value
if (params.category === 'toys') { // ❌ Compile error!
// ...
}
}
Runtime Validation: Combining with Zod
Type definitions only check at compile time; runtime can still receive illegal values. Combining with Zod for runtime validation is safer:
npm install zod
// app/products/[id]/page.tsx
import { z } from 'zod'
import { notFound } from 'next/navigation'
// Define parameter schema
const paramsSchema = z.object({
id: z.string().regex(/^\d+$/, 'Must be numeric ID')
})
export default async function ProductPage({
params
}: {
params: { id: string }
}) {
// Runtime validation
const result = paramsSchema.safeParse(params)
if (!result.success) {
notFound() // Illegal parameters directly return 404
}
const { id } = result.data
const product = await fetchProduct(parseInt(id))
// ...
}
Benefits of this approach:
- Type checking at compile time
- Runtime validation of parameter format
- Illegal requests directly return 404, won’t query database
Advanced Technique: Type-Safe generateStaticParams
generateStaticParams return value can also have type constraints:
// app/blog/[slug]/page.tsx
interface BlogParams {
slug: string
}
export async function generateStaticParams(): Promise<BlogParams[]> {
const posts = await fetchAllPosts()
return posts.map(post => ({
slug: post.slug
// If you write slug: post.id (wrong type), TypeScript will error
}))
}
export default async function BlogPost({
params
}: {
params: BlogParams
}) {
// ...
}
Practical Case: Multi-Language Blog Routing
Let’s say you’re building a multi-language blog, URL is /[locale]/blog/[slug], for example:
/zh/blog/hello-world/en/blog/hello-world
Complete type-safe implementation:
// app/[locale]/blog/[slug]/page.tsx
import { z } from 'zod'
import { notFound } from 'next/navigation'
// Supported language list
const locales = ['zh', 'en', 'ja'] as const
type Locale = typeof locales[number] // "zh" | "en" | "ja"
interface PageParams {
locale: Locale
slug: string
}
// Runtime validation schema
const paramsSchema = z.object({
locale: z.enum(locales),
slug: z.string().min(1)
})
export async function generateStaticParams(): Promise<PageParams[]> {
const posts = await fetchAllPosts()
// Generate corresponding paths for each language
return locales.flatMap(locale =>
posts.map(post => ({
locale,
slug: post.slug
}))
)
}
export default async function BlogPost({
params
}: {
params: PageParams
}) {
// Runtime validation
const result = paramsSchema.safeParse(params)
if (!result.success) {
notFound()
}
const { locale, slug } = result.data
// Get article in corresponding language
const post = await fetchPost(slug, locale)
if (!post) {
notFound()
}
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}
Advantages of this code:
Localetype limited to"zh" | "en" | "ja", errors if you write wronggenerateStaticParamsreturn type isPageParams[], ensures correct structure- Runtime Zod validation prevents illegal requests
- Entire flow from type definition to runtime validation is strict
Common Type Issue Troubleshooting
Issue 1: params type is Promise<...> what to do?
After Next.js 15, params might be async. Need to write like this:
export default async function Page({
params
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params // Await first
// ...
}
Or use sync version (if you’re sure it’s Next.js 14):
export default async function Page({
params
}: {
params: { slug: string }
}) {
// Use directly
}
Issue 2: Type hints not accurate
If TypeScript shows params as any, check:
- Is strict mode enabled in
tsconfig.json? - Are Next.js types correctly imported?
- Is file naming correct? (Must be
page.tsx)
Issue 3: Zod validation fails but want to see detailed errors
const result = paramsSchema.safeParse(params)
if (!result.success) {
console.error('Parameter validation failed:', result.error.format())
notFound()
}
Type Safety Checklist
In your project, check these points to ensure type safety:
- All dynamic route pages have
paramstype defined -
generateStaticParamsreturn type matchesparams - Critical routes use runtime validation (Zod)
- TypeScript strict mode enabled
- Complex parameters use union types or literal types
Do these, and your routing system will basically avoid type-related bugs.
Conclusion
If you’ve followed this article to this point, congratulations! You now have a complete Next.js dynamic routing knowledge system. Let’s review what you’ve learned:
✅ Basic Dynamic Routing: Use [slug] to match single-level paths, understand params parameter access
✅ Catch-All Routes: Use [...slug] to handle multi-level paths, know optional parameter usage
✅ generateStaticParams: Understand when to use, how to use, and on-demand generation strategies
✅ Type Safety Practices: Complete solution from compile-time type constraints to runtime validation
More importantly, you understand the differences between App Router and Pages Router, and won’t confuse their usage anymore. You also know when to pre-render, when to generate on-demand, and can choose appropriate solutions based on actual scenarios.
What Can You Do Next?
Immediate Practice (Don’t procrastinate):
- Create a dynamic route in your project, try
paramsparameter access - If you have multi-level path needs, try Catch-All routes
- Add TypeScript type definitions and Zod validation to your routes
Advanced Learning (Deep mastery):
- Parallel Routes: Load multiple routes on the same page (
@foldersyntax) - Intercepting Routes: Display another route without leaving current page (
(.)foldersyntax) - Route Groups: Organize routes with
(folder)without affecting URL structure - Middleware: Permission control and redirects at route level
Learning Resources (Official is most authoritative):
- Next.js Official Docs - Routing Basics
- Next.js Official Docs - Dynamic Routes
- Next.js Official Docs - generateStaticParams
- TypeScript Deep Dive - Improve TypeScript skills
Common Issues Quick Reference:
| Issue | Check Items | Solution |
|---|---|---|
| Dynamic route 404 | Folder naming, generateStaticParams | Confirm brackets correct, check static generation config |
params is any | TypeScript config | Enable strict mode, define parameter types |
| Build time too long | generateStaticParams return count | Reduce pre-rendered pages, use on-demand generation |
| Data not updating | Cache strategy | Set revalidate or dynamicParams |
Final Thoughts
Next.js routing system has changed significantly from Pages Router to App Router, and I know many people (including myself) have experienced migration pains. But once you master App Router’s way of thinking, you’ll find it’s actually more intuitive and powerful.
Dynamic routing is just one aspect of Next.js, but it’s the foundation of the entire application. Once you understand routing, concepts like data fetching, cache strategies, and middleware will come much more smoothly.
If you encounter problems in practice:
- First check the official docs’ “Troubleshooting” section
- Search related Issues in Next.js GitHub repository
- Ask in Next.js Discord community (English, but responds quickly)
Don’t be afraid to make mistakes—I also struggled through several projects before fully understanding App Router’s routing mechanism. Now you have this article as a reference, should save you a lot of detours.
Now, open your editor and start building your dynamic routes! 🚀
FAQ
How do I get dynamic route parameters?
Key points:
• params is a Promise, must await
• Use destructuring to get specific parameters
• Types need to be defined
Example:
export default async function Page({ params }) {
const { id } = await params
return <div>{id}</div>
}
Why does dynamic route return 404?
• Folder naming error (should be [id] not {id})
• Path mismatch (check URL and folder structure)
• generateStaticParams returns incomplete data
• Missing page.tsx file
Solutions:
• Check if folder naming is correct
• Confirm URL path matches folder structure
• Check generateStaticParams return value
What's the difference between catch-all and optional catch-all routes?
• Must match at least one path segment
• /posts/[...slug] matches /posts/a, but not /posts
Optional catch-all route [[...slug]]:
• Can match 0 or more path segments
• /posts/[[...slug]] matches /posts and /posts/a/b
Use cases:
• catch-all: need at least one parameter
• optional catch-all: parameters optional
How do I implement type-safe dynamic routes?
1) Define params type interface
2) Use Promise<{ params }> type
3) Use generateStaticParams return type
Example:
interface PageProps {
params: Promise<{ id: string }>
}
export default async function Page({ params }: PageProps) {
const { id } = await params
// ...
}
When should I use generateStaticParams?
Suitable scenarios:
• Know all possible parameter values
• Need to statically generate all pages
• Improve performance and SEO
Not suitable:
• Parameter values change dynamically
• Too many parameter values to enumerate
• Need real-time data
Note: Only for static generation, dynamic routes don't need it
How do I migrate dynamic routes from Pages Router?
• getStaticPaths → generateStaticParams
• context.params → params (need await)
• Return format changes from { paths, fallback } to array
Migration steps:
1) Change getStaticPaths to generateStaticParams
2) Modify parameter access (use await params)
3) Update type definitions
4) Test all routes
How do I handle multi-parameter dynamic routes?
app/posts/[category]/[id]/page.tsx
Get parameters:
export default async function Page({ params }) {
const { category, id } = await params
return <div>{category} - {id}</div>
}
generateStaticParams returns all combinations:
export async function generateStaticParams() {
return [
{ category: 'tech', id: '1' },
{ category: 'tech', id: '2' },
// ...
]
}
11 min read · Published on: Dec 25, 2025 · Modified on: Jan 22, 2026
Related Posts
Next.js E-commerce in Practice: Complete Guide to Shopping Cart and Stripe Payment Implementation
Next.js E-commerce in Practice: Complete Guide to Shopping Cart and Stripe Payment Implementation
Complete Guide to Next.js File Upload: S3/Qiniu Cloud Presigned URL Direct Upload
Complete Guide to Next.js File Upload: S3/Qiniu Cloud Presigned URL Direct Upload
Next.js Unit Testing Guide: Complete Jest + React Testing Library Setup

Comments
Sign in with GitHub to leave a comment