Joe Gilmore

17 min read

NextJS Sitemap with Dynamic Content

NextJS is amazing for creating Dynamic pages, but what about your sitemap.xml file. This article explains how you can build a dynamic one.

NextJS Sitemap with Dynamic Content

NextJS and Dynamic Sitemap.xml file.

I absolutely love NextJS, as a React developer it allows you to rapidly build your website/application much quicker than a bare-bones React app, and with it's built in routing system creating dynamic pages is a walk in the park.

A small issue I recently ran into however was on this website while building my dynamic blog pages, I use Markdown files to generate each blog page - and I needed a way of having a sitemap.xml file that picked up these pages dynamically as well, on top of that I wanted my sitemap.xml file to also show the correct Date in the <lastmod></lastmod> tag whenever I edit one of my markdown files.

NextJS allows us to easily create static files by placing them into the /public/ folder, so what we are going to do here is utilize webpack to generate our /public/sitemap.xml file on either our yarn dev or yarn build commands locally, and then it can easily be pushed to our repo ready to be built server side.

NextJS Config Update

First we need to look at updating our next.config.js file with the following (See START and END):

const nextConfig = {
  reactStrictMode: true,
  // START
  webpack: (config, { isServer }) => {
    if (isServer) {
      require("./lib/sitemap-generator");
    }
    return config;
  },
  // END
}
module.exports = nextConfig

The Sitemap Generator

Now create a new file called sitemap-generator.js and place the following contents into it like this:

const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');

const fsPromises = fs.promises;
const SITE_ROUTE = 'https://www.mywebsite.com';

const postsDirectory = path.join(process.cwd(),'markdown-blogs')

const getBlogsFiles = () => {
    return fs.readdirSync(postsDirectory)
}
const getBlogData = ( postIdentifier ) => {
    const postSlug = postIdentifier.replace(/\.md$/,'') // removes file extension
    const filePath = path.join(postsDirectory, `${postSlug}.md`)
    const fileContent = fs.readFileSync(filePath,'utf-8')
    const { data, content } = matter(fileContent)
    const postData = { slug: postSlug , ...data, content, }
    return postData
}
const getAllBlogs = () => {
    const postFiles = getBlogsFiles();
    const allBlogs = postFiles.map(postFile => {
        return getBlogData(postFile)
    })

    const sortedBlogs = allBlogs.sort( (blogA , blogB ) => blogA.date > blogB.date ? -1 : 1 )
    return sortedBlogs
}
    
const getFileLastMod = async (PAGE ) => {
    try{
        const filePath = path.join(process.cwd(),PAGE)
        const stats = await fsPromises.stat(filePath);
        return await new Date(stats.mtime).toISOString();
    }catch(err){
        console.log('🤬',err);
        return new Date().toISOString();
    }
    
}

const regularPages = async (route) => {
    const pages  = {
        'pages/index.tsx' : '/',
        // ... add other static pages here... e.g.
        'pages/about.tsx' : '/about',
        'pages/contact.tsx' : '/contact',
        'pages/blogs/index.tsx' : '/blogs',
    }
    const output = await Object.keys(pages).map(async (file) => {
        const path = pages[file]
        const lastmod = await getFileLastMod(file);
        if(lastmod){
            return `
                <url>
                <loc>${route + path}</loc>
                <lastmod>${lastmod}</lastmod>
                <changefreq>monthly</changefreq>
                <priority>1.0</priority>
                </url>
            `
        }else{
            return `<!-- No file exists for : ${file} (${path}) -->`
        }
    })
    return await Promise.all(output);
}

const listBlogsSitemaps = async (route) => {
    const blogs = getAllBlogs();
    const formatted = await blogs.map( async ( blog ) => {
        return `<url>
                <loc>${route}/blogs/${blog.slug}</loc>
                <lastmod>${await getFileLastMod(`markdown-blogs/${blog.slug}.md`)}</lastmod>  
                <changefreq>monthly</changefreq>
                <priority>1.0</priority>
            </url>`;
        });
    return await Promise.all(formatted);
}  

const generateSitemap = async () => {
    fs.readdirSync(path.join(process.cwd(), 'markdown-blogs'), 'utf8');
    const regPages = await (await regularPages(SITE_ROUTE)).join('')
    const blogs = await (await listBlogsSitemaps(SITE_ROUTE)).join('');
    const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
        <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
            ${regPages}
            ${blogs}
        </urlset>
    `;
    fs.writeFileSync('public/sitemap.xml', sitemap)
    return {
        props: {
        },
    };
};
generateSitemap();

Please note - annoyingly for now the sitemap-generator.js file must be JavaScript and not Typescript - this is due to the NextJS next.config.js file itself being JavaScript. There is more info about this here - this means that some functions are repeated in stead of re-used from our blogs-util.ts if you've been following along to my other article Markdown Blogs with Next JS and Typescript

The 2 main functions in this file are first the regularPages() function, which contains an object with File paths along with the route name, these are our static files we wish to add to our sitemap.

The 2nd function of concern is the listBlogsSitemaps() function - this file reads our Markdown blog files, and creates the xml for the sitemap dynamically.

Now when you either run yarn dev or yarn build the sitemap.xml file will be generated and placed into the /public/sitemap.xml folder ready to be served by https://www.yourwebsite.com/sitemap.xml along with the correct tags for each of your blog articles.

Happy blogging!