Sitemaps for Next.js static sites with dynamic routes

Posted by Luke Boyle on the

I just recently re-built my Gatsby site using Next.js. I liked Gatsby for a while, however, I had a few issues:

  • the build process has always been dodgy for me,
  • the watch (i.e. gatsby start) failed after being up for a while
  • builds didn't work on Windows Linux Subsystem
  • overburdened with configuration modules
![Google's lighthouse audit result shows 99 for performance, 100 for accessibility and best practices](/web/public/images/next-sitemaps/blog-page-lighthouse.jpg)
The Lighthouse audit results after my first round of changes

The biggest selling point for me is the getStaticPaths function in the Next.js pages. Before, as a pre-build step, I was generating the entire page tree of React components using a node script. Super heavy handed, and I'm sure there's better ways to do it in Gatsby. What I'm doing now looks like this:

└── pages
    └── blog-posts
        └── [year]
            └── [month]
                └── [title].tsx

The resulting output is visible in the address bar in your browser. Blog posts routes look like: /blog-posts/2020/08/some-name


export function Post() {}

export async function getStaticPaths() {
    const blogPosts = await getBlogPosts();

    const paths =
        post => `/blog-posts/${post.year}/${post.month}/${post.title}`

    return { paths, fallback: false };

In the getStaticPaths function you return a list of new paths and Next.js automatically spits those pages out. At build time, you can then use the path parameters to fetch external data and build your components. What this means, in effect, is that your /pages folder no longer maps 1:1 to the static output. So you can't just build a sitemap off the page directory anymore.

There's a comprehensive article on the topic by Lee Robinson ( but this guide also assumes your source pages are 1:1 with the expected output. I adapted his script to build based off the folder output instead.

  1. Download required dependencies (square brackets denote optional dependencies)

yarn add -D glob [chalk] [prettier]

  1. Create sitemap script
import glob from 'glob';
import fs from 'fs';
import { red } from 'chalk';
import prettier from 'prettier';
import prettierConfig from './.prettierrc.js';

(() => {
    // default next js output is `out`
    // all the pages are guaranteed to be html
    glob('./out/**/*.html', (err, files) => {
        // If there's no files in the output, a build probably hasn't been run
        if (!files.length) {
            console.error(red('Could not find output directory'));

        const sitemap = `
        <?xml version="1.0" encoding="UTF-8"?>
        <urlset xmlns="">
                .map(page => {
                    const path = page.replace('./out', '').replace('.html', '');
                    const route = path === '/index' ? '/' : path;

                    return `
                            <loc>${`https://{Your Domain Here}${route}/`}</loc>

        // Optional: you can remove this block if you aren't using prettier
        const formatted = prettier.format(sitemap, {
            parser: 'html'

        fs.writeFileSync('./out/sitemap.xml', formatted);
  1. Add script to package.json
    "scripts": {
        "start": "next start",
        "build": "next build && yarn run build:sitemap",
        "build:sitemap": "node ./generate-sitemap.js"
    "devDependencies": {
        "chalk": "^4.1.0",
        "fs-extra": "^6.0.1",
        "glob": "^7.1.3",
        "prettier": "^1.18.2"

That's pretty much it for my implementation. You can see my sitemap here