Earlier I used to manage sitemap manually in NextJS. And I end up forgetting to update the sitemap whenever I add/remove any pages from my website. But now I don't have to remember updating the sitemap. I write dynamic sitemap code once and no need to look at it afterwards.
Here's how you can do it:
There are 2 types of routes you have to add in NextJS sitemap.
- Public routes paths
- Dynamic routes paths
Both routes should be kept in NextJS route groups. This is not a rule. We are doing this because we are going to traverse your NextJS code directory to find all the pages you want to add in sitemap. So, the traverser will be easier if you keep all the public pages in one route groups. There are 2 benefits of using this approach. Firstly, it will be better for your project folder structure. All the public pages will be inside one directory only. Also, it will make the traversal logic easier to implement.
Divide private and public pages using NextJS route groups. Once you have your folder structure like the above image, use the following function to which returns array of paths of all public routes.
import fs from "fs";
import path from "path";
export function findPublicPagesPath() {
const appDir = path.join(process.cwd(), "src", "app", "(public)"); //keep all of your pages you want to add in sitemap inside this directory
const routes: string[] = [];
function traverse(currentDir: string) {
const entries = fs.readdirSync(currentDir, {
withFileTypes: true,
});
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
traverse(fullPath);
continue;
}
if (entry.isFile() && entry.name === "page.tsx") {
const relativePath = path.relative(appDir, fullPath);
let route = relativePath
.replace(/\\/g, "/")
.replace(/\/page\.tsx$/, "")
.replace(/^page\.tsx$/, "");
// remove route groups: (marketing), (auth), etc.
route = route
.split("/")
.filter(segment => !/^\(.*\)$/.test(segment))
.join("/");
// Exclude [id] dynamic routes
if (route.includes("[") || route.includes("]")) {
continue;
}
routes.push(route ? `/${route}` : "/");
}
}
}
traverse(appDir);
return routes;
}
Example output from this function:
[ '/about', '/contact', '/' ]
For dynamic routes you have to fetch all of the slugs/IDs from your DB and pass it to sitemap.
const posts: Post[] = await fetch(
"https://jsonplaceholder.typicode.com/posts"
).then(res => res.json());
For the sake of simplicity, we are using dummy API to fetch all the dynamic routes slugs/IDs. The response from this API will look like this:
[
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
},
{
"userId": 1,
"id": 2,
"title": "qui est esse",
"body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
},
{
"userId": 1,
"id": 3,
"title": "ea molestias quasi exercitationem repellat qui ipsa sit aut",
"body": "et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut"
},
{
"userId": 1,
"id": 4,
"title": "eum et est occaecati",
"body": "ullam et saepe reiciendis voluptatem adipisci\nsit amet autem assumenda provident rerum culpa\nquis hic commodi nesciunt rem tenetur doloremque ipsam iure\nquis sunt voluptatem rerum illo velit"
},
...
]
Once you have this 2 of arrays of routes you can use this code of dynamic sitemap:
import type { MetadataRoute } from 'next'
import { findPublicPagesPath } from './findPublicPagesPath';
type Post = {
id: number;
};
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// src/app/(public) public routes
const paths = findPublicPagesPath();
const publicPagePaths: MetadataRoute.Sitemap = paths.map((path: string) => ({
url: `https://acme.com${path}`,
lastModified: new Date()
}))
// src/app/blog/[id] dynamic routes
const posts: Post[] = await fetch(
"https://jsonplaceholder.typicode.com/posts"
).then(res => res.json());
const dynamicRoutes: MetadataRoute.Sitemap = posts.map((post: { id: number }) => ({
url: `https://acme.com/blog/${post.id}`,
lastModified: new Date(),
}));
return [
...publicPagePaths,
...dynamicRoutes
]
}
This sitemap code will give you following output:
One thing to note is sitemap.js is a special Route Handler that is cached by default. So, you need to configure your caching according to it.
How you will implement caching depends upon whether you are using Caching and Revalidating (Previous Model) or Cache Components. Feel free to refer these docs and add caching as per your use cases.
Along with simply listing URLs in sitemap items, you can give additional metadata information like when was the page last modified, priority of page, changeFrequency and more. Here's a complete type definition for a sitemap item:
type Sitemap = Array<{
url: string
lastModified?: string | Date
changeFrequency?:
| 'always'
| 'hourly'
| 'daily'
| 'weekly'
| 'monthly'
| 'yearly'
| 'never'
priority?: number
alternates?: {
languages?: Languages<string>
}
}>
That's all what you have to do in a nutshell!
here's the source code



Top comments (0)