Sitemaps are very essential to improving SEO and in this article, explore how to create a dynamic sitemap for your Nuxt 3 project. Handles both static and dynamic routes with ease.
Get notified when we release new tutorials, lessons, and other expert Nuxt content.
Creating a well-structured sitemap is very important for SEO optimization, helping search engines discover and index your website's content efficiently. In this guide, I'll break down how to implement a dynamic sitemap generator in a Nuxt 3 application.
Before we begin, let’s understand what a sitemap is. It is an XML file that lists all the important pages on your website, helping search engines like Google understand your site structure.
There are sitemap modules like @nuxtjs/sitemap
that can help you quickly generate a sitemap for your Nuxt app. However, in this article, we’ll focus on building a custom dynamic sitemap tailored to your application’s specific needs. We’ll achieve this using Nuxt 3’s server routes along with the sitemap
npm package to dynamically generate a comprehensive sitemap.
In Nuxt 3, we can create server routes in the /server/routes
directory. Our sitemap generator is set up as sitemap.xml.js
which will automatically handle requests to /sitemap.xml
.
// server/routes/sitemap.xml.js
import { SitemapStream, streamToPromise } from 'sitemap'
import { defineEventHandler } from 'h3'
import { promises as fs } from 'fs'
The code imports essential dependencies:
SitemapStream
and streamToPromise
from the sitemap
packagedefineEventHandler
from Nuxt's H3 serverThe main server handler is created using defineEventHandler
:
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const HOSTNAME = config.public.APP_ROOT
const sitemap = new SitemapStream({
hostname: HOSTNAME,
xmlns: {
news: false,
xhtml: false,
image: false,
video: false
}
})
// we will add our URLs here...
sitemap.end()
const xml = await streamToPromise(sitemap)
event.node.res.setHeader('Content-Type', 'application/xml')
return xml
})
This creates a new SitemapStream
with the application's hostname and ends by converting the stream to XML and returning it with the proper content type.
One of the most powerful features of this implementation is its ability to automatically discover pages:
async function getPageFiles(dir, baseDir = dir) {
const entries = await fs.readdir(dir, { withFileTypes: true })
const files = await Promise.all(
entries.map(async (entry) => {
const path = join(dir, entry.name)
// Skip hidden files and node_modules
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
return []
}
// Handle directories and files...
})
)
return files.flat().filter(Boolean)
}
This function recursively scans the pages directory, finding Vue files that should be included in the sitemap. It intelligently:
[id].vue
)The implementation includes a helper function to safely add URLs to the sitemap:
const addToSitemap = (path, options = {}) => {
// Check for exclusion before any URL processing
if (shouldExclude(path)) {
console.log(`Skipping excluded path: ${path}`)
return false
}
const absoluteUrl = ensureHostname(path)
console.log(`Adding URL: ${absoluteUrl}`)
sitemap.write({
url: absoluteUrl,
...options
})
return true
}
This function ensures URLs are properly formatted with the correct hostname and checks if they should be excluded. It also supports additional options like changefreq
and priority
.
The sitemap generator isn't limited to static pages. It includes:
// Add homepage
addToSitemap('/', {
changefreq: 'daily',
priority: 1.0
})
// Add all static pages from the pages directory
const pagesDir = join(process.cwd(), 'pages')
const pageFiles = await getPageFiles(pagesDir)
If your site relies on dynamic content—such as blogs or other data fetched from an API—you’ll need to include those pages in your sitemap as well. To do this, you’ll typically fetch all relevant data and dynamically generate the corresponding sitemap entries.
Here's how you can approach it:
const response = await $fetch('/articles', {
baseURL: config.public.API_URL,
params: { size: 40 }
})
if (response.data) {
response.data.forEach(article => {
addToSitemap(`blog/${article.slug}`, {
changefreq: 'weekly',
priority: 0.7,
...(article.updated_at && { lastmod: article.updated_at })
})
})
}
The final piece of the puzzle is the ability to exclude specific routes that shouldn't appear in the sitemap:
// Routes to exclude from sitemap
const excludeRoutes = [
'success',
'thank-you',
'/register',
// and others
]
const shouldExclude = (path) => {
// Clean the path first to handle both full URLs and relative paths
const cleanPath = path
.replace(/^https?:\/\/[^\/]+/, '') // Remove hostname if present
.replace(/^\/+|\/+$/g, '') // Remove leading/trailing slashes
return excludeRoutes.some(route => {
const cleanRoute = route.replace(/^\/+|\/+$/g, '')
return cleanPath === cleanRoute
})
}
This allows you to maintain a list of routes that should be excluded from your sitemap, such as:
The shouldExclude
function cleans paths for consistent comparison, ensuring that variations like /register
and register
are treated the same.
Implementing a dynamic sitemap in Nuxt 3 provides tremendous SEO benefits. This implementation offers several advantages:
By combining these techniques, you can create a comprehensive sitemap that accurately represents your Nuxt application's structure, benefiting both search engines and users.