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:
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:
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.
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:
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:
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:
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:
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.