Skip to content

Commit

Permalink
feat: sitemap:input Nitro hook (#397)
Browse files Browse the repository at this point in the history
  • Loading branch information
harlan-zw authored Jan 15, 2025
1 parent 5e934f0 commit e4a987c
Show file tree
Hide file tree
Showing 15 changed files with 161 additions and 71 deletions.
29 changes: 28 additions & 1 deletion docs/content/5.nitro-api/nitro-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,39 @@ description: Learn how to use Nitro Hooks to customize your sitemap entries.

Nitro hooks can be added to modify the output of your sitemaps at runtime.

## `'sitemap:input'`{lang="ts"}

**Type:** `async (ctx: { urls: SitemapUrlInput[]; sitemapName: string }) => void | Promise<void>`{lang="ts"}

Triggers once the raw list of URLs is collected from sources.

This hook is best used for inserting new URLs into the sitemap.

```ts [server/plugins/sitemap.ts]
import { defineNitroPlugin } from 'nitropack/runtime'

export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('sitemap:resolved', 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'`{lang="ts"}

**Type:** `async (ctx: { urls: SitemapConfig; sitemapName: string }) => void | Promise<void>`{lang="ts"}
**Type:** `async (ctx: { urls: ResolvedSitemapUrl[]; sitemapName: string }) => void | Promise<void>`{lang="ts"}

Triggered once the final structure of the XML is generated, provides the URLs as objects.

For new URLs it's recommended to use `sitemap:input` instead. Use this hook for modifying entries or removing them.

```ts [server/plugins/sitemap.ts]
import { defineNitroPlugin } from 'nitropack/runtime'

Expand Down
1 change: 1 addition & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ declare module 'nitropack' {
}
interface NitroRuntimeHooks {
'sitemap:index-resolved': (ctx: import('${typesPath}').SitemapIndexRenderCtx) => void | Promise<void>
'sitemap:input': (ctx: import('${typesPath}').SitemapInputCtx) => void | Promise<void>
'sitemap:resolved': (ctx: import('${typesPath}').SitemapRenderCtx) => void | Promise<void>
'sitemap:output': (ctx: import('${typesPath}').SitemapOutputHookCtx) => void | Promise<void>
}
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/server/routes/sitemap_index.xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default defineEventHandler(async (e) => {
const runtimeConfig = useSimpleSitemapRuntimeConfig()
const nitro = useNitroApp()
const resolvers = useNitroUrlResolvers(e)
const sitemaps = (await buildSitemapIndex(resolvers, runtimeConfig))
const sitemaps = await buildSitemapIndex(resolvers, runtimeConfig, nitro)

// tell the prerender to render the other sitemaps (if we prerender this one)
// this solves the dynamic chunking sitemap issue
Expand Down
12 changes: 9 additions & 3 deletions src/runtime/server/sitemap/builder/sitemap-index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { defu } from 'defu'
import { joinURL } from 'ufo'
import type { NitroApp } from 'nitropack/types'
import type {
ModuleRuntimeConfig,
NitroUrlResolvers,
ResolvedSitemapUrl,
SitemapIndexEntry,
SitemapIndexEntry, SitemapInputCtx,
SitemapUrl,
} from '../../../types'
import { normaliseDate } from '../urlset/normalise'
Expand All @@ -13,7 +14,7 @@ import { sortSitemapUrls } from '../urlset/sort'
import { escapeValueForXml, wrapSitemapXml } from './xml'
import { resolveSitemapEntries } from './sitemap'

export async function buildSitemapIndex(resolvers: NitroUrlResolvers, runtimeConfig: ModuleRuntimeConfig) {
export async function buildSitemapIndex(resolvers: NitroUrlResolvers, runtimeConfig: ModuleRuntimeConfig, nitro?: NitroApp) {
const {
sitemaps,
// enhancing
Expand All @@ -39,7 +40,12 @@ export async function buildSitemapIndex(resolvers: NitroUrlResolvers, runtimeCon
const sitemap = sitemaps.chunks
// we need to figure out how many entries we're dealing with
const sources = await resolveSitemapSources(await globalSitemapSources())
const normalisedUrls = resolveSitemapEntries(sitemap, sources, { autoI18n, isI18nMapped }, resolvers)
const resolvedCtx: SitemapInputCtx = {
urls: sources.flatMap(s => s.urls),
sitemapName: sitemap.sitemapName,
}
await nitro?.hooks.callHook('sitemap:input', resolvedCtx)
const normalisedUrls = resolveSitemapEntries(sitemap, resolvedCtx.urls, { autoI18n, isI18nMapped }, resolvers)
// 2. enhance
const enhancedUrls: ResolvedSitemapUrl[] = normalisedUrls
.map(e => defu(e, sitemap.defaults) as ResolvedSitemapUrl)
Expand Down
24 changes: 14 additions & 10 deletions src/runtime/server/sitemap/builder/sitemap.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { resolveSitePath } from 'nuxt-site-config/urls'
import { joinURL, withHttps } from 'ufo'
import type { NitroApp } from 'nitropack/types'
import type {
AlternativeEntry, AutoI18nConfig,
ModuleRuntimeConfig,
NitroUrlResolvers,
ResolvedSitemapUrl,
SitemapDefinition,
SitemapSourceResolved,
SitemapDefinition, SitemapInputCtx,
SitemapUrlInput,
} from '../../../types'
import { preNormalizeEntry } from '../urlset/normalise'
Expand All @@ -21,7 +21,7 @@ export interface NormalizedI18n extends ResolvedSitemapUrl {
_index?: number
}

export function resolveSitemapEntries(sitemap: SitemapDefinition, sources: SitemapSourceResolved[], runtimeConfig: Pick<ModuleRuntimeConfig, 'autoI18n' | 'isI18nMapped'>, resolvers?: NitroUrlResolvers): ResolvedSitemapUrl[] {
export function resolveSitemapEntries(sitemap: SitemapDefinition, urls: SitemapUrlInput[], runtimeConfig: Pick<ModuleRuntimeConfig, 'autoI18n' | 'isI18nMapped'>, resolvers?: NitroUrlResolvers): ResolvedSitemapUrl[] {
const {
autoI18n,
isI18nMapped,
Expand All @@ -31,7 +31,7 @@ export function resolveSitemapEntries(sitemap: SitemapDefinition, sources: Sitem
exclude: sitemap.exclude,
})
// 1. normalise
const _urls = sources.flatMap(e => e.urls).map((_e) => {
const _urls = urls.map((_e) => {
const e = preNormalizeEntry(_e, resolvers)
if (!e.loc || !filterPath(e.loc))
return false
Expand Down Expand Up @@ -176,7 +176,7 @@ export function resolveSitemapEntries(sitemap: SitemapDefinition, sources: Sitem
return _urls
}

export async function buildSitemapUrls(sitemap: SitemapDefinition, resolvers: NitroUrlResolvers, runtimeConfig: ModuleRuntimeConfig) {
export async function buildSitemapUrls(sitemap: SitemapDefinition, resolvers: NitroUrlResolvers, runtimeConfig: ModuleRuntimeConfig, nitro?: NitroApp) {
// 0. resolve sources
// 1. normalise
// 2. filter
Expand Down Expand Up @@ -222,11 +222,15 @@ export async function buildSitemapUrls(sitemap: SitemapDefinition, resolvers: Ni
}
// 0. resolve sources
// always fetch all sitemap data for the primary sitemap
const sources = sitemap.includeAppSources ? await globalSitemapSources() : []
sources.push(...await childSitemapSources(sitemap))
const resolvedSources = await resolveSitemapSources(sources, resolvers.event)

const enhancedUrls = resolveSitemapEntries(sitemap, resolvedSources, { autoI18n, isI18nMapped }, resolvers)
const sourcesInput = sitemap.includeAppSources ? await globalSitemapSources() : []
sourcesInput.push(...await childSitemapSources(sitemap))
const sources = await resolveSitemapSources(sourcesInput, resolvers.event)
const resolvedCtx: SitemapInputCtx = {
urls: sources.flatMap(s => s.urls),
sitemapName: sitemap.sitemapName,
}
await nitro?.hooks.callHook('sitemap:input', resolvedCtx)
const enhancedUrls = resolveSitemapEntries(sitemap, resolvedCtx.urls, { autoI18n, isI18nMapped }, resolvers)
// 3. filtered urls
// TODO make sure include and exclude start with baseURL?
const filteredUrls = enhancedUrls.filter((e) => {
Expand Down
10 changes: 8 additions & 2 deletions src/runtime/server/sitemap/nitro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type {
import { logger, mergeOnKey, splitForLocales } from '../../utils-pure'
import { createNitroRouteRuleMatcher } from '../kit'
import { buildSitemapUrls, urlsToXml } from './builder/sitemap'
import { normaliseEntry } from './urlset/normalise'
import { normaliseEntry, preNormalizeEntry } from './urlset/normalise'
import { sortSitemapUrls } from './urlset/sort'
import { useNitroApp, createSitePathResolver, getPathRobotConfig, useSiteConfig } from '#imports'

Expand Down Expand Up @@ -49,7 +49,7 @@ export async function createSitemap(event: H3Event, definition: SitemapDefinitio
}
}
const resolvers = useNitroUrlResolvers(event)
let sitemapUrls = await buildSitemapUrls(definition, resolvers, runtimeConfig)
let sitemapUrls = await buildSitemapUrls(definition, resolvers, runtimeConfig, nitro)

const routeRuleMatcher = createNitroRouteRuleMatcher()
const { autoI18n } = runtimeConfig
Expand Down Expand Up @@ -84,11 +84,17 @@ export async function createSitemap(event: H3Event, definition: SitemapDefinitio
}).filter(Boolean)

// 6. nitro hooks
const locSize = sitemapUrls.length
const resolvedCtx: SitemapRenderCtx = {
urls: sitemapUrls,
sitemapName: sitemapName,
}
await nitro.hooks.callHook('sitemap:resolved', resolvedCtx)
// we need to normalize any new urls otherwise they won't appear in the final sitemap
// Note this is risky and users should be using the sitemap:input hook for additions
if (resolvedCtx.urls.length !== locSize) {
resolvedCtx.urls = resolvedCtx.urls.map(e => preNormalizeEntry(e, resolvers))
}

const maybeSort = (urls: ResolvedSitemapUrl[]) => runtimeConfig.sortEntries ? sortSitemapUrls(urls) : urls
// final urls
Expand Down
5 changes: 5 additions & 0 deletions src/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,11 @@ export interface SitemapRenderCtx {
urls: ResolvedSitemapUrl[]
}

export interface SitemapInputCtx {
sitemapName: string
urls: SitemapUrlInput[]
}

export interface SitemapOutputHookCtx {
sitemapName: string
sitemap: string
Expand Down
2 changes: 1 addition & 1 deletion test/bench/i18n.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('i18n', () => {
bench('normaliseI18nSources', () => {
resolveSitemapEntries({
sitemapName: 'sitemap.xml',
}, sources, {
}, sources.flatMap(s => s.urls), {
autoI18n: {
locales: [
{ code: 'en', iso: 'en' },
Expand Down
2 changes: 0 additions & 2 deletions test/bench/normalize.bench.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { bench, describe } from 'vitest'
import { preNormalizeEntry } from '../../src/runtime/server/sitemap/urlset/normalise'
import type { SitemapSourceResolved } from '#sitemap'
import { resolveSitemapEntries } from '~/src/runtime/server/sitemap/builder/sitemap'

const sources: SitemapSourceResolved[] = [
{
Expand All @@ -17,7 +16,6 @@ const sources: SitemapSourceResolved[] = [

describe('normalize', () => {
bench('preNormalizeEntry', () => {
resolveSitemapEntries(sources)
const urls = sources.flatMap(s => s.urls)
urls.map(u => preNormalizeEntry(u))
}, {
Expand Down
6 changes: 6 additions & 0 deletions test/fixtures/basic/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@ export default defineNuxtConfig({
modules: [
NuxtSitemap,
],

site: {
url: 'https://nuxtseo.com',
},

routeRules: {
'/foo-redirect': {
redirect: '/foo',
},
},

compatibilityDate: '2025-01-15',

debug: process.env.NODE_ENV === 'test',

sitemap: {
autoLastmod: false,
credits: false,
Expand Down
28 changes: 28 additions & 0 deletions test/fixtures/hooks/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import NuxtSitemap from '../../../src/module'

// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
modules: [
NuxtSitemap,
],

site: {
url: 'https://nuxtseo.com',
},

routeRules: {
'/foo-redirect': {
redirect: '/foo',
},
},

compatibilityDate: '2025-01-15',

debug: process.env.NODE_ENV === 'test',

sitemap: {
autoLastmod: false,
credits: false,
debug: true,
},
})
7 changes: 7 additions & 0 deletions test/fixtures/hooks/pages/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<div>
<a href="/sitemap.xml">
sitemap.xml
</a>
</div>
</template>
13 changes: 13 additions & 0 deletions test/fixtures/hooks/server/plugins/sitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineNitroPlugin } from 'nitropack/runtime'

export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('sitemap:input', async (ctx) => {
ctx.urls.push({
loc: '/test-1',
})

ctx.urls.push({
loc: '/test-2',
})
})
})
13 changes: 13 additions & 0 deletions test/fixtures/hooks/server/routes/__sitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineEventHandler } from 'h3'

export default defineEventHandler(() => {
return [
'/__sitemap/url',
{
loc: '/__sitemap/loc',
},
{
loc: 'https://nuxtseo.com/__sitemap/abs',
},
]
})
Loading

0 comments on commit e4a987c

Please sign in to comment.