diff --git a/src/module.ts b/src/module.ts index 4aa7e6e9..ce0e3a09 100644 --- a/src/module.ts +++ b/src/module.ts @@ -319,8 +319,10 @@ declare module 'vue-router' { nuxt.options.nitro.routeRules['/sitemap.xml'] = { redirect: '/sitemap_index.xml' } nuxt.options.nitro.routeRules['/sitemap_index.xml'] = routeRules if (typeof config.sitemaps === 'object') { - for (const k in config.sitemaps) - nuxt.options.nitro.routeRules[`/${k}-sitemap.xml`] = routeRules + for (const k in config.sitemaps) { + nuxt.options.nitro.routeRules[`/sitemap/${k}.xml`] = routeRules + nuxt.options.nitro.routeRules[`/${k}-sitemap.xml`] = { redirect: `/sitemap/${k}.xml` } + } } else { // TODO we should support the chunked generated sitemap names @@ -394,6 +396,14 @@ declare module 'vue-router' { addServerHandler({ route: '/sitemap_index.xml', handler: resolve('./runtime/nitro/routes/sitemap_index.xml'), + lazy: true, + middleware: false, + }) + addServerHandler({ + route: `/sitemap/**:sitemap`, + handler: resolve('./runtime/nitro/routes/sitemap/[sitemap].xml'), + lazy: true, + middleware: false, }) sitemaps.index = { sitemapName: 'index', @@ -405,15 +415,11 @@ declare module 'vue-router' { for (const sitemapName in config.sitemaps) { if (sitemapName === 'index') continue - addServerHandler({ - route: `/${sitemapName}-sitemap.xml`, - handler: resolve('./runtime/nitro/middleware/[sitemap]-sitemap.xml'), - }) const definition = config.sitemaps[sitemapName] as MultiSitemapEntry[string] sitemaps[sitemapName as keyof typeof sitemaps] = defu( { sitemapName, - _route: withBase(`${sitemapName}-sitemap.xml`, nuxt.options.app.baseURL || '/'), + _route: withBase(`sitemap/${sitemapName}.xml`, nuxt.options.app.baseURL || '/'), _hasSourceChunk: typeof definition.urls !== 'undefined' || definition.sources?.length || !!definition.dynamicUrlsApiEndpoint, }, { ...definition, urls: undefined, sources: undefined }, @@ -422,10 +428,7 @@ declare module 'vue-router' { } } else { - // we have to registrer it as a middleware we can't match the URL pattern - addServerHandler({ - handler: resolve('./runtime/nitro/middleware/[sitemap]-sitemap.xml'), - }) + // we have to register it as a middleware we can't match the URL pattern sitemaps.chunks = { sitemapName: 'chunks', defaults: config.defaults, diff --git a/src/runtime/nitro/routes/sitemap.xsl.ts b/src/runtime/nitro/routes/sitemap.xsl.ts index 1da87671..0aa79433 100644 --- a/src/runtime/nitro/routes/sitemap.xsl.ts +++ b/src/runtime/nitro/routes/sitemap.xsl.ts @@ -19,7 +19,8 @@ export default defineEventHandler(async (e) => { const { name: siteName, url: siteUrl } = useSiteConfig(e) const referrer = getHeader(e, 'Referer')! || '/' - const isNotIndexButHasIndex = referrer !== fixPath('/sitemap.xml') && parseURL(referrer).pathname.endsWith('-sitemap.xml') + const referrerPath = parseURL(referrer).pathname + const isNotIndexButHasIndex = referrerPath !== '/sitemap.xml' && referrerPath !== '/sitemap_index.xml' && referrerPath.endsWith('.xml') const sitemapName = parseURL(referrer).pathname.split('/').pop()?.split('-sitemap')[0] || fallbackSitemapName const title = `${siteName}${sitemapName !== 'sitemap.xml' ? ` - ${sitemapName === 'sitemap_index.xml' ? 'index' : sitemapName}` : ''}`.replace(/&/g, '&') diff --git a/src/runtime/nitro/middleware/[sitemap]-sitemap.xml.ts b/src/runtime/nitro/routes/sitemap/[sitemap].xml.ts similarity index 51% rename from src/runtime/nitro/middleware/[sitemap]-sitemap.xml.ts rename to src/runtime/nitro/routes/sitemap/[sitemap].xml.ts index 37ebd89e..e0467df3 100644 --- a/src/runtime/nitro/middleware/[sitemap]-sitemap.xml.ts +++ b/src/runtime/nitro/routes/sitemap/[sitemap].xml.ts @@ -1,22 +1,16 @@ -import { createError, defineEventHandler } from 'h3' -import { parseURL } from 'ufo' -import { useSimpleSitemapRuntimeConfig } from '../utils' -import { createSitemap } from '../sitemap/nitro' +import { createError, defineEventHandler, getRouterParam } from 'h3' +import { useSimpleSitemapRuntimeConfig } from '../../utils' +import { createSitemap } from '../../sitemap/nitro' export default defineEventHandler(async (e) => { - const path = parseURL(e.path).pathname - if (!path.endsWith('-sitemap.xml')) - return - - const runtimeConfig = useSimpleSitemapRuntimeConfig() + const runtimeConfig = useSimpleSitemapRuntimeConfig(e) const { sitemaps } = runtimeConfig - const sitemapName = path - .replace('-sitemap.xml', '') - .replace('/', '') + const sitemapName = (getRouterParam(e, 'sitemap') || e.path)?.replace('.xml', '') + .replace('/sitemap/', '') // check if sitemapName can be cast to a number safely const isChunking = typeof sitemaps.chunks !== 'undefined' && !Number.isNaN(Number(sitemapName)) - if (!(sitemapName in sitemaps) && !isChunking) { + if (!sitemapName || (!(sitemapName in sitemaps) && !isChunking)) { return createError({ statusCode: 404, message: `Sitemap "${sitemapName}" not found.`, diff --git a/src/runtime/nitro/routes/sitemap_index.xml.ts b/src/runtime/nitro/routes/sitemap_index.xml.ts index b653de61..5285f5f0 100644 --- a/src/runtime/nitro/routes/sitemap_index.xml.ts +++ b/src/runtime/nitro/routes/sitemap_index.xml.ts @@ -2,7 +2,7 @@ import { appendHeader, defineEventHandler, setHeader } from 'h3' import { useSimpleSitemapRuntimeConfig } from '../utils' import { buildSitemapIndex, urlsToIndexXml } from '../sitemap/builder/sitemap-index' import type { SitemapOutputHookCtx } from '../../types' -import { useNitroUrlResolvers } from '..//sitemap/nitro' +import { useNitroUrlResolvers } from '../sitemap/nitro' import { useNitroApp } from '#imports' export default defineEventHandler(async (e) => { @@ -18,7 +18,7 @@ export default defineEventHandler(async (e) => { e, 'x-nitro-prerender', sitemaps.filter(entry => !!entry._sitemapName) - .map(entry => encodeURIComponent(`/${entry._sitemapName}-sitemap.xml`)).join(', '), + .map(entry => encodeURIComponent(`/sitemap/${entry._sitemapName}.xml`)).join(', '), ) } diff --git a/src/runtime/nitro/sitemap/builder/sitemap-index.ts b/src/runtime/nitro/sitemap/builder/sitemap-index.ts index f881f035..cc306645 100644 --- a/src/runtime/nitro/sitemap/builder/sitemap-index.ts +++ b/src/runtime/nitro/sitemap/builder/sitemap-index.ts @@ -64,7 +64,7 @@ export async function buildSitemapIndex(resolvers: NitroUrlResolvers, runtimeCon const sitemap = chunks[name] const entry: SitemapIndexEntry = { _sitemapName: name, - sitemap: resolvers.canonicalUrlResolver(`${name}-sitemap.xml`), + sitemap: resolvers.canonicalUrlResolver(`sitemap/${name}.xml`), } let lastmod = sitemap.urls .filter(a => !!a?.lastmod) diff --git a/src/runtime/nitro/sitemap/nitro.ts b/src/runtime/nitro/sitemap/nitro.ts index 1adfdbc9..e5d68b46 100644 --- a/src/runtime/nitro/sitemap/nitro.ts +++ b/src/runtime/nitro/sitemap/nitro.ts @@ -33,19 +33,19 @@ export function useNitroUrlResolvers(e: H3Event): NitroUrlResolvers { } } -export async function createSitemap(e: H3Event, definition: SitemapDefinition, runtimeConfig: ModuleRuntimeConfig) { +export async function createSitemap(event: H3Event, definition: SitemapDefinition, runtimeConfig: ModuleRuntimeConfig) { const { sitemapName } = definition const nitro = useNitroApp() - const resolvers = useNitroUrlResolvers(e) + const resolvers = useNitroUrlResolvers(event) let sitemapUrls = await buildSitemapUrls(definition, resolvers, runtimeConfig) const routeRuleMatcher = createNitroRouteRuleMatcher() const { autoI18n } = runtimeConfig - sitemapUrls = sitemapUrls.map((e) => { - // blocked by nuxt-simple-robots (this is a polyfill if not installed) - if (!getPathRobotConfig(e, { path: e._path.pathname, skipSiteIndexable: true }).indexable) + sitemapUrls = sitemapUrls.map((u) => { + const path = u._path?.pathname || u.loc + // blocked by @nuxtjs/robots (this is a polyfill if not installed) + if (!getPathRobotConfig(event, { path, skipSiteIndexable: true }).indexable) return false - const path = e._path.pathname let routeRules = routeRuleMatcher(path) // apply top-level path without prefix, users can still target the localed path if (autoI18n?.locales && autoI18n?.strategy !== 'no_prefix') { @@ -70,7 +70,7 @@ export async function createSitemap(e: H3Event, definition: SitemapDefinition, r if (routeRules.redirect || hasRobotsDisabled) return false - return routeRules.sitemap ? defu(e, routeRules.sitemap) as ResolvedSitemapUrl : e + return routeRules.sitemap ? defu(u, routeRules.sitemap) as ResolvedSitemapUrl : u }).filter(Boolean) // 6. nitro hooks @@ -88,11 +88,11 @@ export async function createSitemap(e: H3Event, definition: SitemapDefinition, r const ctx = { sitemap, sitemapName } await nitro.hooks.callHook('sitemap:output', ctx) // need to clone the config object to make it writable - setHeader(e, 'Content-Type', 'text/xml; charset=UTF-8') + setHeader(event, 'Content-Type', 'text/xml; charset=UTF-8') if (runtimeConfig.cacheMaxAgeSeconds) - setHeader(e, 'Cache-Control', `public, max-age=${runtimeConfig.cacheMaxAgeSeconds}, must-revalidate`) + setHeader(event, 'Cache-Control', `public, max-age=${runtimeConfig.cacheMaxAgeSeconds}, must-revalidate`) else - setHeader(e, 'Cache-Control', `no-cache, no-store`) - e.context._isSitemap = true + setHeader(event, 'Cache-Control', `no-cache, no-store`) + event.context._isSitemap = true return ctx.sitemap } diff --git a/test/integration/chunks/default.ts b/test/integration/chunks/default.ts index 1a9b95c0..7d35a1ea 100644 --- a/test/integration/chunks/default.ts +++ b/test/integration/chunks/default.ts @@ -17,20 +17,20 @@ describe('multi chunks', () => { " - https://nuxtseo.com/0-sitemap.xml + https://nuxtseo.com/sitemap/0.xml - https://nuxtseo.com/1-sitemap.xml + https://nuxtseo.com/sitemap/1.xml - https://nuxtseo.com/2-sitemap.xml + https://nuxtseo.com/sitemap/2.xml - https://nuxtseo.com/3-sitemap.xml + https://nuxtseo.com/sitemap/3.xml " `) - const sitemap0 = await $fetch('/0-sitemap.xml') + const sitemap0 = await $fetch('/sitemap/0.xml') expect(sitemap0).toMatchInlineSnapshot(` " diff --git a/test/integration/chunks/generate.test.ts b/test/integration/chunks/generate.test.ts index 440c33f2..49abd85c 100644 --- a/test/integration/chunks/generate.test.ts +++ b/test/integration/chunks/generate.test.ts @@ -27,20 +27,20 @@ describe('generate', () => { " - https://nuxtseo.com/0-sitemap.xml + https://nuxtseo.com/sitemap/0.xml - https://nuxtseo.com/1-sitemap.xml + https://nuxtseo.com/sitemap/1.xml - https://nuxtseo.com/2-sitemap.xml + https://nuxtseo.com/sitemap/2.xml - https://nuxtseo.com/3-sitemap.xml + https://nuxtseo.com/sitemap/3.xml " `) - const sitemapEn = (await readFile(resolve(rootDir, '.output/public/0-sitemap.xml'), 'utf-8')).replace(/lastmod>(.*?)<') + const sitemapEn = (await readFile(resolve(rootDir, '.output/public/sitemap/0.xml'), 'utf-8')).replace(/lastmod>(.*?)<') expect(sitemapEn).toMatchInlineSnapshot(` " diff --git a/test/integration/i18n/domains.test.ts b/test/integration/i18n/domains.test.ts index 4fb2bfcd..2ef7bc18 100644 --- a/test/integration/i18n/domains.test.ts +++ b/test/integration/i18n/domains.test.ts @@ -41,18 +41,18 @@ describe('i18n domains', () => { " - https://nuxtseo.com/en-US-sitemap.xml + https://nuxtseo.com/sitemap/en-US.xml - https://nuxtseo.com/es-ES-sitemap.xml + https://nuxtseo.com/sitemap/es-ES.xml - https://nuxtseo.com/fr-FR-sitemap.xml + https://nuxtseo.com/sitemap/fr-FR.xml " `) - const fr = await $fetch('/fr-FR-sitemap.xml') + const fr = await $fetch('/sitemap/fr-FR.xml') expect(fr).toMatchInlineSnapshot(` " diff --git a/test/integration/i18n/dynamic-urls.test.ts b/test/integration/i18n/dynamic-urls.test.ts index 54157c53..11e6dac7 100644 --- a/test/integration/i18n/dynamic-urls.test.ts +++ b/test/integration/i18n/dynamic-urls.test.ts @@ -24,7 +24,7 @@ await setup({ }) describe('i18n dynamic urls', () => { it('basic', async () => { - let sitemap = await $fetch('/en-US-sitemap.xml') + let sitemap = await $fetch('/sitemap/en-US.xml') // strip lastmod sitemap = sitemap.replace(/.*<\/lastmod>/g, '') diff --git a/test/integration/i18n/filtering-include.test.ts b/test/integration/i18n/filtering-include.test.ts index 9fff4f33..de2f2196 100644 --- a/test/integration/i18n/filtering-include.test.ts +++ b/test/integration/i18n/filtering-include.test.ts @@ -19,7 +19,7 @@ await setup({ }) describe('i18n filtering with include', () => { it('basic', async () => { - const sitemap = await $fetch('/main-sitemap.xml') + const sitemap = await $fetch('/sitemap/main.xml') expect(sitemap).toMatchInlineSnapshot(` " diff --git a/test/integration/i18n/filtering-regexp.test.ts b/test/integration/i18n/filtering-regexp.test.ts index b763d178..e25f80ce 100644 --- a/test/integration/i18n/filtering-regexp.test.ts +++ b/test/integration/i18n/filtering-regexp.test.ts @@ -21,7 +21,7 @@ await setup({ }) describe('i18n filtering with regexp', () => { it('basic', async () => { - let sitemap = await $fetch('/en-US-sitemap.xml') + let sitemap = await $fetch('/sitemap/en-US.xml') // strip lastmod sitemap = sitemap.replace(/.*<\/lastmod>/g, '') diff --git a/test/integration/i18n/filtering.test.ts b/test/integration/i18n/filtering.test.ts index f51084d4..a75cc131 100644 --- a/test/integration/i18n/filtering.test.ts +++ b/test/integration/i18n/filtering.test.ts @@ -16,7 +16,7 @@ await setup({ }) describe('i18n filtering', () => { it('basic', async () => { - let sitemap = await $fetch('/en-US-sitemap.xml') + let sitemap = await $fetch('/sitemap/en-US.xml') // strip lastmod sitemap = sitemap.replace(/.*<\/lastmod>/g, '') diff --git a/test/integration/i18n/generate.test.ts b/test/integration/i18n/generate.test.ts index 870b83a9..114cdabe 100644 --- a/test/integration/i18n/generate.test.ts +++ b/test/integration/i18n/generate.test.ts @@ -27,17 +27,17 @@ describe('generate', () => { " - https://nuxtseo.com/en-US-sitemap.xml + https://nuxtseo.com/sitemap/en-US.xml - https://nuxtseo.com/es-ES-sitemap.xml + https://nuxtseo.com/sitemap/es-ES.xml - https://nuxtseo.com/fr-FR-sitemap.xml + https://nuxtseo.com/sitemap/fr-FR.xml " `) - const sitemapEn = (await readFile(resolve(rootDir, '.output/public/en-US-sitemap.xml'), 'utf-8')).replace(/lastmod>(.*?)<') + const sitemapEn = (await readFile(resolve(rootDir, '.output/public/sitemap/en-US.xml'), 'utf-8')).replace(/lastmod>(.*?)<') expect(sitemapEn).toMatchInlineSnapshot(` " diff --git a/test/integration/i18n/pages-multi.test.ts b/test/integration/i18n/pages-multi.test.ts index c574fe5e..ef3c3950 100644 --- a/test/integration/i18n/pages-multi.test.ts +++ b/test/integration/i18n/pages-multi.test.ts @@ -6,13 +6,22 @@ const { resolve } = createResolver(import.meta.url) await setup({ rootDir: resolve('../../fixtures/i18n'), - build: true, server: true, nuxtConfig: { i18n: { locales: [ - 'en', - 'fr', + { + code: 'en', + iso: 'en-US', + }, + { + code: 'es', + iso: 'es-ES', + }, + { + code: 'fr', + iso: 'fr-FR', + }, ], pages: { 'about': { @@ -49,22 +58,22 @@ await setup({ }) describe('i18n pages multi', () => { it('basic', async () => { - const index = await $fetch('/sitemap.xml') + const index = await $fetch('/sitemap_index.xml') expect(index).toMatchInlineSnapshot(` " - https://nuxtseo.com/en-US-sitemap.xml + https://nuxtseo.com/sitemap/en-US.xml - https://nuxtseo.com/fr-FR-sitemap.xml + https://nuxtseo.com/sitemap/es-ES.xml - https://nuxtseo.com/es-ES-sitemap.xml + https://nuxtseo.com/sitemap/fr-FR.xml " `) - const fr = await $fetch('/fr-FR-sitemap.xml') + const fr = await $fetch('/sitemap/fr-FR.xml') expect(fr).toMatchInlineSnapshot(` " @@ -85,8 +94,8 @@ describe('i18n pages multi', () => { weekly - + https://nuxtseo.com/fr/offres/developement @@ -114,19 +123,5 @@ describe('i18n pages multi', () => { " `) - const es = await $fetch('/es-ES-sitemap.xml') - expect(es).toMatchInlineSnapshot(` - " - - - https://nuxtseo.com/es/__sitemap/url - weekly - - - - - - " - `) }, 60000) }) diff --git a/test/integration/multi/defaults.ts b/test/integration/multi/defaults.ts index 6516522e..2fa94083 100644 --- a/test/integration/multi/defaults.ts +++ b/test/integration/multi/defaults.ts @@ -36,7 +36,7 @@ await setup({ }) describe('mutli defaults', () => { it('basic', async () => { - let sitemap = await $fetch('/foo-sitemap.xml') + let sitemap = await $fetch('/sitemap/foo.xml') // remove lastmods before tresting sitemap = sitemap.replace(/lastmod>(.*?)<') // basic test to make sure we get a valid response diff --git a/test/integration/multi/endpoints.ts b/test/integration/multi/endpoints.ts index 2e0bead0..e7bea1e8 100644 --- a/test/integration/multi/endpoints.ts +++ b/test/integration/multi/endpoints.ts @@ -25,7 +25,7 @@ await setup({ }) describe('multi endpoints', () => { it('basic', async () => { - let sitemap = await $fetch('/foo-sitemap.xml') + let sitemap = await $fetch('/sitemap/foo.xml') // remove lastmods before tresting sitemap = sitemap.replace(/lastmod>(.*?)<') // basic test to make sure we get a valid response diff --git a/test/integration/multi/filtering.test.ts b/test/integration/multi/filtering.test.ts index 9e6cd716..c22b31a0 100644 --- a/test/integration/multi/filtering.test.ts +++ b/test/integration/multi/filtering.test.ts @@ -36,7 +36,7 @@ await setup({ }) describe('multi filtering', () => { it('basic', async () => { - let sitemap = await $fetch('/foo-sitemap.xml') + let sitemap = await $fetch('/sitemap/foo.xml') // strip lastmod sitemap = sitemap.replace(/.*<\/lastmod>/g, '')