@nuxtjs/sitemap 8.4.0 → 8.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/module.mjs +109 -20
- package/dist/runtime/server/routes/__sitemap__/debug.js +2 -2
- package/dist/runtime/server/sitemap/builder/sitemap-index.js +4 -1
- package/dist/runtime/server/sitemap/builder/sitemap.js +8 -2
- package/dist/runtime/server/sitemap/event-handlers.js +5 -5
- package/dist/runtime/server/sitemap/urlset/sources.js +84 -9
- package/dist/runtime/server/utils.d.ts +1 -0
- package/dist/runtime/server/utils.js +60 -4
- package/dist/runtime/types.d.ts +19 -1
- package/package.json +4 -4
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -8,9 +8,12 @@ import { dirname, extname } from 'pathe';
|
|
|
8
8
|
import { readPackageJSON } from 'pkg-types';
|
|
9
9
|
import { withBase, withHttps, withTrailingSlash, withoutLeadingSlash, joinURL, withLeadingSlash } from 'ufo';
|
|
10
10
|
import { setupDevToolsUI as setupDevToolsUI$1 } from 'nuxtseo-shared/devtools';
|
|
11
|
+
import { once } from 'node:events';
|
|
11
12
|
import { readFileSync, statSync } from 'node:fs';
|
|
12
13
|
import { rm, mkdir, writeFile } from 'node:fs/promises';
|
|
13
|
-
import { join } from 'node:path';
|
|
14
|
+
import { join, resolve } from 'node:path';
|
|
15
|
+
import { pathToFileURL } from 'node:url';
|
|
16
|
+
import { Worker, MessageChannel } from 'node:worker_threads';
|
|
14
17
|
import { colors } from 'consola/utils';
|
|
15
18
|
import { splitForLocales, createPathFilter } from '../dist/runtime/utils-pure.js';
|
|
16
19
|
import { p as parseHtmlExtractSitemapMeta } from './shared/sitemap.BoMnWHOt.mjs';
|
|
@@ -75,6 +78,10 @@ export async function readSourcesFromFilesystem(filename) {
|
|
|
75
78
|
`;
|
|
76
79
|
});
|
|
77
80
|
nuxt.hooks.hook("nitro:init", async (nitro) => {
|
|
81
|
+
let prerendererNitro = nitro;
|
|
82
|
+
nitro.hooks.hook("prerender:init", (prerenderer) => {
|
|
83
|
+
prerendererNitro = prerenderer;
|
|
84
|
+
});
|
|
78
85
|
await Promise.all([
|
|
79
86
|
rm(join(runtimeAssetsPath, "global-sources.json"), { force: true }),
|
|
80
87
|
rm(join(runtimeAssetsPath, "child-sources.json"), { force: true })
|
|
@@ -128,12 +135,101 @@ export async function readSourcesFromFilesystem(filename) {
|
|
|
128
135
|
await writeFile(join(runtimeAssetsPath, "global-sources.json"), JSON.stringify(globalSources));
|
|
129
136
|
await writeFile(join(runtimeAssetsPath, "child-sources.json"), JSON.stringify(childSources));
|
|
130
137
|
const sitemapEntry = options.isMultiSitemap ? "/sitemap_index.xml" : `/${Object.keys(options.sitemaps)[0]}`;
|
|
131
|
-
const
|
|
132
|
-
await nuxt.hooks.callHook("sitemap:prerender:done", { options, sitemaps });
|
|
138
|
+
const prerenderServer = await loadPrerenderServer(prerendererNitro);
|
|
139
|
+
await prerenderSitemapsFromEntry(nitro, prerenderServer.fetch, sitemapEntry).then((sitemaps) => nuxt.hooks.callHook("sitemap:prerender:done", { options, sitemaps })).finally(prerenderServer.close);
|
|
133
140
|
});
|
|
134
141
|
});
|
|
135
142
|
}
|
|
136
|
-
|
|
143
|
+
const PrerenderWorkerCode = `
|
|
144
|
+
const { parentPort, workerData } = require('node:worker_threads')
|
|
145
|
+
|
|
146
|
+
;(async () => {
|
|
147
|
+
const serverEntry = await import(workerData.entry)
|
|
148
|
+
const server = serverEntry.default
|
|
149
|
+
const localFetch = serverEntry.localFetch
|
|
150
|
+
const fetch = typeof server?.fetch === 'function'
|
|
151
|
+
? (input, headers) => server.fetch(new Request(new URL(input, 'http://localhost'), { headers }))
|
|
152
|
+
: typeof localFetch === 'function'
|
|
153
|
+
// older nitropack exposes an ofetch instance: a bare call returns parsed data, .raw returns the response
|
|
154
|
+
? (input, headers) => (localFetch.raw ?? localFetch)(input, { headers })
|
|
155
|
+
: undefined
|
|
156
|
+
const close = typeof server?.close === 'function'
|
|
157
|
+
? () => server.close()
|
|
158
|
+
: typeof serverEntry.closePrerenderer === 'function'
|
|
159
|
+
? () => serverEntry.closePrerenderer()
|
|
160
|
+
: async () => {}
|
|
161
|
+
|
|
162
|
+
parentPort.postMessage(fetch ? { _tag: 'Ready' } : { _tag: 'Err', message: 'Nitro prerender server does not expose a fetch handler' })
|
|
163
|
+
parentPort.on('message', ({ _tag, input, headers, port }) => {
|
|
164
|
+
const task = _tag === 'Fetch'
|
|
165
|
+
? fetch(input, headers).then(async response => ({
|
|
166
|
+
_tag: 'Ok',
|
|
167
|
+
status: response.status,
|
|
168
|
+
statusText: response.statusText,
|
|
169
|
+
headers: [...response.headers],
|
|
170
|
+
body: await response.arrayBuffer(),
|
|
171
|
+
}))
|
|
172
|
+
: close().then(() => ({ _tag: 'Ok' }))
|
|
173
|
+
task.catch(error => ({ _tag: 'Err', message: error instanceof Error ? error.message : String(error) }))
|
|
174
|
+
.then(result => port.postMessage(result, result.body ? [result.body] : []))
|
|
175
|
+
})
|
|
176
|
+
})().catch(error => parentPort.postMessage({ _tag: 'Err', message: error instanceof Error ? error.message : String(error) }))
|
|
177
|
+
`;
|
|
178
|
+
function raceWithTimeout(promises, ms, message) {
|
|
179
|
+
let timer;
|
|
180
|
+
const timeout = new Promise((_, reject) => {
|
|
181
|
+
timer = setTimeout(() => reject(new Error(message)), ms);
|
|
182
|
+
});
|
|
183
|
+
return Promise.race([...promises, timeout]).finally(() => clearTimeout(timer));
|
|
184
|
+
}
|
|
185
|
+
async function loadPrerenderServer(nitro) {
|
|
186
|
+
const entryFileNames = nitro.options.rollupConfig?.output?.entryFileNames;
|
|
187
|
+
const serverFilename = typeof entryFileNames === "string" ? entryFileNames : "index.mjs";
|
|
188
|
+
const worker = new Worker(PrerenderWorkerCode, {
|
|
189
|
+
eval: true,
|
|
190
|
+
workerData: { entry: pathToFileURL(resolve(nitro.options.output.serverDir, serverFilename)).href }
|
|
191
|
+
});
|
|
192
|
+
const workerDead = new Promise((_, reject) => {
|
|
193
|
+
worker.once("exit", (code) => reject(new Error(`The Nitro prerender server worker exited unexpectedly (code ${code})`)));
|
|
194
|
+
worker.once("error", (error) => reject(error));
|
|
195
|
+
});
|
|
196
|
+
workerDead.catch(() => {
|
|
197
|
+
});
|
|
198
|
+
try {
|
|
199
|
+
const [ready] = await raceWithTimeout([once(worker, "message"), workerDead], 6e4, "The Nitro prerender server did not become ready in time");
|
|
200
|
+
if (ready._tag === "Err")
|
|
201
|
+
throw new Error(ready.message);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
await worker.terminate();
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
const callWorker = async (message) => {
|
|
207
|
+
const { port1, port2 } = new MessageChannel();
|
|
208
|
+
worker.postMessage({ ...message, port: port2 }, [port2]);
|
|
209
|
+
try {
|
|
210
|
+
const [result] = await Promise.race([once(port1, "message"), workerDead]);
|
|
211
|
+
if (result._tag === "Err")
|
|
212
|
+
throw new Error(result.message);
|
|
213
|
+
return result;
|
|
214
|
+
} finally {
|
|
215
|
+
port1.close();
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
return {
|
|
219
|
+
async fetch(input, headers) {
|
|
220
|
+
const { _tag, body, ...responseInit } = await callWorker({ _tag: "Fetch", input, headers });
|
|
221
|
+
return new Response(body, responseInit);
|
|
222
|
+
},
|
|
223
|
+
async close() {
|
|
224
|
+
try {
|
|
225
|
+
await raceWithTimeout([callWorker({ _tag: "Close" })], 3e3, "The Nitro prerender server did not close in time");
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
await worker.terminate();
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
async function prerenderSitemapsFromEntry(nitro, fetch, entry) {
|
|
137
233
|
const sitemaps = [];
|
|
138
234
|
const queue = [entry];
|
|
139
235
|
const processed = /* @__PURE__ */ new Set();
|
|
@@ -142,7 +238,7 @@ async function prerenderSitemapsFromEntry(nitro, entry) {
|
|
|
142
238
|
if (processed.has(route))
|
|
143
239
|
continue;
|
|
144
240
|
processed.add(route);
|
|
145
|
-
const { filePath, prerenderUrls } = await prerenderRoute(nitro, route);
|
|
241
|
+
const { filePath, prerenderUrls } = await prerenderRoute(nitro, fetch, route);
|
|
146
242
|
sitemaps.push({
|
|
147
243
|
name: route,
|
|
148
244
|
get content() {
|
|
@@ -153,27 +249,19 @@ async function prerenderSitemapsFromEntry(nitro, entry) {
|
|
|
153
249
|
}
|
|
154
250
|
return sitemaps;
|
|
155
251
|
}
|
|
156
|
-
async function prerenderRoute(nitro, route) {
|
|
252
|
+
async function prerenderRoute(nitro, fetch, route) {
|
|
157
253
|
const start = Date.now();
|
|
158
254
|
const _route = { route, fileName: route };
|
|
159
255
|
const encodedRoute = encodeURI(route);
|
|
160
256
|
const fetchUrl = withBase(encodedRoute, nitro.options.baseURL);
|
|
161
|
-
const res = await
|
|
162
|
-
|
|
163
|
-
{
|
|
164
|
-
headers: { "x-nitro-prerender": encodedRoute },
|
|
165
|
-
retry: nitro.options.prerender.retry,
|
|
166
|
-
retryDelay: nitro.options.prerender.retryDelay
|
|
167
|
-
}
|
|
168
|
-
);
|
|
257
|
+
const res = await fetch(fetchUrl, { "x-nitro-prerender": encodedRoute });
|
|
258
|
+
if (!res.ok)
|
|
259
|
+
throw new Error(`Failed to prerender '${fetchUrl}': ${res.status} ${res.statusText}`);
|
|
169
260
|
const header = res.headers.get("x-nitro-prerender") || "";
|
|
170
261
|
const prerenderUrls = header.split(",").map((i) => decodeURIComponent(i.trim())).filter(Boolean);
|
|
171
262
|
const filePath = join(nitro.options.output.publicDir, _route.fileName);
|
|
172
263
|
await mkdir(dirname(filePath), { recursive: true });
|
|
173
|
-
const
|
|
174
|
-
if (data === void 0)
|
|
175
|
-
throw new Error(`No data returned from '${fetchUrl}'`);
|
|
176
|
-
const content = filePath.endsWith("json") || typeof data === "object" ? JSON.stringify(data) : data;
|
|
264
|
+
const content = await res.text();
|
|
177
265
|
await writeFile(filePath, content, "utf8");
|
|
178
266
|
_route.generateTimeMS = Date.now() - start;
|
|
179
267
|
nitro._prerenderedRoutes.push(_route);
|
|
@@ -192,7 +280,8 @@ function registerTypeTemplates(nitroCompatibility) {
|
|
|
192
280
|
'sitemap:input': (ctx: SitemapInputCtx<${nitroCompatibility.eventType}>) => void | Promise<void>
|
|
193
281
|
'sitemap:resolved': (ctx: SitemapRenderCtx<${nitroCompatibility.eventType}>) => void | Promise<void>
|
|
194
282
|
'sitemap:output': (ctx: SitemapOutputHookCtx<${nitroCompatibility.eventType}>) => void | Promise<void>
|
|
195
|
-
'sitemap:sources': (ctx: SitemapSourcesHookCtx<${nitroCompatibility.eventType}>) => void | Promise<void
|
|
283
|
+
'sitemap:sources': (ctx: SitemapSourcesHookCtx<${nitroCompatibility.eventType}>) => void | Promise<void>
|
|
284
|
+
'sitemap:sitemaps-resolved': (ctx: SitemapsResolvedCtx<${nitroCompatibility.eventType}>) => void | Promise<void>`;
|
|
196
285
|
const nitroTypes = renderNitroTypeAugmentations(nitroCompatibility, {
|
|
197
286
|
nitroInterfaces: {
|
|
198
287
|
PrerenderRoute: "_sitemap?: SitemapUrl"
|
|
@@ -203,7 +292,7 @@ function registerTypeTemplates(nitroCompatibility) {
|
|
|
203
292
|
});
|
|
204
293
|
return `// Generated by @nuxtjs/sitemap
|
|
205
294
|
/// <reference path="./nuxt-sitemap-virtual.d.ts" />
|
|
206
|
-
import type { SitemapUrl, SitemapItemDefaults, SitemapIndexRenderCtx, SitemapInputCtx, SitemapRenderCtx, SitemapOutputHookCtx, SitemapSourcesHookCtx } from '@nuxtjs/sitemap'
|
|
295
|
+
import type { SitemapUrl, SitemapItemDefaults, SitemapIndexRenderCtx, SitemapInputCtx, SitemapRenderCtx, SitemapOutputHookCtx, SitemapSourcesHookCtx, SitemapsResolvedCtx } from '@nuxtjs/sitemap'
|
|
207
296
|
|
|
208
297
|
${nitroTypes}
|
|
209
298
|
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
globalSitemapSources,
|
|
7
7
|
resolveSitemapSources
|
|
8
8
|
} from "../../sitemap/urlset/sources.js";
|
|
9
|
-
import {
|
|
9
|
+
import { useResolvedSitemapRuntimeConfig } from "../../utils.js";
|
|
10
10
|
function attachUrlWarnings(sources) {
|
|
11
11
|
for (const source of sources) {
|
|
12
12
|
if (!source.urls?.length)
|
|
@@ -26,7 +26,7 @@ function attachUrlWarnings(sources) {
|
|
|
26
26
|
return sources;
|
|
27
27
|
}
|
|
28
28
|
export default defineEventHandler(async (e) => {
|
|
29
|
-
const _runtimeConfig =
|
|
29
|
+
const _runtimeConfig = await useResolvedSitemapRuntimeConfig(e);
|
|
30
30
|
const siteConfig = getSiteConfig(e);
|
|
31
31
|
const { sitemaps: _sitemaps } = _runtimeConfig;
|
|
32
32
|
const runtimeConfig = { ..._runtimeConfig };
|
|
@@ -3,6 +3,7 @@ import { getHeader } from "#nuxtseo/h3";
|
|
|
3
3
|
import { defineCachedFunction } from "#nuxtseo/nitro";
|
|
4
4
|
import staticConfig from "#sitemap-virtual/static-config.mjs";
|
|
5
5
|
import { normaliseDate } from "../urlset/normalise.js";
|
|
6
|
+
import { parseChunkInfo } from "../utils/chunk.js";
|
|
6
7
|
import { getResolvedSitemapUrls } from "./sitemap.js";
|
|
7
8
|
const SERVER_CACHE_MAX_AGE = staticConfig.cacheMaxAgeSeconds || 60 * 10;
|
|
8
9
|
const buildSitemapIndexCached = defineCachedFunction(
|
|
@@ -43,7 +44,9 @@ async function buildSitemapIndexInternal(resolvers, runtimeConfig, nitro) {
|
|
|
43
44
|
sitemapConfig._isChunking = true;
|
|
44
45
|
sitemapConfig._chunkSize = sitemapConfig.chunkSize || (typeof sitemapConfig.chunks === "number" ? sitemapConfig.chunks : defaultSitemapsChunkSize || 1e3);
|
|
45
46
|
} else {
|
|
46
|
-
|
|
47
|
+
const chunkInfo = parseChunkInfo(sitemapName, sitemaps, defaultSitemapsChunkSize || void 0);
|
|
48
|
+
if (!chunkInfo.isChunked)
|
|
49
|
+
nonChunkedNames.push(sitemapName);
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
const indexLastmod = autoLastmod ? normaliseDate(/* @__PURE__ */ new Date()) : void 0;
|
|
@@ -50,7 +50,7 @@ export async function buildResolvedSitemapUrls(effectiveSitemap, matchName, isCh
|
|
|
50
50
|
if (typeof e._sitemap === "string" && !hasMatchingSitemap) {
|
|
51
51
|
if (!warnedSitemaps.has(e._sitemap)) {
|
|
52
52
|
warnedSitemaps.add(e._sitemap);
|
|
53
|
-
logger.error(`Sitemap \`${e._sitemap}\` not found in sitemap config. Available sitemaps: ${sitemapNames.join(", ")}. Entry \`${e.loc}\` will be omitted.`);
|
|
53
|
+
logger.error(`Sitemap \`${e._sitemap}\` not found in sitemap config. Available sitemaps: ${sitemapNames.join(", ")}. Either add it to the sitemap config or register it with the sitemap:sitemaps-resolved hook. Entry \`${e.loc}\` will be omitted.`);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -88,7 +88,13 @@ export const buildResolvedSitemapUrlsCached = defineCachedFunction(
|
|
|
88
88
|
const proto = getHeader(event, "x-forwarded-proto") || "https";
|
|
89
89
|
return `resolved-${isChunked ? "chunked-" : ""}${matchName}-${proto}-${host}`;
|
|
90
90
|
},
|
|
91
|
-
swr: true
|
|
91
|
+
swr: true,
|
|
92
|
+
// A build with failed sources is never cached: one outage must not pin an empty sitemap
|
|
93
|
+
// for a whole cache window. The next request retries the sources instead.
|
|
94
|
+
validate: (entry) => {
|
|
95
|
+
const value = entry.value;
|
|
96
|
+
return value !== void 0 && !value.failedSources?.length;
|
|
97
|
+
}
|
|
92
98
|
}
|
|
93
99
|
);
|
|
94
100
|
export async function getResolvedSitemapUrls(effectiveSitemap, matchName, isChunked, resolvers, runtimeConfig, nitro) {
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { joinURL, withBase, withLeadingSlash, withoutLeadingSlash, withoutTrailingSlash } from "ufo";
|
|
2
2
|
import { appendHeader, createError, getRequestURL, getRouterParam, sendRedirect } from "#nuxtseo/h3";
|
|
3
3
|
import { useNitroApp, useRuntimeConfig } from "#nuxtseo/nitro";
|
|
4
|
-
import {
|
|
4
|
+
import { useResolvedSitemapRuntimeConfig } from "../utils.js";
|
|
5
5
|
import { urlsToIndexXml, urlsToIndexXmlStream } from "./builder/index-xml.js";
|
|
6
6
|
import { buildSitemapIndex } from "./builder/sitemap-index.js";
|
|
7
7
|
import { createSitemap, renderSitemapOutput, setSitemapResponseHeaders, useNitroUrlResolvers } from "./nitro.js";
|
|
8
8
|
import { getSitemapConfig, parseChunkInfo } from "./utils/chunk.js";
|
|
9
9
|
export async function sitemapXmlEventHandler(e) {
|
|
10
|
-
const runtimeConfig =
|
|
10
|
+
const runtimeConfig = await useResolvedSitemapRuntimeConfig(e);
|
|
11
11
|
const { sitemaps } = runtimeConfig;
|
|
12
12
|
if ("index" in sitemaps)
|
|
13
13
|
return sendRedirect(e, withBase("/sitemap_index.xml", useRuntimeConfig().app.baseURL), import.meta.dev ? 302 : 301);
|
|
14
14
|
return createSitemap(e, Object.values(sitemaps)[0], runtimeConfig);
|
|
15
15
|
}
|
|
16
16
|
export async function sitemapIndexXmlEventHandler(e) {
|
|
17
|
-
const runtimeConfig =
|
|
17
|
+
const runtimeConfig = await useResolvedSitemapRuntimeConfig(e);
|
|
18
18
|
const nitro = useNitroApp();
|
|
19
19
|
const resolvers = useNitroUrlResolvers(e);
|
|
20
20
|
const { entries: sitemaps, failedSources } = await buildSitemapIndex(resolvers, runtimeConfig, nitro);
|
|
@@ -44,7 +44,7 @@ export async function sitemapChildXmlEventHandler(e) {
|
|
|
44
44
|
const pathname = getRequestURL(e).pathname;
|
|
45
45
|
if (!pathname.endsWith(".xml"))
|
|
46
46
|
return;
|
|
47
|
-
const runtimeConfig =
|
|
47
|
+
const runtimeConfig = await useResolvedSitemapRuntimeConfig(e);
|
|
48
48
|
const { sitemaps } = runtimeConfig;
|
|
49
49
|
let sitemapName = getRouterParam(e, "sitemap");
|
|
50
50
|
if (!sitemapName) {
|
|
@@ -66,7 +66,7 @@ export async function sitemapChildXmlEventHandler(e) {
|
|
|
66
66
|
sitemapName = withoutLeadingSlash(withoutTrailingSlash(sitemapName));
|
|
67
67
|
const chunkInfo = parseChunkInfo(sitemapName, sitemaps, runtimeConfig.defaultSitemapsChunkSize);
|
|
68
68
|
const isAutoChunked = typeof sitemaps.chunks !== "undefined" && !Number.isNaN(Number(sitemapName));
|
|
69
|
-
const sitemapExists = sitemapName
|
|
69
|
+
const sitemapExists = Object.hasOwn(sitemaps, sitemapName) || Object.hasOwn(sitemaps, chunkInfo.baseSitemapName) || isAutoChunked;
|
|
70
70
|
if (!sitemapExists)
|
|
71
71
|
throw createError({ statusCode: 404, message: `Sitemap "${sitemapName}" not found.` });
|
|
72
72
|
if (chunkInfo.isChunked && chunkInfo.chunkIndex !== void 0) {
|
|
@@ -2,8 +2,9 @@ import { defu } from "defu";
|
|
|
2
2
|
import { $fetch } from "ofetch";
|
|
3
3
|
import { collectSitemap } from "sitemapd/parse";
|
|
4
4
|
import { parseURL } from "ufo";
|
|
5
|
-
import { getRequestHost } from "#nuxtseo/h3";
|
|
6
|
-
import { fetchWithEvent } from "#nuxtseo/nitro";
|
|
5
|
+
import { getHeader, getRequestHost } from "#nuxtseo/h3";
|
|
6
|
+
import { defineCachedFunction, fetchWithEvent } from "#nuxtseo/nitro";
|
|
7
|
+
import staticConfig from "#sitemap-virtual/static-config.mjs";
|
|
7
8
|
import { logger } from "../../../utils-pure.js";
|
|
8
9
|
const changeFrequencies = /* @__PURE__ */ new Set([
|
|
9
10
|
"always",
|
|
@@ -69,10 +70,78 @@ async function tryFetchWithFallback(url, options, event) {
|
|
|
69
70
|
}
|
|
70
71
|
return event ? await fetchWithEvent(event, url, options) : await globalThis.$fetch(url, options);
|
|
71
72
|
}
|
|
73
|
+
const SOURCE_FETCH_MEMO_KEY = "_sitemapSourceFetches";
|
|
74
|
+
const SERVER_CACHE_MAX_AGE = staticConfig.cacheMaxAgeSeconds || 60 * 10;
|
|
75
|
+
const prerenderSourceFetches = import.meta.prerender ? /* @__PURE__ */ new Map() : void 0;
|
|
76
|
+
function useSourceFetchMemo(event) {
|
|
77
|
+
if (import.meta.prerender)
|
|
78
|
+
return prerenderSourceFetches;
|
|
79
|
+
const context = event?.context;
|
|
80
|
+
if (!context)
|
|
81
|
+
return void 0;
|
|
82
|
+
const existing = context[SOURCE_FETCH_MEMO_KEY];
|
|
83
|
+
if (existing)
|
|
84
|
+
return existing;
|
|
85
|
+
const memo = /* @__PURE__ */ new Map();
|
|
86
|
+
context[SOURCE_FETCH_MEMO_KEY] = memo;
|
|
87
|
+
return memo;
|
|
88
|
+
}
|
|
89
|
+
function hashCacheKey(key) {
|
|
90
|
+
let h1 = 3735928559;
|
|
91
|
+
let h2 = 1103547991;
|
|
92
|
+
for (let i = 0; i < key.length; i++) {
|
|
93
|
+
const ch = key.charCodeAt(i);
|
|
94
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
95
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
96
|
+
}
|
|
97
|
+
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
98
|
+
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
99
|
+
return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36);
|
|
100
|
+
}
|
|
101
|
+
const fetchSourceUrlsCached = defineCachedFunction(
|
|
102
|
+
(event, _key, url, options) => fetchSourceUrls(url, options, event),
|
|
103
|
+
{
|
|
104
|
+
name: "sitemap:source-urls",
|
|
105
|
+
group: "sitemap",
|
|
106
|
+
base: "sitemap",
|
|
107
|
+
maxAge: SERVER_CACHE_MAX_AGE,
|
|
108
|
+
getKey: (event, key) => {
|
|
109
|
+
const host = getHeader(event, "host") || getHeader(event, "x-forwarded-host") || "";
|
|
110
|
+
const proto = getHeader(event, "x-forwarded-proto") || "https";
|
|
111
|
+
return `source-${proto}-${host}-${hashCacheKey(key)}`;
|
|
112
|
+
},
|
|
113
|
+
swr: true,
|
|
114
|
+
// A failed fetch must never be served again, otherwise one outage empties the sitemap for a
|
|
115
|
+
// whole cache window.
|
|
116
|
+
validate: (entry) => {
|
|
117
|
+
const value = entry.value;
|
|
118
|
+
return value !== void 0 && !value._isFailure;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
function isSourceCacheEnabled() {
|
|
123
|
+
if (import.meta.dev || import.meta.prerender)
|
|
124
|
+
return false;
|
|
125
|
+
const cacheMaxAgeSeconds = staticConfig.cacheMaxAgeSeconds;
|
|
126
|
+
return typeof cacheMaxAgeSeconds === "number" && cacheMaxAgeSeconds > 0;
|
|
127
|
+
}
|
|
72
128
|
export async function fetchDataSource(input, event) {
|
|
73
129
|
const context = typeof input.context === "string" ? { name: input.context } : input.context || { name: "fetch" };
|
|
74
130
|
const url = typeof input.fetch === "string" ? input.fetch : input.fetch[0];
|
|
75
131
|
const options = typeof input.fetch === "string" ? {} : input.fetch[1];
|
|
132
|
+
const memo = useSourceFetchMemo(event);
|
|
133
|
+
const key = `${url}::${JSON.stringify(options || {})}`;
|
|
134
|
+
let request = memo?.get(key);
|
|
135
|
+
if (!request) {
|
|
136
|
+
request = event && isSourceCacheEnabled() ? fetchSourceUrlsCached(event, key, url, options) : fetchSourceUrls(url, options, event);
|
|
137
|
+
memo?.set(key, request);
|
|
138
|
+
}
|
|
139
|
+
const result = await request;
|
|
140
|
+
if (result._isFailure)
|
|
141
|
+
memo?.delete(key);
|
|
142
|
+
return { ...input, context, ...result };
|
|
143
|
+
}
|
|
144
|
+
async function fetchSourceUrls(url, options, event) {
|
|
76
145
|
const start = Date.now();
|
|
77
146
|
const isExternalUrl = !url.startsWith("/");
|
|
78
147
|
const timeout = isExternalUrl ? 1e4 : options.timeout || 5e3;
|
|
@@ -113,11 +182,12 @@ export async function fetchDataSource(input, event) {
|
|
|
113
182
|
const timeTakenMs = Date.now() - start;
|
|
114
183
|
if (isMaybeErrorResponse) {
|
|
115
184
|
return {
|
|
116
|
-
...input,
|
|
117
|
-
context,
|
|
118
185
|
urls: [],
|
|
119
186
|
timeTakenMs,
|
|
120
|
-
error: "Received HTML response instead of JSON"
|
|
187
|
+
error: "Received HTML response instead of JSON",
|
|
188
|
+
// An HTML page is usually an outage or an auth wall, both transient. Treat it like a
|
|
189
|
+
// failed fetch so the empty result is never cached.
|
|
190
|
+
_isFailure: true
|
|
121
191
|
};
|
|
122
192
|
}
|
|
123
193
|
let urls = [];
|
|
@@ -133,8 +203,6 @@ export async function fetchDataSource(input, event) {
|
|
|
133
203
|
urls = res.urls || res;
|
|
134
204
|
}
|
|
135
205
|
return {
|
|
136
|
-
...input,
|
|
137
|
-
context,
|
|
138
206
|
timeTakenMs,
|
|
139
207
|
urls
|
|
140
208
|
};
|
|
@@ -154,8 +222,6 @@ export async function fetchDataSource(input, event) {
|
|
|
154
222
|
logger.error("Failed to fetch source.", { url, error: error.message });
|
|
155
223
|
}
|
|
156
224
|
return {
|
|
157
|
-
...input,
|
|
158
|
-
context,
|
|
159
225
|
urls: [],
|
|
160
226
|
error: error.message,
|
|
161
227
|
_isFailure: true
|
|
@@ -179,6 +245,15 @@ export async function globalSitemapSources() {
|
|
|
179
245
|
return [...m.sources];
|
|
180
246
|
}
|
|
181
247
|
export async function childSitemapSources(definition) {
|
|
248
|
+
if (definition?.sources?.length)
|
|
249
|
+
return [...definition.sources];
|
|
250
|
+
if (definition?.urls) {
|
|
251
|
+
const urls = typeof definition.urls === "function" ? await definition.urls() : definition.urls;
|
|
252
|
+
return [{
|
|
253
|
+
context: { name: `sitemaps:${definition.sitemapName}:urls`, description: "Set with the sitemap definition `urls`." },
|
|
254
|
+
urls
|
|
255
|
+
}];
|
|
256
|
+
}
|
|
182
257
|
if (!definition?._hasSourceChunk)
|
|
183
258
|
return [];
|
|
184
259
|
if (import.meta.prerender) {
|
|
@@ -2,3 +2,4 @@ import type { H3Event } from '#nuxtseo/h3';
|
|
|
2
2
|
import type { ModuleRuntimeConfig } from '../types.js';
|
|
3
3
|
export * from '../utils-pure.js';
|
|
4
4
|
export declare function useSitemapRuntimeConfig(e?: H3Event): ModuleRuntimeConfig;
|
|
5
|
+
export declare function useResolvedSitemapRuntimeConfig(e: H3Event): Promise<ModuleRuntimeConfig>;
|
|
@@ -1,18 +1,74 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getHeader } from "#nuxtseo/h3";
|
|
2
|
+
import { defineCachedFunction, useNitroApp, useRuntimeConfig } from "#nuxtseo/nitro";
|
|
2
3
|
import staticConfig from "#sitemap-virtual/static-config.mjs";
|
|
3
4
|
import { normalizeRuntimeFilters } from "../utils-pure.js";
|
|
4
5
|
export * from "../utils-pure.js";
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
const SERVER_CACHE_MAX_AGE = staticConfig.cacheMaxAgeSeconds || 60 * 10;
|
|
7
|
+
function dynamicRuntimeConfig(e) {
|
|
8
|
+
return useRuntimeConfig(e).sitemap;
|
|
9
|
+
}
|
|
10
|
+
function copyStaticSitemaps() {
|
|
11
|
+
return Object.fromEntries(
|
|
7
12
|
Object.entries(staticConfig.sitemaps).map(([name, sitemap]) => [name, {
|
|
8
13
|
...sitemap,
|
|
9
14
|
include: normalizeRuntimeFilters("include" in sitemap ? sitemap.include : void 0),
|
|
10
15
|
exclude: normalizeRuntimeFilters("exclude" in sitemap ? sitemap.exclude : void 0)
|
|
11
16
|
}])
|
|
12
17
|
);
|
|
18
|
+
}
|
|
19
|
+
export function useSitemapRuntimeConfig(e) {
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
...staticConfig,
|
|
22
|
+
sitemaps: copyStaticSitemaps(),
|
|
23
|
+
...dynamicRuntimeConfig(e)
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function serializeFilters(filters) {
|
|
27
|
+
if (!filters?.length)
|
|
28
|
+
return void 0;
|
|
29
|
+
return filters.map((f) => {
|
|
30
|
+
if (f instanceof RegExp)
|
|
31
|
+
return { regex: `/${f.source}/${f.flags}` };
|
|
32
|
+
return f;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async function resolveSitemapSitemaps(e, nitro) {
|
|
36
|
+
const ctx = { sitemaps: copyStaticSitemaps(), event: e };
|
|
37
|
+
await nitro.hooks.callHook("sitemap:sitemaps-resolved", ctx);
|
|
38
|
+
const sitemaps = { ...ctx.sitemaps };
|
|
39
|
+
for (const name of Object.keys(sitemaps)) {
|
|
40
|
+
const sitemap = { ...sitemaps[name] };
|
|
41
|
+
if (typeof sitemap.urls === "function")
|
|
42
|
+
sitemap.urls = await sitemap.urls();
|
|
43
|
+
sitemap.include = serializeFilters(sitemap.include);
|
|
44
|
+
sitemap.exclude = serializeFilters(sitemap.exclude);
|
|
45
|
+
sitemaps[name] = sitemap;
|
|
46
|
+
}
|
|
47
|
+
return sitemaps;
|
|
48
|
+
}
|
|
49
|
+
const resolveSitemapSitemapsCached = defineCachedFunction(
|
|
50
|
+
resolveSitemapSitemaps,
|
|
51
|
+
{
|
|
52
|
+
name: "sitemap:runtime-sitemaps",
|
|
53
|
+
group: "sitemap",
|
|
54
|
+
maxAge: SERVER_CACHE_MAX_AGE,
|
|
55
|
+
base: "sitemap",
|
|
56
|
+
// nitro calls getKey with the full fn args (event, nitro)
|
|
57
|
+
getKey: (e) => {
|
|
58
|
+
const host = e && (getHeader(e, "host") || getHeader(e, "x-forwarded-host")) || "";
|
|
59
|
+
const proto = e && getHeader(e, "x-forwarded-proto") || "https";
|
|
60
|
+
return `runtime-sitemaps-${proto}-${host}`;
|
|
61
|
+
},
|
|
62
|
+
swr: true
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
export async function useResolvedSitemapRuntimeConfig(e) {
|
|
66
|
+
const maxAge = dynamicRuntimeConfig(e)?.cacheMaxAgeSeconds ?? staticConfig.cacheMaxAgeSeconds;
|
|
67
|
+
const shouldCache = !import.meta.dev && !import.meta.prerender && typeof maxAge === "number" && maxAge > 0;
|
|
68
|
+
const sitemaps = shouldCache ? await resolveSitemapSitemapsCached(e, useNitroApp()) : await resolveSitemapSitemaps(e, useNitroApp());
|
|
13
69
|
return Object.freeze({
|
|
14
70
|
...staticConfig,
|
|
15
71
|
sitemaps,
|
|
16
|
-
...
|
|
72
|
+
...dynamicRuntimeConfig(e)
|
|
17
73
|
});
|
|
18
74
|
}
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -245,7 +245,7 @@ export interface ModuleRuntimeConfig extends Pick<ModuleOptions, 'sitemapsPathPr
|
|
|
245
245
|
index?: Pick<SitemapDefinition, 'sitemapName' | '_route'> & {
|
|
246
246
|
sitemaps: SitemapIndexEntry[];
|
|
247
247
|
};
|
|
248
|
-
} & Record<string,
|
|
248
|
+
} & Record<string, SitemapDefinition & {
|
|
249
249
|
_hasSourceChunk?: boolean;
|
|
250
250
|
}>;
|
|
251
251
|
autoI18n?: AutoI18nConfig;
|
|
@@ -398,6 +398,20 @@ export interface SitemapSourcesHookCtx<Event = H3Event> extends NitroBaseHook<Ev
|
|
|
398
398
|
sitemapName: string;
|
|
399
399
|
sources: SitemapSourceInput[];
|
|
400
400
|
}
|
|
401
|
+
export interface SitemapsResolvedCtx<Event = H3Event> extends NitroBaseHook<Event> {
|
|
402
|
+
/**
|
|
403
|
+
* The sitemaps config about to be used to build the sitemap index and serve child
|
|
404
|
+
* sitemaps. Static definitions from nuxt.config are already present.
|
|
405
|
+
*
|
|
406
|
+
* Push definitions here to register sitemaps at runtime, for example when the data set
|
|
407
|
+
* grows while the server is running. Delete a key to remove its sitemap from the index
|
|
408
|
+
* and stop serving it. Registered sitemaps accept the same fields as nuxt.config
|
|
409
|
+
* definitions (`sources`, `urls`, `include`, `exclude`, `defaults`, `chunks`) and are
|
|
410
|
+
* served, listed in the sitemap index, and passed the `sitemap:sources` hook like
|
|
411
|
+
* static ones.
|
|
412
|
+
*/
|
|
413
|
+
sitemaps: ModuleRuntimeConfig['sitemaps'];
|
|
414
|
+
}
|
|
401
415
|
export type Changefreq = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
|
|
402
416
|
export interface SitemapUrl {
|
|
403
417
|
loc: string;
|
|
@@ -413,6 +427,10 @@ export interface SitemapUrl {
|
|
|
413
427
|
images?: Array<ImageEntry>;
|
|
414
428
|
videos?: Array<VideoEntry>;
|
|
415
429
|
_i18nTransform?: boolean;
|
|
430
|
+
/**
|
|
431
|
+
* Route this URL to a specific sitemap. The name must exist in the sitemap config,
|
|
432
|
+
* either set in `nuxt.config` or registered with the `sitemap:sitemaps-resolved` hook.
|
|
433
|
+
*/
|
|
416
434
|
_sitemap?: string | false;
|
|
417
435
|
/**
|
|
418
436
|
* Mark the URL as already encoded.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuxtjs/sitemap",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "8.
|
|
4
|
+
"version": "8.5.0",
|
|
5
5
|
"description": "Powerfully flexible XML Sitemaps that integrate seamlessly, for Nuxt.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
74
74
|
"@harlan-zw/comark-content": "^0.1.3",
|
|
75
75
|
"@nuxt/content": "^3.15.2",
|
|
76
|
-
"@nuxt/devtools-kit": "4.0.0-alpha.
|
|
76
|
+
"@nuxt/devtools-kit": "4.0.0-alpha.12",
|
|
77
77
|
"@nuxt/module-builder": "^1.0.3",
|
|
78
78
|
"@nuxt/test-utils": "^4.1.0",
|
|
79
79
|
"@nuxt/ui": "^4.10.0",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"bumpp": "^12.2.1",
|
|
85
85
|
"eslint": "^10.8.1",
|
|
86
86
|
"eslint-plugin-harlanzw": "^0.20.0",
|
|
87
|
-
"happy-dom": "^20.11.
|
|
87
|
+
"happy-dom": "^20.11.6",
|
|
88
88
|
"nuxt": "^4.5.2",
|
|
89
89
|
"nuxt-i18n-micro": "^3.28.0",
|
|
90
90
|
"nuxtseo-layer-devtools": "^5.3.14",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"vitest": "^4.1.11",
|
|
95
95
|
"vue-tsc": "^3.3.10",
|
|
96
96
|
"zod": "^4.4.3",
|
|
97
|
-
"@nuxtjs/sitemap": "8.
|
|
97
|
+
"@nuxtjs/sitemap": "8.5.0"
|
|
98
98
|
},
|
|
99
99
|
"scripts": {
|
|
100
100
|
"lint": "eslint .",
|