Creating your own sitemap module for Nuxt
Daniel Roe
Posted on November 12, 2022
When you're dotting the i's and crossing the t's of a shiny new Nuxt website, you will almost certainly want to ensure your site has a sitemap so that search engines know what pages of your site to index. At the moment, the Nuxt sitemap module hasn't yet been updated for Nuxt 3. But that shouldn't hold you back; let's make a quick-n-dirty module to generate a sitemap.
Deciding on the requirements
Here's what we need to achieve for the sitemap I have in mind:
It's for a static site - so no need to fetch pages at runtime. (If this is something you need, look below instead.)
We want both raw XML and gzipped sitemaps.
Scaffolding a module
I routinely extract boilerplate out into VS Code snippets. Here's my snippet for a module. If you want to add it into your own settings, type Cmd-Shift-P, select Snippets: Configure User Snippets
and then typescript.json (TypeScript)
.
snippets/typescript.json
{
"Nuxt Module": {
"prefix": "mod",
"body": [
"import { defineNuxtModule, useNuxt } from '@nuxt/kit'",
"",
"export default defineNuxtModule({",
" meta: {",
" name: '$1',",
" },",
" setup () {",
" const nuxt = useNuxt()",
" $2",
" },",
"})"
]
}
}
To be fair, this isn't much boilerplate, but still - it saves time.
Start by creating a new file in ~/modules/sitemap.ts
and type mod
+ Tab to fill in the scaffolding. Hey presto - we have a Nuxt module!
The good news is that Nitro stores a list of all the routes that have been prerendered; all we need to do is get this list. We can do this by hooking into nitro:init
to get access to the Nitro builder. It has its own hooks, and we can use the Nitro close
hook to output our sitemap at the very end of the build process.
~/modules/sitemap.ts
const nuxt = useNuxt()
nuxt.hook('nitro:init', nitro => {
nitro.hooks.hook('close', async () => {
const routes = nitro._prerenderedRoutes
// you might also have other logic to ensure only pages are included
?.filter(r => r.fileName?.endsWith('.html'))
.map(r => r.route)
if (!routes?.length) return
// ...
})
})
Now we just need to convert these routes into a sitemap and write it to disk. The good news is that it's not that complex of a file format, and we're not planning on taking advantage of any advanced features of the sitemap like <priority>
or <lastmod>
for our simple little static site. So something like this should work just fine:
const timestamp = new Date().toISOString()
const sitemap = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
...routes.map(route =>
[
'<url>',
` <loc>https://yourdomain.com${route}</loc>`,
` <lastmod>${timestamp}</lastmod>`,
'</url>',
].join('')
),
'</urlset>',
].join('')
Finally, all we need to do is write that to disk.
const dir = nitro.options.output.publicDir
await writeFile(join(dir, 'sitemap.xml'), sitemap)
await writeFile(join(dir, 'sitemap.xml.gz'), gzipSync(sitemap))
Here's the full module:
import { writeFile } from 'node:fs/promises'
import { gzipSync } from 'node:zlib'
import { defineNuxtModule, useNuxt } from '@nuxt/kit'
import { join } from 'pathe'
export default defineNuxtModule({
meta: {
name: 'sitemap',
},
setup() {
const nuxt = useNuxt()
nuxt.hook('nitro:init', nitro => {
nitro.hooks.hook('close', async () => {
const routes = nitro._prerenderedRoutes
?.filter(r => r.fileName?.endsWith('.html'))
.map(r => r.route)
if (!routes?.length) return
const timestamp = new Date().toISOString()
const sitemap = [
`<?xml version="1.0" encoding="UTF-8"?>`,
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`,
...routes.map(
route =>
`<url><loc>https://yourdomain.com${route}</loc><lastmod>${timestamp}</lastmod></url>`
),
`</urlset>`,
].join('')
const dir = nitro.options.output.publicDir
await writeFile(join(dir, 'sitemap.xml'), sitemap)
await writeFile(join(dir, 'sitemap.xml.gz'), gzipSync(sitemap))
})
})
},
})
Enabling the module
All you need to do to enable your new module is to add it to your nuxt.config
file.
~/nuxt.config.ts
export default defineNuxtConfig({
modules: ['~/modules/sitemap'],
})
Now you can run nuxi generate
and check your .output/public
folder to make sure that sitemap.xml
and sitemap.xml.gz
are present and correct!
A different approach for a dynamic sitemap
Alternatively, your website may be dynamic (for example, the page slugs may come from a CMS) or you may not be prerendering your routes. In this case, you can skip the module entirely.
Instead, create ~/server/routes/sitemap.xml.get.ts
and add the following:
~/server/routes/sitemap.xml.get.ts
function ()
export default defineEventHandler(async event => {
// perform async logic
const routes = await fetchMyRoutesFromCMS()
// copy the logic from the module above though you might consider,
// if relevant, using your CMS's modified date for <lastmod> instead
const timestamp = new Date().toISOString()
const sitemap = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
...routes.map(
route => [
'<url>',
` <loc>https://yourdomain.com${route}</loc>`,
` <lastmod>${timestamp}</lastmod>`,
'</url>'
].join('')
),
'</urlset>',
].join('')
setHeader(event, 'content-type', 'application/xml')
return sitemap
})
You can then prerender this, if it isn't going to change, with a line in your config file:
export default defineNuxtConfig({
nitro: {
prerender: {
routes: ['/sitemap.xml'],
},
},
})
If you need it to be dynamic but would benefit from light caching, you can use defineCachedEventHandler
instead of defineEventHandler
and Nitro will apply some optimisations for you.
Posted on November 12, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024
November 30, 2024