DEV Community

Daniel Roe
Daniel Roe

Posted on β€’ Edited on β€’ Originally published at roe.dev

4

Creating your own sitemap module for Nuxt

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:

  1. It's for a static site - so no need to fetch pages at runtime. (If this is something you need, look below instead.)

  2. 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",
      "  },",
      "})"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
    // ...
  })
})
Enter fullscreen mode Exit fullscreen mode

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('')
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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))
      })
    })
  },
})
Enter fullscreen mode Exit fullscreen mode

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'],
})
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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'],
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

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.

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

Top comments (2)

Collapse
 
kissu profile image
Konstantin BIFERT β€’

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! πŸ™πŸ»πŸ’š

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

πŸ‘‹ Kindness is contagious

Please leave a ❀️ or a friendly comment on this post if you found it helpful!

Okay