Create a Dynamic Sitemap in Next.js

A sitemap tells search engines what pages exist on your site. A dynamic sitemap builds itself as you add pages, so you never have to maintain a URL list by hand. This tutorial walks through generating a sitemap.xml at build time using Next.js (v9.5, the latest at time of writing), globby for file matching, and prettier for clean XML output.
What is a sitemap
A sitemap is a file where you specify your site pages, media, files, their relations and additional information about it. You can specify their URLs, last modified date, change frequency and priority for example. Search engines search and retrieve information from this file to crawl your site and better index it. You can check more on sitemaps.org
Sample XML sitemap file
The sitemap protocol format consists of XML tags. All data values in a Sitemap must be entity-escaped. The file itself must be UTF-8 encoded.
It must:
- begin with an opening
<urlset>tag - end with a closing
</urlset>tag. - Specify the namespace (protocol standard) within the
<urlset>tag. - include a
<url>entry for each URL, as a parent XML tag. - include a
<loc>child entry for each<url>parent tag.
All other tags are optional.
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>http://www.nunorralves.com/</loc>
<lastmod>2020-10-24</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
</urlset>All URLs in a Sitemap must be from a single host. You can check protocol here
What is a robots.txt file
A robots.txt file tells search engine crawlers which pages or files the crawler can or can't request from your site. This is used mainly to avoid overloading your site with requests.
Sample robots.txt file
Create a static file at public/robots.txt. It will define which files can be crawled and where the sitemap is located.
User-agent: *
Sitemap: https://www.nunorralves.pt/sitemap.xmlSimple as that!
Static vs Dynamic sitemap file
If your site is not frequently updated, you might consider defining and updating this file statically. On other hand, if you update it frequently or you have dozens or even hundreds of pages, you might / should consider doing it dynamically.
Let's implement a dynamic sitemap.xml generator
First, install the packages we'll need:
npm install globby prettierglobby does user-friendly glob matching across your pages directory, and prettier formats the XML output so it's human-readable.
Now, create a scripts/generate-sitemap.js file in your project root:
/* eslint-disable @typescript-eslint/no-var-requires */
const fs = require("fs");
const globby = require("globby");
const prettier = require("prettier");
(async () => {
const prettierConfig = await prettier.resolveConfig(
"../../.prettier.config.js",
);
const pages = await globby([
"src/pages/**/*{.js,.jsx,.ts,.tsx,.mdx}",
"!src/pages/_*{.js,.jsx,.ts,.tsx,.mdx}",
"!src/pages/api",
]);
const sitemap = `
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${pages
.map((page) => {
const path = page
.replace("src/pages", "")
.replace(".jsx", "")
.replace(".tsx", "")
.replace(".mdx", "")
.replace(".js", "")
.replace(".ts", "");
const route = path === "/index" ? "" : path;
const changeFreq = "<changefreq>weekly</changefreq>";
const priority = "<priority>0.8</priority>";
return `
<url>
<loc>${`https://www.nunorralves.pt${route}`}</loc>
${
path === "/index" || path === "/blog"
? changeFreq
: ""
}
${path === "/index" ? priority : ""}
</url>
`;
})
.join("")}
</urlset>
`;
const formatted = prettier.format(sitemap, {
...prettierConfig,
parser: "html",
});
fs.writeFileSync("public/sitemap.xml", formatted);
})();As you can read from above, we use globby to define the array of pages path. Then we start generating sitemap.xml according to the template described and on it, iterate over pages, creating respective URL tags. In the end, just format it according to your prettier configurations file and write the generated file to the filesystem under public folder.
To complete the implementation, go to your next.config.js file and extend webpack definition to run the created script at start:
module.exports = {
(...)
webpack: (config, { isServer }) => {
if (isServer) {
require('./scripts/generate-sitemap');
}
return config;
}
(...)
};Nothing else to do for the basic setup.
Test it!
Just start you server. If you follow this tutorial you should see a new file under public/sitemap.xml with something like below:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://www.nunorralves.pt/about</loc>
</url>
<url>
<loc>https://www.nunorralves.pt/blog</loc>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>https://www.nunorralves.pt</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://www.nunorralves.pt/blog/create-dynamic-sitemap-nextjs</loc>
</url>
<url>
<loc
>https://www.nunorralves.pt/blog/create-simple-blog-nextjs-markdown</loc
>
</url>
</urlset>Honest Take: What This Approach Doesn't Handle
This pattern works well for static sites with a known set of page files, but a few limits are worth naming before you rely on it:
- Dynamic routes are invisible. The script scans file paths - it has no way to know what slugs your
[slug].jspages will resolve to. If your blog has 50 posts from a CMS or local markdown files, they won't appear in the sitemap unless you add a separate data-fetching step. - The webpack hook is fragile. Running the script via
isServerinnext.config.jsmeans it executes during webpack initialisation - before the full Next.js runtime is ready. It's fine for a simple glob, but if the script grows to read from a database or API, you'll want to move it to a dedicated build step or apostbuildnpm script. - Hardcoded domain. The script embeds
nunorralves.ptdirectly. It won't adapt between local dev, staging, and production without environment-aware logic. - Prettier path dependency. The
../../.prettier.config.jsrelative path only works if the script is invoked from the project root. If you later change how the script is called, this breaks silently. - No incremental builds. Every run regenerates the full sitemap. For most sites (even hundreds of pages) this is irrelevant, but it's worth knowing.
For a personal blog with static pages, this is the right level of complexity. For anything larger, look at Next.js's built-in getServerSideProps sitemap pattern or dedicated packages like next-sitemap.
Wrapping Up
You now have a sitemap.xml that builds itself: add a page file, rebuild, and the sitemap updates automatically - no manual URL list maintenance.
robots.txtpoints crawlers to the sitemapglobbydiscovers all page files- The script transforms file paths into
sitemap.org-compliant XML - Prettier keeps the output clean