Next.js Catch-all Routes
In modern web applications, flexible routing is essential for handling different URL patterns efficiently. Next.js provides a powerful feature called catch-all routes that allows you to match dynamic routes with variable path segments. This feature is particularly useful when you need to capture an unknown number of URL segments.
Introduction to Catch-all Routes
Catch-all routes in Next.js enable you to create dynamic routes that can match any number of path segments. Unlike standard dynamic routes that capture a single segment (like [id]
), catch-all routes can capture multiple segments, making them incredibly versatile for complex routing scenarios.
This feature is valuable when building:
- Documentation sites with nested categories
- Blog platforms with hierarchical content
- E-commerce sites with nested category structures
- Multi-level dashboards
How Catch-all Routes Work
In Next.js, you can create catch-all routes by using square brackets with three dots before the parameter name: [...paramName]
. This syntax tells Next.js to capture all the remaining URL segments and make them available as an array through the params
object.
Basic Syntax
To create a catch-all route file, name your file with the following pattern:
pages/posts/[...slug].js // Next.js Pages Router
app/posts/[...slug]/page.js // Next.js App Router
Implementing Catch-all Routes
Using the Pages Router
Let's create a simple example using the Pages Router approach:
// pages/posts/[...slug].js
import { useRouter } from 'next/router'
export default function Post() {
const router = useRouter()
const { slug } = router.query
return (
<div>
<h1>Post</h1>
<p>Path segments: {slug ? slug.join('/') : 'Loading...'}</p>
{slug && (
<ul>
{slug.map((segment, index) => (
<li key={index}>{segment}</li>
))}
</ul>
)}
</div>
)
}
With this component, when a user visits:
/posts/2020/01/hello-world
The slug
parameter will contain the array: ['2020', '01', 'hello-world']
Using the App Router
Here's how you can implement the same functionality using the App Router:
// app/posts/[...slug]/page.js
export default function Post({ params }) {
const { slug } = params
return (
<div>
<h1>Post</h1>
<p>Path segments: {slug.join('/')}</p>
<ul>
{slug.map((segment, index) => (
<li key={index}>{segment}</li>
))}
</ul>
</div>
)
}
Optional Catch-all Routes
Next.js also supports optional catch-all routes, which will match even if there are no path segments. These are created by adding double brackets around your parameter:
pages/posts/[[...slug]].js // Pages Router
app/posts/[[...slug]]/page.js // App Router
With optional catch-all routes:
/posts
- Will match (slug will be an empty array)/posts/2020
- Will match (slug will be['2020']
)/posts/2020/01/hello-world
- Will match (slug will be['2020', '01', 'hello-world']
)
Example with Optional Catch-all Route
// pages/posts/[[...slug]].js
import { useRouter } from 'next/router'
export default function Post() {
const router = useRouter()
const { slug } = router.query
if (!slug) {
return <div>Loading...</div>
}
return (
<div>
<h1>{slug.length === 0 ? 'All Posts' : 'Post Details'}</h1>
{slug.length === 0 ? (
<p>Browse all posts</p>
) : (
<>
<p>Path segments: {slug.join('/')}</p>
<ul>
{slug.map((segment, index) => (
<li key={index}>{segment}</li>
))}
</ul>
</>
)}
</div>
)
}
Practical Examples
Example 1: Documentation Site
For a documentation site with nested categories and articles:
// pages/docs/[...slug].js
import { useRouter } from 'next/router'
import DocLayout from '../../components/DocLayout'
// Imagine this function fetches content based on path segments
async function fetchDocContent(slugArray) {
// In a real app, this would fetch from a CMS or database
return {
title: `Documentation for ${slugArray.join(' > ')}`,
content: `This is the content for ${slugArray.join('/')}`,
}
}
export default function DocPage() {
const router = useRouter()
const { slug } = router.query
const [content, setContent] = useState(null)
useEffect(() => {
if (slug) {
fetchDocContent(slug).then(setContent)
}
}, [slug])
if (!slug || !content) return <div>Loading...</div>
return (
<DocLayout>
<h1>{content.title}</h1>
<div className="breadcrumbs">
<a href="/docs">Docs</a>
{slug.map((segment, i) => (
<span key={i}>
{' > '}
<a href={`/docs/${slug.slice(0, i + 1).join('/')}`}>{segment}</a>
</span>
))}
</div>
<div className="content">
{content.content}
</div>
</DocLayout>
)
}
Example 2: E-commerce Product Categories
For an e-commerce site with nested product categories:
// pages/products/[...categories].js
import { useRouter } from 'next/router'
import ProductListing from '../../components/ProductListing'
export default function CategoryPage() {
const router = useRouter()
const { categories } = router.query
if (!categories) return <div>Loading...</div>
const currentCategory = categories[categories.length - 1]
const isSubcategory = categories.length > 1
return (
<div>
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/products">All Products</a></li>
{categories.map((category, index) => (
<li key={index}>
<a href={`/products/${categories.slice(0, index + 1).join('/')}`}>
{category}
</a>
</li>
))}
</ol>
</nav>
<h1>{currentCategory} Products</h1>
{isSubcategory && (
<p>Browsing subcategory of {categories[categories.length - 2]}</p>
)}
<ProductListing categoryPath={categories.join('/')} />
</div>
)
}
Data Fetching with Catch-all Routes
Catch-all routes work great with Next.js data fetching methods. Here's an example using getStaticProps
and getStaticPaths
to pre-render dynamic routes:
// pages/blog/[...slug].js
export default function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
export async function getStaticProps({ params }) {
// In a real app, you would fetch this data from an API
const { slug } = params
// Example: fetch post data based on slug
const post = await fetchPostBySlug(slug)
return {
props: {
post,
},
revalidate: 60, // Re-generate the page every 60 seconds if requested
}
}
export async function getStaticPaths() {
// In a real app, you might fetch a list of all possible paths
const posts = await fetchAllPosts()
// Transform posts into paths for catch-all routes
const paths = posts.map(post => ({
params: {
// Split the URL path into segments
slug: post.urlPath.split('/'),
},
}))
return {
paths,
fallback: 'blocking', // Show a loading state for paths not generated at build time
}
}
// Mock functions for illustration
async function fetchPostBySlug(slug) {
return {
title: `Post about ${slug.join(' ')}`,
content: `<p>This is content for ${slug.join('/')}</p>`,
}
}
async function fetchAllPosts() {
return [
{ urlPath: '2023/01/hello-world' },
{ urlPath: 'tech/javascript/async' },
// more posts...
]
}
Best Practices for Catch-all Routes
-
Use optional catch-all routes when appropriate: If you want to match the base path too (
/posts
), use optional catch-all routes with double brackets. -
Carefully plan your routing hierarchy: Make sure catch-all routes don't conflict with other more specific routes.
-
Validate parameters: Always check that the provided path segments are valid before using them.
-
Create intuitive URL structures: Even though you can handle any structure, keep URLs meaningful for users.
-
Watch out for performance: When pre-rendering with
getStaticPaths
, avoid generating too many combinations of paths. -
Add clear breadcrumb navigation: For deeply nested routes, help users understand where they are.
Common Challenges and Solutions
Challenge 1: Route Conflicts
If you have both specific routes and catch-all routes, the specific routes should come first in the file system.
For example, if you have:
pages/posts/create.js
(specific route)pages/posts/[...slug].js
(catch-all route)
Next.js will match /posts/create
to the specific route and not the catch-all.
Challenge 2: Handling 404s
When using catch-all routes, you need to handle invalid paths yourself:
export default function Post({ params }) {
const { slug } = params
// Check if the requested path actually exists in your data
const postExists = checkIfPostExists(slug)
if (!postExists) {
// You can either:
// 1. Return a custom 404 component
return <Custom404 path={slug.join('/')} />
// 2. Or redirect to 404 page (in Pages Router)
// useEffect(() => {
// router.push('/404')
// }, [])
// 3. Or in App Router, you can throw a not found error
// notFound()
}
// Continue with your component if the post exists...
}
Summary
Catch-all routes are a powerful feature in Next.js that allow you to:
- Handle dynamic routes with multiple path segments
- Create flexible URL structures for your applications
- Build complex hierarchical navigation systems
- Capture multiple URL parameters in a single route
By using the [...paramName]
syntax for required catch-all routes or [[...paramName]]
for optional catch-all routes, you can build sophisticated routing patterns that meet the needs of modern web applications.
Additional Resources
- Official Next.js Documentation on Dynamic Routes
- Next.js App Router Documentation
- Next.js Pages Router Documentation
Exercise Ideas
- Build a simple wiki application that uses catch-all routes to display nested content.
- Create a portfolio website with nested project categories using catch-all routes.
- Implement breadcrumb navigation for a catch-all route system.
- Build a file explorer interface that displays content based on nested folder paths.
- Create a multi-level comment system where URLs represent the comment hierarchy.
With catch-all routes, you can build more flexible and powerful applications that handle complex URL structures elegantly. Remember that the key to effective routing is designing intuitive URL patterns that make sense to your users.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)