How to generate a dynamic sitemap for your Next JS website
A short tutorial to create a dynamic sitemap (Headless CMS)
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