How to generate a dynamic sitemap for your Next JS website

A short tutorial to create a dynamic sitemap

Published: July 14, 2021

Updated: August 10, 2023


A sitemap will give your website pages more visibility on Google Search by defining the relationship between pages.

In this tutorial, I will walk you through how to create a static and dynamic sitemap for your Next JS website.

Static Sitemap

If you just need a simple sitemap, you can simply create a sitemap.xml.js in your pages folder and structure it in the following way:

<urset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://deolaj.com</loc>
    <changefreq>monthly</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>https://deolaj.com/notes</loc>
    <changefreq>monthly</changefreq>
    <priority>1.0</priority>
  </url>
<urlset>

Dynamic Sitemap with local pages

To create a dynamic sitemap, first create a sitemap.js file anywhere in the root of your project or in the src folder. Then add the globby library to your project to get the list of routes:

yarn add globby --dev

First, to get the page routes, we will use the globby package to extract the routes first. We exclude the Next JS default pages (_app.tsx, _document.tsx etc.), [slug].tsx, and api pages since they are not important.

const globby = require('globby');

const pages = await globby([
  'pages/**/*.tsx',
  '!pages/_*.tsx',
  '!pages/**/[slug].tsx',
  '!pages/api',
]);

Next, we get the current Date:

const currentDate = new Date().toISOString();

Next, we create the complete sitemap string

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('pages', '')
          .replace('.js', '')
          .replace('.tsx', '')
          .replace('.md', '');
        const route = path === '/index' ? '' : path;
        return `
          <url>
            <loc>${`https://deolaj.com${route}`}</loc>
            <lastmod>${currentDate}</lastmod>
            <changefreq>monthly</changefreq>
            <priority>1.0</priority>
          </url>
        `;
      })
      .join('')}
   </urlset>
`;

Next, write this file to your public folder:

const fs = require('fs');
fs.writeFileSync('public/sitemap.xml', sitemap);

Finally, put everything together

const fs = require('fs');
const globby = require('globby');

async function generateSiteMap() {
  const pages = await globby([
    'pages/**/*.tsx',
    '!pages/_*.tsx',
    '!pages/**/[slug].tsx',
    '!pages/api',
  ]);

  const currentDate = new Date().toISOString();

  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('pages', '')
            .replace('.js', '')
            .replace('.tsx', '')
            .replace('.md', '');
          const route = path === '/index' ? '' : path;
          return `
            <url>
              <loc>${`https://deolaj.com${route}`}</loc>
              <lastmod>${currentDate}</lastmod>
              <changefreq>monthly</changefreq>
              <priority>1.0</priority>
            </url>
          `;
        })
        .join('')}
    </urlset>
  `;

  fs.writeFileSync('public/sitemap.xml', sitemap);
}

generateSiteMap();

Dynamic Sitemap with remote content

In this example, I will use contentful to illustrate how to generate dynamic pages using remote content. E.g blog posts.

First, initialize the contentful client with your space id and access token

const { createClient } = require('contentful');

const client = createClient({
  space: process.env.CONTENTFUL_SPACE_ID,
  accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
});

Get the posts from contentful and their paths (slug)

const notePosts = await client
  .getEntries({ content_type: 'notes', order: 'sys.createdAt' })
  .then((response) => {
     // this is a custom utility function I created to extract the posts
     const posts = generateNotePosts(response.items);
     return posts;
});
const notePaths = notePosts.map(({ fields: { slug } }) => slug);   

Next, modify the sitemap string with a new section for remote pages

 ...
 ${notePaths
    .map((route) => {
      return `
        <url>
          <loc>${`https://deolaj.com/notes/${route}`}</loc>
          <lastmod>${currentDate}</lastmod>
          <changefreq>monthly</changefreq>
          <priority>1.0</priority>
        </url>
      `;
    })
    .join('')}
 ...

Finally, put everything together

const fs = require('fs');
const globby = require('globby');
const { createClient } = require('contentful');

async function generateSiteMap() {
  const pages = await globby([
    'pages/**/*.tsx',
    '!pages/_*.tsx',
    '!pages/**/[slug].tsx',
    '!pages/api',
  ]);

  const client = createClient({
    space: process.env.CONTENTFUL_SPACE_ID,
    accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
  });

  const currentDate = new Date().toISOString();

  const notePosts = await client
  .getEntries({ content_type: 'notes', order: 'sys.createdAt' })
  .then((response) => {
     // this is a custom utility function I created to extract the posts
     const posts = generateNotePosts(response.items);
     return posts;
   });

  const notePaths = notePosts.map(({ fields: { slug } }) => slug);

  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('pages', '')
          .replace('.js', '')
          .replace('.tsx', '')
          .replace('.md', '');
        const route = path === '/index' ? '' : path;
        return `
          <url>
            <loc>${`https://deolaj.com${route}`}</loc>
            <lastmod>${currentDate}</lastmod>
            <changefreq>monthly</changefreq>
            <priority>1.0</priority>
          </url>
        `;
      })
      .join('')}

      ${notePaths
      .map((route) => {
        return `
          <url>
            <loc>${`https://deolaj.com/notes/${route}`}</loc>
            <lastmod>${currentDate}</lastmod>
            <changefreq>monthly</changefreq>
            <priority>1.0</priority>
          </url>
        `;
      })
      .join('')}
  </urlset>
`;

  fs.writeFileSync('public/sitemap.xml', sitemap);
}

generateSiteMap();

Putting it together

To ensure that your page is built, add the following to your next.config.js

module.exports = {
  webpack: (config, { isServer }) => {
    if (isServer) {
      // Replace the path below with the `sitemap.js` file location
      require('./src/utils/sitemap');
    }

    return config;
  },
};

Run your local server and the sitemap.xml file should appear in your public folder

Contact

Are you ready to work with me?

hero illustration

I'm actively open to new opportunities and requests.

If you have a question, or just want to say hi, I'll try my best to get back to you.