How to build a dynamic sitemap for your Next.js project
At the time of this writing, there are official docs on building a sitemap for your Next.js project that uses the newer app router pattern.
However, they're on the sparse side, and there's more than a little confusion going around about how to pull this off based on the npm packages and GitHub issues I encountered when doing my own research.
When you're finished this post, you'll have the knowledge and code you need to build a dynamic sitemap
for your Next.js project that includes both 'static' routes such as /about
or /home
and dynamic routes such as /blog/[id]/page.tsx
or /posts/[slug]/page.js
.
You can also feel free to skip ahead to the implementation below. This is what I used to get a working sitemap that included all the routes I cared about and, more importantly, was accepted by Google Search's console / robots:
Key things to understand
Here's the things I needed to know upfront when I wanted to build my own sitemap:
The Next.js file to route convention is supported
You can use the file-convention approach, meaning that a file that lives at src/app/sitemap.(js|ts)
Your file should export a default function - conventionally named sitemap
. I've demonstrated this in the
implementation example below.
You can test your sitemap locally
To test your sitemap, you can use curl like curl http://localhost:3000/sitemap.xml > /tmp/sitemap.xml
and I recommend redirecting
the output to a file as shown so that you can examine it more closely.
At a minimum, all of the URLs you want search engines and other readers of sitemaps to be aware of need to be present in this file
and wrapped in <loc>
or location tags.
If your sitemap is missing key URLs you're looking to get indexed, there's something wrong in your logic causing your routes to be excluded.
You can do print debugging and add logging statments throughout you sitemap file so that when you're running npm run dev
and you curl
your sitemap, you can see the output in your terminal to help you diagnose what's going on.
Next.js dynamic and static sitemap.xml example
The following implementation creates a sitemap out of my portfolio project.
It works by defining the root directory of the project (src/app
), and then reading all the directories it finds there, applying the following rules:
- If the sub-directory is named
blog
orvideos
, it is a dynamic route, which needs to be recursively read for individual posts - If the sub-directory is not named
blog
orvideos
, it is a static or top-level route, and processing can stop after obtaining the entry name. This will be the case for any top-level pages such as/photos
or/subscribe
, for example.
Note that if you're copy-pasting this into your own file, you need to update:
- your
baseUrl
variable, since it's currently pointing to my domain - the names of your dynamic directories. For my current site version, I only have
/blog
and/videos
set up as dynamic routes - Your excludeDirs. I added this because I didn't want my API routes that support backend functionality in my sitemap
import fs from 'fs';
import path from 'path';
const baseUrl = process.env.SITE_URL || 'https://zackproser.com';
const baseDir = 'src/app';
const dynamicDirs = ['blog', 'videos']
const excludeDirs = ['api'];
function getRoutes() {
const fullPath = path.join(process.cwd(), baseDir);
const entries = fs.readdirSync(fullPath, { withFileTypes: true });
let routes = [];
entries.forEach(entry => {
if (entry.isDirectory() && !excludeDirs.includes(entry.name)) {
// Handle 'static' routes at the baseDir
routes.push(`/${entry.name}`);
// Handle dynamic routes
if (dynamicDirs.includes(entry.name)) {
const subDir = path.join(fullPath, entry.name);
const subEntries = fs.readdirSync(subDir, { withFileTypes: true });
subEntries.forEach(subEntry => {
if (subEntry.isDirectory()) {
routes.push(`/${entry.name}/${subEntry.name}`);
}
});
}
}
});
return routes.map(route => ({
url: `${baseUrl}${route}`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 1.0,
}));
}
function sitemap() {
return getRoutes();
}
export default sitemap;
That's it! Save this code to your src/app/sitemap.js
file and test it as described above.
I hope this was helpful, and if it was, please consider subscribing to my newsletter below! 🙇