Nitro Hooks
Nitro hooks can be added to modify the output of your sitemaps at runtime.
'sitemap:input'
Type: async (ctx: { event: RequestEvent; urls: SitemapUrlInput[]; sitemapName: string }) => void | Promise<void>
Triggers once the raw list of URLs is collected from sources.
This hook is best used for inserting new URLs into the sitemap.
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('sitemap:input', async (ctx) => {
// SitemapUrlInput is either a string
ctx.urls.push('/foo')
// or an object with loc, changefreq, and priority
ctx.urls.push({
loc: '/bar',
changefreq: 'daily',
priority: 0.8,
})
})
})'sitemap:resolved'
Type: async (ctx: { event: RequestEvent; urls: ResolvedSitemapUrl[]; sitemapName: string }) => void | Promise<void>
Triggered once the final structure of the XML is generated, provides the URLs as objects.
With experimentalStreaming and production caching enabled, the finalized URL plan is cached. This hook runs on a plan cache miss or stale-while-revalidate refresh rather than on every sitemap response.
For new URLs it's recommended to use sitemap:input instead. Use this hook for modifying entries or removing them.
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('sitemap:resolved', async (ctx) => {
// single sitemap example - just add the url directly
ctx.urls.push({
loc: '/my-secret-url',
changefreq: 'daily',
priority: 0.8,
})
// multi sitemap example - filter for a sitemap name
if (ctx.sitemapName === 'posts') {
ctx.urls.push({
loc: '/posts/my-post',
changefreq: 'daily',
priority: 0.8,
})
}
})
})'sitemap:index-resolved'
Type: async (ctx: { event: RequestEvent; sitemaps: { sitemap: string, lastmod?: string }[] }) => void | Promise<void>
Triggered once the final structure of the sitemap index is generated, provides the sitemaps as objects.
In a sitemap index, lastmod records when the sitemap file changed. Page modification dates belong to the URL entries inside that file. Use this hook to add accurate dates to generated sitemap entries. For chunked sitemaps, fetch the metadata in one query and map it by filename:
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('sitemap:index-resolved', async ({ sitemaps }) => {
// Your application should return W3C date strings keyed by sitemap filename.
const lastmodByFile = await getSitemapLastmods()
for (const entry of sitemaps) {
const filename = new URL(entry.sitemap).pathname.split('/').pop()!
const lastmod = lastmodByFile[filename]
if (lastmod)
entry.lastmod = lastmod
}
})
})Avoid fetching the generated sitemap files from this hook. Query the database or metadata store that records when they changed instead.
The hook can also add another sitemap to the index:
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('sitemap:index-resolved', ({ sitemaps }) => {
sitemaps.push({
sitemap: 'https://mysite.com/my-sitemap.xml',
lastmod: '2026-07-20',
})
})
})'sitemap:sitemaps-resolved'
Type: async (ctx: { event: RequestEvent; sitemaps: Record<string, SitemapDefinition> }) => void | Promise<void>
Runs before the sitemap index is built and before child sitemaps are served. Add definitions to ctx.sitemaps to register sitemaps at runtime, or delete a key to remove one (static definitions included).
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('sitemap:sitemaps-resolved', async ({ sitemaps }) => {
sitemaps['games-0'] = {
sitemapName: 'games-0',
sources: ['/api/__sitemap__/games?chunk=0'],
}
})
})Constraints:
- the hook starts from a fresh copy of the sitemap config on every run: register every sitemap you want each time, removal is just not registering it
- definitions take the same fields as
nuxt.config(sources,urlsincluding functions,include,exclude,defaults,chunks/chunkSize); if bothsourcesandurlsare set,sourceswins - other module settings (
autoLastmod,sortEntries, ...) stay global - registered sitemaps need the default
sitemapsPathPrefix; with/orfalse, routes only exist for build-time names - with
cacheMaxAgeSecondsset in production, the resolved sitemap list is cached for the same window as the sitemaps themselves; dev and prerender run the hook per request ctx.eventbelongs to whichever request triggered the hook; with caching on, requests to the same host share one resolved config, so keep registration independent of request headers
See the runtime registration guide for chunking strategies, removal, and caching behavior.
'sitemap:output'
Type: async (ctx: { event: RequestEvent; sitemap: string; sitemapName: string }) => void | Promise<void>
Triggered before the sitemap is sent to the client. It provides the sitemap as an XML string.
When experimentalStreaming is enabled, the XML string is created lazily. Reading or replacing ctx.sitemap buffers the complete XML response. A hook that only observes the event or sitemap name preserves streaming serialization. With debug enabled, check X-Sitemap-Render-Mode for stream or buffered-hook.
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('sitemap:output', async (ctx) => {
// append a comment credit to the footer of the xml
ctx.sitemap = `${ctx.sitemap}\n<!-- Sitemap output test-->`
})
})'sitemap:sources'
Type: async (ctx: { event: RequestEvent; sitemapName: string; sources: SitemapSourceInput[] }) => void | Promise<void>
Triggered before resolving sitemap sources. This hook allows you to:
- Add new sources dynamically
- Remove sources
- Modify source configurations including fetch options and headers
This hook runs before sources are resolved, providing full control over the source list.
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('sitemap:sources', async (ctx) => {
// Add a source that will be fetched
ctx.sources.push('/api/dynamic-urls')
// Add a source with fetch options
ctx.sources.push(['/api/authenticated-urls', { headers: { 'X-Api-Key': 'secret' } }])
// Add a resolved source with URLs directly (no fetch needed)
ctx.sources.push({
context: { name: 'my-custom-source' },
urls: ['/page-1', '/page-2', { loc: '/page-3', priority: 0.8 }],
})
// Modify existing sources to add headers
ctx.sources = ctx.sources.map((source) => {
if (typeof source === 'object' && 'fetch' in source && source.fetch) {
const [url, options = {}] = Array.isArray(source.fetch) ? source.fetch : [source.fetch, {}]
// Add headers from original request
const authHeader = ctx.event.req.headers.get('authorization')
if (authHeader) {
options.headers = options.headers || {}
options.headers.Authorization = authHeader
}
source.fetch = [url, options]
}
return source
})
// Filter out sources
ctx.sources = ctx.sources.filter((source) => {
if (typeof source === 'string')
return !source.includes('skip-this')
return true
})
})
})Recipes
Modify Sitemap xmlns attribute
For some search engines, you may need to add a custom xmlns attribute to the sitemap. You can do this with a simple
search and replace in the sitemap:output hook.
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('sitemap:output', async (ctx) => {
ctx.sitemap = ctx.sitemap.replace('<urlset ', '<urlset xmlns:mobile="http://www.baidu.com/schemas/sitemap-mobile/1/" ')
})
})Modify Video Entries For Host
Sometimes you'll want to include the videos from your markup automatically but exclude some of them based on the host.
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('sitemap:resolved', (ctx) => {
ctx.urls.map((url) => {
if (url.videos?.length) {
url.videos = url.videos.filter((video) => {
if (video.content_loc) {
const url = new URL(video.content_loc)
return url.host.startsWith('www.youtube.com')
}
return false
})
}
return url
})
})
})