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.
Top comments (2)
The implementation for Nuxt3 is indeed not ready yet: github.com/nuxt-community/sitemap-...
I feel like that with the flexibility of Nuxt3 and unJS + VueUse's tools, we don't even need to have modules by themselves. Just knowing how to plug pieces together is enough apparently. ππ»
Thanks for showing us how to make that ourselves Daniel! ππ»π
If you use Nuxt Content (Which I do) then this work as well as an option to make a sitemap
Nuxt Content - Sitemap