How to create dynamic sitemap for SEO

Contents

Intro

A sitemap.xml is an XML file that lists all the URLs on your website, guiding search engine crawlers through your content. This file is essential for SEO because it helps search engines index your pages more efficiently. But in dynamic websites where content changes frequently, maintaining a static sitemap can be tedious.

Creating a dynamic sitemap.xml via an API endpoint that queries the database and builds the sitemap on the fly can be a powerful approach:

AdvantagesDisadvantages
✅ Always Up-to-Date❌ Performance Concerns
✅ Reduced Manual Work❌ Load on Database
✅ Scalable Solution❌ Complexity
✅ Flexibility in Data Inclusion❌ SEO Impact If Slow or Unavailable

I'll leave you to decide whether you need this or not. But let's assume you do, and you already have a Node server with an array of entities (from your database) that you want to include in a dynamic sitemap.xml

Setup

I'll explain the main points, using this array of posts as an example:

const postList: Partial<Post>[] = [
  {
    id: 1,
    title: 'The Sun',
    image: 'https://example.com/images/the-sun.png',
    updatedAt: '2024-01-01T10:20:30.000Z'
  }
];

First, we need to install the XML builder and optionally dayjs (see the spoilers below for functions like getPriority and getChangeFreq)

npm i xmlbuilder dayjs

Build

Here's a simple method that creates a sitemap constant, then pushes each post as a new URL element into the file. Afterward, we'll respond to the client with the generated file.

const sitemap: XMLElement = xmlbuilder
  .create('urlset', { encoding: 'UTF-8' })
  .att('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9')
  .att('xmlns:image', 'http://www.google.com/schemas/sitemap-image/1.1');
 
postList.forEach((post: Partial<Post>) => {
  const urlElement: XMLElement = sitemap.ele('url');
 
  const loc: string = `https://example.com/posts/${post.id}`;
  const lastmod: string = post.updatedAt;
  const priority: string = getPriority(loc, post.updatedAt);
  const changefreq: string = getChangeFreq(post.updatedAt);
 
  urlElement.ele('loc', loc);
  urlElement.ele('lastmod', lastmod);
  urlElement.ele('priority', priority);
  urlElement.ele('changefreq', changefreq);
 
  if (post.image) {
    const imageElement: XMLElement = urlElement.ele('image:image');
 
    imageElement.ele('image:loc', post.image);
    imageElement.ele('image:title', post.title);
  }
});
 
const xml: string = sitemap.end({
  pretty: true
});
 
return response
  .header('Content-Disposition', 'inline')
  .type('application/xml')
  .status(200)
  .send(xml);

Here is a couple interesting points..

Priority

This method calculates a priority score for a given URL based on it's depth (position within the website structure) and update frequency.

Feel free to tune it 😉

const getPriority = (url: string, lastmod: Date): string => {
  // Default priority
  let priority: number = 0.5;
 
  // Increase priority for top-level pages
  const depth: number = url.split('/').length - 3; // Subtracting base URL parts (e.g., "https://example.com")
 
  priority -= depth * 0.1;
 
  // Calculate the time difference using dayjs
  const daysDiff: number = dayjs().diff(dayjs(lastmod), 'day');
 
  if (daysDiff < 30) {
    // Recently updated within the last month
    priority += 0.2;
  } else {
    if (daysDiff < 90) {
      // Updated within the last three months
      priority += 0.1;
    }
  }
 
  // Ensure priority is within the range [0.0, 1.0]
  return Math.max(0.0, Math.min(1.0, priority)).toFixed(2);
}

Change Frequency

This method determines the change frequency for a given URL based on its last modification date.

Feel free to tune it 😉

getChangeFreq: (lastmod: Date): string => {
  // Calculate the time difference using dayjs
  const daysDiff: number = dayjs().diff(dayjs(lastmod), 'day');
 
  // Ensure changeFreq
  if (daysDiff < 1) {
    return 'hourly';
  } else if (daysDiff < 7) {
    return 'daily';
  } else if (daysDiff < 30) {
    return 'weekly';
  } else if (daysDiff < 365) {
    return 'monthly';
  } else {
    return 'yearly';
  }
}

Merge

Our final sitemap.xml file:

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
  <url>
    <loc>https://example.com/posts/1</loc>
    <lastmod>2024-01-01T10:20:30.000Z</lastmod>
    <priority>0.50</priority>
    <changefreq>weekly</changefreq>
    <image:image>
      <image:loc>https://example.com/images/the-sun.png</image:loc>
      <image:title>The Sun</image:title>
    </image:image>
  </url>
</urlset>

Did you know that you can split sitemaps and have as many as you'd like? You can use your main sitemap.xml as an index file and add references to other parts of your site.

One of these could be your new endpoint, which will use the code I shared with you above:

<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>https://example.com/sitemaps/posts.xml</loc>
  </sitemap>
  <sitemap>
    <loc>https://example.com/sitemaps/users.xml</loc>
  </sitemap>
</sitemapindex>