How to generate a dynamic sitemap for your Next JS website

Adeola Adeyemo J.
3 min readDec 15, 2022

--

A short tutorial to create a dynamic sitemap (Headless CMS)

Photo by Dariusz Sankowski on Unsplash

Originally posted in my portfolio notes on July, 2021.

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>
<changefre>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 should appear in your /public folder

--

--