NextJS is amazing for creating Dynamic pages, but what about your sitemap.xml file. This article explains how you can build a dynamic one.
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.
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
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
Happy blogging!