Next.js 15: Dynamic routes and Static Site Generation (SSG)
Explore Next.js dynamic routing and Static Site Generation (SSG) through practical examples.
Daniel Chorągwicki
Frontend Developer
2024-12-17
#Frontend
In this article
Intro
Defining dynamic routes
Catch-all segments
Optional catch-all segments
Generating static pages
Conclusion
Intro
One of the key elements of any website or web application is routing. It enables users to navigate between different views and pages. Routing can be static or dynamic. In the case of static routing, the paths are predefined and do not change. Dynamic routing, on the other hand, is the practice of creating paths based on dynamic data and user interactions.
In this article, you will learn how to use dynamic routes in Next.js by utilizing the app router and which conventions to follow. We will also cover more advanced topics such as catch-all segments, static page generation (SSG), and the use of the generateStaticParams
function.
Request a free Next.js consultation
Facing technological challenges? Contact us for a free consultation in just 1 step!
Defining dynamic routes
Next.js allows the creation of dynamic paths using a convention with square brackets. Importantly (and this is a change compared to version 12 and its Pages Router), paths are created solely through folders.
1 2 3 4
app/ └── blog/ └── [slug]/ └── page.tsx
In this case, the [slug]
folder is a dynamic segment of the article page path and is passed to the component as params
with a name corresponding to the folder's name.
1 2 3 4 5
example url: `/blog/introduction-to-next-js` params: { slug: 'introduction-to-next-js' } example url: `/blog/developing-a-great-website-in-next-js` params: { slug: 'developing-a-great-website-in-next-js' }
1 2 3 4 5 6 7
// src/app/blog/[slug]/page.tsx export default function Article({ params }: { params: Promise<{ slug: string }> }) { const slug = (await params).slug; return <h1>Article slug: {slug}</h1>; }
Parameters are passed not only to page as mentioned above but also to layout, route, as well as to functions like generateMetadata and generateStaticParams.
USEPARAMS
The above example pertains to server components and functions executed on the server side. If you want to use a parameter in a client component, you can use the useParams
hook.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
'use client' import { useParams } from 'next/navigation' export const ExampleClientComponent = () => { const params = useParams<{ category: string; slug: string }>() // Route -> /blog/[category]/[slug] // URL -> /blog/next-js/introduction-to-next-js // `params` -> { category: 'next-js', slug: 'introduction-to-next-js' } console.log(params) // ... }
NESTED DYNAMIC ROUTES
Dynamic paths can be nested as needed. Below is an example of a folder structure for a blog category page with the path /blog/frontend/2
:
1 2 3 4 5
app/ └── blog/ └── [category]/ └── [pageNumber]/ └── page.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// src/app/blog/[category]/[pageNumber]/page.tsx export default function ArticleList({ params, }: { params: Promise<{ category: string; pageNumber: string }>; }) { const category = (await params).category; const pageNumber = (await params).pageNumber; return ( <> <h1>List category: {category}</h1> <p>Current page: {pageNumber}</p> </> ); }
Catch-all segments
Let's expand our example of an article list by adding a sub-category page. All blog paths will include a list of articles with pagination. We want the pagination to be reflected in the path. Here are possible examples of the sub-pages:
- /blog/2
- /blog/frontend/2
- /blog/frontend/react/4
Using nesting, the file structure might look like this:
1 2 3 4 5 6 7 8 9 10
app/ └── blog/ ├── [pageNumber]/ │ └── page.tsx └── [category]/ ├── [pageNumber]/ │ └── page.tsx └── [subcategory]/ └── [pageNumber]/ └── page.tsx
Assuming that each sub-page includes filters, a list, and pagination, a potential issue is that each page.tsx file may render a similar view, leading to excessive complexity and maintenance difficulties.
With Next.js, we can use the Catch-all Segments technique to handle all dynamic segments of the path with a single page.tsx
file. We achieve this by adding an ellipsis before the name of the dynamic segment.
1 2 3 4
app/ └── blog/ └── [...slug]/ └── page.tsx
Parameters in the path will be passed to the file in the following way:
1 2 3 4 5 6 7 8
example url: `/blog/3` params: { slug: ['3'] } example url: `/blog/frontend/2` params: { slug: ['frontend', '2'] } example url: `/blog/frontend/react/4` params: { slug: ['frontend', 'react', '4'] }
Here's an example of the Catch-all Segment component with simplified handling of slug
, assuming that the last segment of the path is the page number for pagination:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// src/app/blog/[...slug]/page.tsx export default function ArticleList({ params, }: { params: Promise<{ slug: string[] }>; }) { const slug = (await params).slug let currentPage; slug.forEach((segment, index) => { if (index + 1 === slug.length && typeof segment === 'number') { currentPage = segment; } else { // Category and subcategory handling } }); // ... }
Optional catch-all segments
In the example above, we assume that a sub-page is displayed only if the final segment is a page number, so the default blog page would have the path /blog/1
. In this case, the path /blog
would return an error indicating that the page was not found.
If you want the /blog
path to display the default list of articles on the first page of pagination, you can use Optional Catch-all Segments by adding extra square brackets to the dynamic segment.
1 2 3 4
app/ └── blog/ └── [[...slug]]/ └── page.tsx
At the moment, page.tsx
also handles the /blog
path without any parameters (by default showing the first page of pagination).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// src/app/blog/[[...slug]]/page.tsx export default function ArticleList({ params, }: { params: Promise<{ slug?: string[] }>; }) { const slug = (await params).slug let currentPage = 1; slug?.forEach((segment, index) => { // ... }); // ... }
Request a free Next.js consultation
Facing technological challenges? Contact us for a free consultation in just 1 step!
Generating static pages
Dynamic pages are generated by fetching data from the backend. In the case of the Next.js framework, this is achieved by creating an asynchronous server component page
and fetching data using the fetch
API. Since the component renders on the server side, the request is made directly within the component body.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// src/app/blog/[category]/[subcategory]/page.tsx export default async function ArticleList({ params, }: { params: Promise<{ category: string; subcategory: string }>; }) { const category = (await params).category const subcategory = (await params).subcategory const articles = await fetch( `[...]/articles?category=${category}&subcategory=${subcategory}`, ).then((res) => res.json()); // ... }
In this case (excluding Next.js caching mechanisms, which you can read about here), the request to the backend will be made each time a user requests our page. Therefore, the user will have to wait until the page renders on the server side to receive clean HTML with the page content.
To avoid this waiting time, we can use the static site generation method to render the subpage not on user demand, but at the time of building the application. With this method, the server will already have pre-rendered HTML files, and the user will not experience delays related to server-side requests.
To achieve this, we need to implement the generateStaticParams
function. We can do this in the page.tsx
or layout.tsx
file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// src/app/blog/[category]/[subcategory]/page.tsx export function generateStaticParams() { return [ { category: "frontend", subcategory: "next-js" }, { category: "frontend", subcategory: "react" }, { category: "frontend", subcategory: "framer-motion" }, { category: "backend", subcategory: "php" }, { category: "backend", subcategory: "node-js" }, ]; } export default async function ArticleList({ params, }: { params: Promise<{ category: string; subcategory: string }>; }) { // ... }
Above, we defined an array of objects with category
and subcategory
. During the build, they will be passed as params
to the ArticleList
component, and then fetched via the API to generate 5 pre-rendered HTML files.
GENERATING ONLY PART OF THE PAGES
If there are other categories not included in the explicitly defined parameters above, they will be rendered server-side on demand, just as when generateStaticParams
is not used.
However, if we want to limit the number of subpages to only the 5 defined above, we can use a special route segment configuration option called dynamicParams
, which makes any parameters outside those generated by generateStaticParams
return a 404 error.
1 2 3 4 5 6 7
// src/app/blog/[category]/[subcategory]/page.tsx export const dynamicParams = false; export function generateStaticParams() { // ... }
ASYNCHRONOUS generateStaticParams
If we want to generate static parameters on the server side using dynamic data from the backend, we can use generateStaticParams
as an asynchronous function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// src/app/blog/[category]/[subcategory]/page.tsx export async function generateStaticParams() { const subcategories = await fetch(`[...]/subcategories`).then((res) => res.json(), ); return subcategories.map((subcategory) => { return { category: subcategory.parent.slug, subcategory: subcategory.slug, }; }); } // ...
A downside of this approach – especially with a large number of subpages – can be a long build time. The Next.js creators addressed this by reducing compilation time through intelligent data fetching. Requests in generateStaticParams
are automatically memoized, meaning that requests with the same arguments in different generateStaticParams
will only be executed once.
generateStaticParams IN LAYOUT
Optionally, generateStaticParams
accepts parameters returned from generateStaticParams
invoked in a parent component. Importantly, this function is executed in the child component for each parameter generated in the parent.
Let’s consider a hypothetical situation where we want to create a layout with a different primary color depending on the category. In the page for the list, we want to fetch articles filtered by subcategories. In this case, we can split the parameter generation into two places.
In layout.tsx
, we fetch the categories:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
// src/app/blog/[category]/layout.tsx export async function generateStaticParams() { const categories = await fetch(`[...]/categories`).then((res) => res.json(), ); return categories.map((category) => { return { category: category.slug, }; }); } export default async function Layout({ params, }: { params: Promise<{ category: string }>; }) { const category = (await params).category const color = COLORS[category]; // ... }
In page.tsx
, for the subcategories, we fetch articles for the list using the categories that were previously generated:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// src/app/blog/[category]/[subcategory]/page.tsx export async function generateStaticParams(params: { category }) { const subcategories = await fetch( `[...]/subcategories?category=${category}`, ).then((res) => res.json()); return subcategories.map((subcategory) => { return { subcategory: subcategory.slug, }; }); } export default async function ArticleList({ params, }: { params: Promise<{ category: string; subcategory: string }>; }) { // ... }
Conclusion
The article discussed basic conventions as well as more advanced techniques for working with dynamic routes in Next.js 15. Understanding this topic and leveraging Next.js routing capabilities allows for building scalable web applications and complex websites. By using methods to generate dynamic subpages at build time, we can significantly improve loading times and enhance SEO.
Daniel Chorągwicki
Frontend Developer
Share this post
Related posts
Want to light up your ideas with us?
Kickstart your new project with us in just 1 step!
Dev and Deliver
sp. z o.o. sp. k.
- Józefitów 8
- 30-039 Cracow, Poland
Address
- 9452214307
- 368739409
- 94552994
NIP
Regon
KRS
Our services
Proud Member of