diff --git a/src/runtime/server/composables/defineSitemapEventHandler.ts b/src/runtime/server/composables/defineSitemapEventHandler.ts index 37346aaf..cb020cdf 100644 --- a/src/runtime/server/composables/defineSitemapEventHandler.ts +++ b/src/runtime/server/composables/defineSitemapEventHandler.ts @@ -2,4 +2,4 @@ import { defineEventHandler } from 'h3' import type { EventHandlerRequest, EventHandlerResponse } from 'h3' import type { SitemapUrlInput } from '../../types' -export const defineSitemapEventHandler: typeof defineEventHandler> = defineEventHandler +export const defineSitemapEventHandler: typeof defineEventHandler> = defineEventHandler diff --git a/src/runtime/server/index.ts b/src/runtime/server/index.ts new file mode 100644 index 00000000..988cf75b --- /dev/null +++ b/src/runtime/server/index.ts @@ -0,0 +1,2 @@ +export { defineSitemapEventHandler } from './composables/defineSitemapEventHandler' +export { asSitemapUrl } from './composables/asSitemapUrl' diff --git a/src/runtime/server/routes/__sitemap__/debug.ts b/src/runtime/server/routes/__sitemap__/debug.ts index 7da4753f..e33e0885 100644 --- a/src/runtime/server/routes/__sitemap__/debug.ts +++ b/src/runtime/server/routes/__sitemap__/debug.ts @@ -1,4 +1,5 @@ import { defineEventHandler } from 'h3' +import { useStorage } from 'nitropack/runtime' import type { SitemapDefinition } from '../../../types' import { useSitemapRuntimeConfig } from '../../utils' import { @@ -24,7 +25,10 @@ export default defineEventHandler(async (e) => { sources: await resolveSitemapSources(await childSitemapSources(_sitemaps[s])), } } + const cacheKeys = await useStorage().keys('cache:sitemap') + const cacheFiles = await Promise.all(cacheKeys.map(async k => ({ key: k, val: await useStorage().get(k) }))) return { + cache: cacheFiles, nitroOrigin, sitemaps, runtimeConfig, diff --git a/src/runtime/server/sitemap/builder/sitemap-index.ts b/src/runtime/server/sitemap/builder/sitemap-index.ts index 3359ce47..06b43763 100644 --- a/src/runtime/server/sitemap/builder/sitemap-index.ts +++ b/src/runtime/server/sitemap/builder/sitemap-index.ts @@ -1,6 +1,7 @@ import { defu } from 'defu' import { joinURL } from 'ufo' import type { NitroApp } from 'nitropack/types' +import { useStorage } from 'nitropack/runtime' import type { ModuleRuntimeConfig, NitroUrlResolvers, @@ -58,10 +59,16 @@ export async function buildSitemapIndex(resolvers: NitroUrlResolvers, runtimeCon }) } else { + const host = new URL(resolvers.canonicalUrlResolver('/')).hostname + const cacheKey = `cache:sitemap:${host}:skip.json` + const currStats = ((await useStorage().get(cacheKey)) || {}) as Record for (const sitemap in sitemaps) { if (sitemap !== 'index') { - // user provided sitemap config - chunks[sitemap] = chunks[sitemap] || { urls: [] } + // automatically hide sitemap if it's empty + if (!(sitemap in currStats) || !currStats[sitemap]) { + // user provided sitemap config + chunks[sitemap] = chunks[sitemap] || { urls: [] } + } } } } diff --git a/src/runtime/server/sitemap/builder/sitemap.ts b/src/runtime/server/sitemap/builder/sitemap.ts index 009ccbae..02f44aea 100644 --- a/src/runtime/server/sitemap/builder/sitemap.ts +++ b/src/runtime/server/sitemap/builder/sitemap.ts @@ -176,7 +176,7 @@ export function resolveSitemapEntries(sitemap: SitemapDefinition, urls: SitemapU return _urls } -export async function buildSitemapUrls(sitemap: SitemapDefinition, resolvers: NitroUrlResolvers, runtimeConfig: ModuleRuntimeConfig, nitro?: NitroApp) { +export async function buildSitemapUrls(sitemap: SitemapDefinition, resolvers: NitroUrlResolvers, runtimeConfig: ModuleRuntimeConfig, nitro?: NitroApp): Promise<{ skip: number } | ResolvedSitemapUrl[]> { // 0. resolve sources // 1. normalise // 2. filter @@ -225,6 +225,17 @@ export async function buildSitemapUrls(sitemap: SitemapDefinition, resolvers: Ni const sourcesInput = sitemap.includeAppSources ? await globalSitemapSources() : [] sourcesInput.push(...await childSitemapSources(sitemap)) const sources = await resolveSitemapSources(sourcesInput, resolvers.event) + let skipTime = 0 + for (const source of sources) { + if (source._skip) { + skipTime = Math.max(skipTime, source._skip) + } + } + if (skipTime > 0) { + return { + skip: skipTime, + } + } const resolvedCtx: SitemapInputCtx = { urls: sources.flatMap(s => s.urls), sitemapName: sitemap.sitemapName, diff --git a/src/runtime/server/sitemap/nitro.ts b/src/runtime/server/sitemap/nitro.ts index 1e03de54..df951512 100644 --- a/src/runtime/server/sitemap/nitro.ts +++ b/src/runtime/server/sitemap/nitro.ts @@ -2,6 +2,7 @@ import { getQuery, setHeader, createError } from 'h3' import type { H3Event } from 'h3' import { fixSlashes } from 'nuxt-site-config/urls' import { defu } from 'defu' +import { useStorage, cachedFunction } from 'nitropack/runtime' import type { ModuleRuntimeConfig, NitroUrlResolvers, @@ -50,6 +51,17 @@ export async function createSitemap(event: H3Event, definition: SitemapDefinitio } const resolvers = useNitroUrlResolvers(event) let sitemapUrls = await buildSitemapUrls(definition, resolvers, runtimeConfig, nitro) + if (typeof sitemapUrls === 'object' && sitemapUrls.skip) { + const host = new URL(resolvers.canonicalUrlResolver('/')).hostname + const cacheKey = `cache:sitemap:${host}:skip.json` + const currStats = ((await useStorage().get(cacheKey)) || {}) as Record + currStats[sitemapName] = { + + } + await useStorage().set(cacheKey, currStats) + // still render + sitemapUrls = [] + } const routeRuleMatcher = createNitroRouteRuleMatcher() const { autoI18n } = runtimeConfig diff --git a/src/runtime/server/sitemap/urlset/sources.ts b/src/runtime/server/sitemap/urlset/sources.ts index f90a40fa..531aed53 100644 --- a/src/runtime/server/sitemap/urlset/sources.ts +++ b/src/runtime/server/sitemap/urlset/sources.ts @@ -51,9 +51,15 @@ export async function fetchDataSource(input: SitemapSourceBase | SitemapSourceRe error: 'Received HTML response instead of JSON', } } + let _skip: false | number = false let urls = [] if (typeof res === 'object') { - urls = res.urls || res + if (res.skip) { + _skip = res.skip as number + } + else { + urls = res.urls || res + } } else if (typeof res === 'string' && parseURL(url).pathname.endsWith('.xml')) { // fast pass XML extract all loc data, let's use @@ -61,6 +67,7 @@ export async function fetchDataSource(input: SitemapSourceBase | SitemapSourceRe } return { ...input, + _skip, context, timeTakenMs, urls: urls as SitemapUrlInput[], diff --git a/src/runtime/types.ts b/src/runtime/types.ts index 591c1f29..b0216252 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -179,6 +179,10 @@ export interface SitemapSourceBase { fetch?: string | [string, FetchOptions] urls?: SitemapUrlInput[] sourceType?: 'app' | 'user' + /** + * @internal + */ + _skip?: false | number } export interface SitemapSourceResolved extends Omit { urls: SitemapUrlInput[] diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts index ea2353c1..75a57917 100644 --- a/test/fixtures/basic/nuxt.config.ts +++ b/test/fixtures/basic/nuxt.config.ts @@ -24,5 +24,20 @@ export default defineNuxtConfig({ autoLastmod: false, credits: false, debug: true, + sitemaps: { + foo: { + sources: ['/api/sitemap/foo'], + defaults: { + changefreq: 'weekly', + priority: 0.7, + }, + }, + bar: { + sources: ['/api/sitemap/bar'], + }, + empty: { + sources: ['/api/sitemap/empty'], + }, + }, }, }) diff --git a/test/fixtures/basic/server/api/sitemap/empty.ts b/test/fixtures/basic/server/api/sitemap/empty.ts new file mode 100644 index 00000000..e8f25e6b --- /dev/null +++ b/test/fixtures/basic/server/api/sitemap/empty.ts @@ -0,0 +1,8 @@ +import { defineSitemapEventHandler } from '#sitemap/server' + +export default defineSitemapEventHandler(async () => { + await new Promise(resolve => setTimeout(resolve, 0)) + return { + skip: 120_000, + } +}) diff --git a/test/integration/multi/endpoints.ts b/test/integration/multi/endpoints.ts index 6175943a..d8496be4 100644 --- a/test/integration/multi/endpoints.ts +++ b/test/integration/multi/endpoints.ts @@ -19,6 +19,9 @@ await setup({ bar: { sources: ['/api/sitemap/bar'], }, + empty: { + sources: ['/api/sitemap/empty'], + }, }, }, },