🔥 AdonisJS From Scracth Early Access is Now Open!

April 05, 2025

Automatically generating sitemap in AdonisJS

I recently set out to improve the SEO of https://mezielabs.com, and one of the first steps was to generate an up-to-date sitemap. While I already had a sitemap created last year using a third-party service, I wanted to automate the process to ensure I don't forget to update it.

In this article, I’ll show how to automatically generate sitemaps in AdonisJS. While I’m using AdonisJS, the principles apply to any Node.js application.

Installing sitemap

To generate the sitemap, I’ll use a Node.js package called sitemap. Let’s start by installing it:

terminal
npm i sitemap

Creating the generate sitemap command

The logic for generating the sitemap will be wrapped inside a command, making it easier to run as needed or through a scheduler. We can create a command in AdonisJS using the make:command command:

terminal
node ace make:command GenerateSitemap

This will create a generate_sitemap.ts file inside the commands directory. Next, let’s open it and add the logic for generating the sitemap.

commands/generate_sitemap.ts
import { BaseCommand } from '@adonisjs/core/ace'
import type { CommandOptions } from '@adonisjs/core/types/ace'
import { SitemapStream } from 'sitemap'
import app from '@adonisjs/core/services/app'
import Course from '#models/course'
import { CourseStatus } from '../app/enums/course_status.js'
import Post from '#models/post'
import { createWriteStream } from 'node:fs'
import { promisify } from 'node:util'
import { finished } from 'node:stream'

export default class GenerateSitemap extends BaseCommand {
  static commandName = 'sitemap:generate'
  static description = 'Generate sitemap'

  static options: CommandOptions = {
    startApp: true,
  }

  async run() {
    try {
      const courses = await Course.query()
        .whereIn('status', [
          CourseStatus.PUBLISHED,
          CourseStatus.COMING_SOON,
          CourseStatus.EARLY_ACCESS,
        ])
        .orderBy('created_at', 'desc')
        .preload('lessons', (query) => query.where('status', true).orderBy('sort_order', 'asc'))

      const posts = await Post.query()
        .withScopes((scopes) => scopes.published())
        .orderBy('published_at', 'desc')

      const links = [
        // Homepage - highest priority
        {
          url: '/',
          lastmod: new Date().toISOString(),
          changefreq: 'daily',
          priority: 1.0,
        },
        // Main sections - high priority
        {
          url: '/courses',
          lastmod: new Date().toISOString(),
          changefreq: 'daily',
          priority: 0.9,
        },
        {
          url: '/articles',
          lastmod: new Date().toISOString(),
          changefreq: 'daily',
          priority: 0.9,
        },
        // Authentication pages - medium priority
        {
          url: '/register',
          lastmod: new Date().toISOString(),
          changefreq: 'monthly',
          priority: 0.7,
        },
        {
          url: '/login',
          lastmod: new Date().toISOString(),
          changefreq: 'monthly',
          priority: 0.7,
        },
        {
          url: '/password/reset',
          lastmod: new Date().toISOString(),
          changefreq: 'monthly',
          priority: 0.7,
        },
        // Pro and legal pages - medium priority
        {
          url: '/pro',
          lastmod: new Date().toISOString(),
          changefreq: 'weekly',
          priority: 0.8,
        },
        {
          url: '/terms',
          lastmod: new Date().toISOString(),
          changefreq: 'monthly',
          priority: 0.5,
        },
        {
          url: '/privacy',
          lastmod: new Date().toISOString(),
          changefreq: 'monthly',
          priority: 0.5,
        },
        // Community - medium priority
        {
          url: '/community',
          lastmod: new Date().toISOString(),
          changefreq: 'weekly',
          priority: 0.7,
        },
        // Course pages - high priority
        ...courses.map((course) => ({
          url: `/courses/${course.slug}`,
          lastmod: course.updatedAt.toString(),
          changefreq: 'weekly',
          priority: 0.8,
        })),
        // Course lesson pages - medium-high priority
        ...courses.flatMap(
          (course) =>
            course.lessons?.map((lesson) => ({
              url: `/courses/${course.slug}/lessons/${lesson.slug}`,
              lastmod: lesson.updatedAt.toString(),
              changefreq: 'weekly',
              priority: 0.7,
            })) || []
        ),
        // Article pages - high priority
        ...posts.map((post) => ({
          url: `/articles/${post.slug}`,
          lastmod: post.updatedAt.toString(),
          changefreq: 'weekly',
          priority: 0.8,
        })),
      ]

      const sitemap = new SitemapStream({ hostname: 'https://mezielabs.com' })
      const writeStream = createWriteStream(app.publicPath('sitemap.xml'))

      // Pipe the sitemap stream to the write stream
      sitemap.pipe(writeStream)

      // Write all links to the sitemap
      for (const link of links) {
        sitemap.write(link)
      }

      // End the sitemap stream
      sitemap.end()

      // Wait for the write stream to finish
      await promisify(finished)(writeStream)

      this.logger.success('Sitemap generated successfully')
    } catch (error) {
      this.logger.error('Failed to generate sitemap')

      throw error
    }
  }
}

First, we'll give the command a name and a description. Since this command interacts with the database, we need to inform Ace to start the application by setting startApp to true in the command options.

The core functionality resides within the run method. I start by fetching the courses and their corresponding lessons that should be included in the sitemap. Then, I do the same for articles. In addition to links for courses and articles, I also include static links such as the homepage, authentication pages, legal pages, etc. For each link, I specify its priority, update frequency, and the last modified date.

To create the sitemap, we use SitemapStream, passing it the hostname (which is https://mezielabs.com in my case). The sitemap is saved to a file named sitemap.xml located within the public directory, so we create a write stream for that. Then, we pipe the sitemap stream into the write stream. Next, we loop through the links and write each link to the sitemap. After adding all links, we end the sitemap stream and wait for the write stream to finish.

If everything goes well, a success message will be displayed; otherwise, an appropriate error message will be displayed.

We can run the command using the following command:

terminal
node ace sitemap:generate

This will generate the sitemap in the public directory.

Automating the sitemap generation

To automate the sitemap generation process, we'll use the adonisjs-scheduler package, which allows us to run the command at scheduled intervals.

First, we need to install the package:

terminal
node ace add adonisjs-scheduler

This will install the package, create a scheduler.ts file inside the start directory, and register the package inside the adonisrc.ts file.

The scheduler.ts file already contains sample commands to demonstrate how to use the scheduler. We will modify it to contain only the GenerateSitemap command:

start/scheduler.ts
import scheduler from 'adonisjs-scheduler/services/main'
import GenerateSitemap from '../commands/generate_sitemap.js'

scheduler.command(GenerateSitemap).daily()

This sets the command to run daily at midnight.

To start the scheduler, we can use the scheduler:run command:

terminal
node ace scheduler:run

Conclusion

That’s how to automatically generate a sitemap for an AdonisJS application using the sitemap package. I provided one method for utilising the sitemap package; be sure to check the package’s documentation for additional usage options.

If you found this article helpful and want to learn AdonisJS by building a real-world application, consider checking out my AdonisJS from Scratch course.

Chimezie Enyinnaya
Chimezie Enyinnaya
Author
Share: