@nuxtjs/sitemap 8.3.4 → 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/devtools/nuxt.config.ts +4 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +201 -49
- package/dist/runtime/server/routes/__sitemap__/content-urls.d.ts +9 -0
- package/dist/runtime/server/routes/__sitemap__/content-urls.js +34 -0
- 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 +20 -2
- package/package.json +14 -13
- package/dist/runtime/server/routes/__sitemap__/nuxt-content-urls-v3.d.ts +0 -2
- package/dist/runtime/server/routes/__sitemap__/nuxt-content-urls-v3.js +0 -40
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
1
2
|
import { resolve } from 'pathe'
|
|
2
3
|
|
|
4
|
+
const currentDir = fileURLToPath(new URL('.', import.meta.url))
|
|
5
|
+
|
|
3
6
|
// Nuxt SEO devtools panel, shipped as a layer (Model C). Components flat-registered
|
|
4
7
|
// so intra-panel references resolve by name.
|
|
5
8
|
export default defineNuxtConfig({
|
|
6
|
-
components: [{ path: resolve(
|
|
9
|
+
components: [{ path: resolve(currentDir, './components'), pathPrefix: false }],
|
|
7
10
|
})
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -2,15 +2,18 @@ import { useNuxt, hasNuxtModule, addTypeTemplate, addTemplate, defineNuxtModule,
|
|
|
2
2
|
import { defu } from 'defu';
|
|
3
3
|
import { withSiteUrl, installNuxtSiteConfig } from 'nuxt-site-config/kit';
|
|
4
4
|
import { isPathFile } from 'nuxt-site-config/urls';
|
|
5
|
-
import { isNuxtGenerate, renderNitroTypeAugmentations, useModuleLogger, setupNitroRuntimeCompatibility, getNuxtModuleOptions, resolveNitroPreset,
|
|
5
|
+
import { isNuxtGenerate, renderNitroTypeAugmentations, useModuleLogger, setupNitroRuntimeCompatibility, getNuxtModuleOptions, resolveNitroPreset, resolveContentProvider, setupContentRuntime, createPagesPromise, createNitroPromise } from 'nuxtseo-shared/kit';
|
|
6
6
|
import { serializeFilters } from 'nuxtseo-shared/utils';
|
|
7
7
|
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
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
13
|
-
import { join } from 'node:path';
|
|
13
|
+
import { rm, mkdir, writeFile } from 'node:fs/promises';
|
|
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';
|
|
@@ -18,6 +21,9 @@ import { normaliseDate } from '../dist/runtime/server/sitemap/urlset/normalise.j
|
|
|
18
21
|
import { mapPathForI18nPages, splitPathForI18nLocales as splitPathForI18nLocales$1, expandCompactLocaleRoute, normalizeLocales, generatePathForI18nPages } from 'nuxtseo-shared/i18n';
|
|
19
22
|
import 'ultrahtml';
|
|
20
23
|
|
|
24
|
+
const COMARK_CONTENT_SOURCE = "@harlan-zw/comark-content:urls";
|
|
25
|
+
const COMARK_CONTENT_SITEMAP_ROUTE = "/__sitemap__/comark-content-urls.json";
|
|
26
|
+
|
|
21
27
|
function setupDevToolsUI(_options, resolve, nuxt = useNuxt()) {
|
|
22
28
|
setupDevToolsUI$1(
|
|
23
29
|
{ route: "/__nuxt-sitemap", name: "sitemap", title: "Sitemap", icon: "carbon:load-balancer-application" },
|
|
@@ -72,6 +78,14 @@ export async function readSourcesFromFilesystem(filename) {
|
|
|
72
78
|
`;
|
|
73
79
|
});
|
|
74
80
|
nuxt.hooks.hook("nitro:init", async (nitro) => {
|
|
81
|
+
let prerendererNitro = nitro;
|
|
82
|
+
nitro.hooks.hook("prerender:init", (prerenderer) => {
|
|
83
|
+
prerendererNitro = prerenderer;
|
|
84
|
+
});
|
|
85
|
+
await Promise.all([
|
|
86
|
+
rm(join(runtimeAssetsPath, "global-sources.json"), { force: true }),
|
|
87
|
+
rm(join(runtimeAssetsPath, "child-sources.json"), { force: true })
|
|
88
|
+
]);
|
|
75
89
|
nitro.hooks.hook("prerender:generate", async (route) => {
|
|
76
90
|
const html = route.contents;
|
|
77
91
|
if (!route.fileName?.endsWith(".html") || !html || ["/200.html", "/404.html"].includes(route.route))
|
|
@@ -121,12 +135,101 @@ export async function readSourcesFromFilesystem(filename) {
|
|
|
121
135
|
await writeFile(join(runtimeAssetsPath, "global-sources.json"), JSON.stringify(globalSources));
|
|
122
136
|
await writeFile(join(runtimeAssetsPath, "child-sources.json"), JSON.stringify(childSources));
|
|
123
137
|
const sitemapEntry = options.isMultiSitemap ? "/sitemap_index.xml" : `/${Object.keys(options.sitemaps)[0]}`;
|
|
124
|
-
const
|
|
125
|
-
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);
|
|
126
140
|
});
|
|
127
141
|
});
|
|
128
142
|
}
|
|
129
|
-
|
|
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) {
|
|
130
233
|
const sitemaps = [];
|
|
131
234
|
const queue = [entry];
|
|
132
235
|
const processed = /* @__PURE__ */ new Set();
|
|
@@ -135,7 +238,7 @@ async function prerenderSitemapsFromEntry(nitro, entry) {
|
|
|
135
238
|
if (processed.has(route))
|
|
136
239
|
continue;
|
|
137
240
|
processed.add(route);
|
|
138
|
-
const { filePath, prerenderUrls } = await prerenderRoute(nitro, route);
|
|
241
|
+
const { filePath, prerenderUrls } = await prerenderRoute(nitro, fetch, route);
|
|
139
242
|
sitemaps.push({
|
|
140
243
|
name: route,
|
|
141
244
|
get content() {
|
|
@@ -146,27 +249,19 @@ async function prerenderSitemapsFromEntry(nitro, entry) {
|
|
|
146
249
|
}
|
|
147
250
|
return sitemaps;
|
|
148
251
|
}
|
|
149
|
-
async function prerenderRoute(nitro, route) {
|
|
252
|
+
async function prerenderRoute(nitro, fetch, route) {
|
|
150
253
|
const start = Date.now();
|
|
151
254
|
const _route = { route, fileName: route };
|
|
152
255
|
const encodedRoute = encodeURI(route);
|
|
153
256
|
const fetchUrl = withBase(encodedRoute, nitro.options.baseURL);
|
|
154
|
-
const res = await
|
|
155
|
-
|
|
156
|
-
{
|
|
157
|
-
headers: { "x-nitro-prerender": encodedRoute },
|
|
158
|
-
retry: nitro.options.prerender.retry,
|
|
159
|
-
retryDelay: nitro.options.prerender.retryDelay
|
|
160
|
-
}
|
|
161
|
-
);
|
|
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}`);
|
|
162
260
|
const header = res.headers.get("x-nitro-prerender") || "";
|
|
163
261
|
const prerenderUrls = header.split(",").map((i) => decodeURIComponent(i.trim())).filter(Boolean);
|
|
164
262
|
const filePath = join(nitro.options.output.publicDir, _route.fileName);
|
|
165
263
|
await mkdir(dirname(filePath), { recursive: true });
|
|
166
|
-
const
|
|
167
|
-
if (data === void 0)
|
|
168
|
-
throw new Error(`No data returned from '${fetchUrl}'`);
|
|
169
|
-
const content = filePath.endsWith("json") || typeof data === "object" ? JSON.stringify(data) : data;
|
|
264
|
+
const content = await res.text();
|
|
170
265
|
await writeFile(filePath, content, "utf8");
|
|
171
266
|
_route.generateTimeMS = Date.now() - start;
|
|
172
267
|
nitro._prerenderedRoutes.push(_route);
|
|
@@ -185,7 +280,8 @@ function registerTypeTemplates(nitroCompatibility) {
|
|
|
185
280
|
'sitemap:input': (ctx: SitemapInputCtx<${nitroCompatibility.eventType}>) => void | Promise<void>
|
|
186
281
|
'sitemap:resolved': (ctx: SitemapRenderCtx<${nitroCompatibility.eventType}>) => void | Promise<void>
|
|
187
282
|
'sitemap:output': (ctx: SitemapOutputHookCtx<${nitroCompatibility.eventType}>) => void | Promise<void>
|
|
188
|
-
'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>`;
|
|
189
285
|
const nitroTypes = renderNitroTypeAugmentations(nitroCompatibility, {
|
|
190
286
|
nitroInterfaces: {
|
|
191
287
|
PrerenderRoute: "_sitemap?: SitemapUrl"
|
|
@@ -196,7 +292,7 @@ function registerTypeTemplates(nitroCompatibility) {
|
|
|
196
292
|
});
|
|
197
293
|
return `// Generated by @nuxtjs/sitemap
|
|
198
294
|
/// <reference path="./nuxt-sitemap-virtual.d.ts" />
|
|
199
|
-
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'
|
|
200
296
|
|
|
201
297
|
${nitroTypes}
|
|
202
298
|
|
|
@@ -455,7 +551,34 @@ function generateExtraRoutesFromNuxtConfig(nuxt = useNuxt()) {
|
|
|
455
551
|
}).map(([k]) => k).filter(filterForValidPage);
|
|
456
552
|
return { routeRules };
|
|
457
553
|
}
|
|
554
|
+
function resolveExcludedAppSources(resolved, authored) {
|
|
555
|
+
if (resolved === true || authored === true)
|
|
556
|
+
return true;
|
|
557
|
+
if (!Array.isArray(authored))
|
|
558
|
+
return resolved;
|
|
559
|
+
const excluded = [...resolved];
|
|
560
|
+
for (const source of authored) {
|
|
561
|
+
if (typeof source === "string" && !excluded.includes(source))
|
|
562
|
+
excluded.push(source);
|
|
563
|
+
}
|
|
564
|
+
return excluded;
|
|
565
|
+
}
|
|
458
566
|
|
|
567
|
+
const IMAGE_TAGS = /* @__PURE__ */ new Set(["image", "img", "nuxtimg", "nuxt-img"]);
|
|
568
|
+
function discoverContentImages(body) {
|
|
569
|
+
const images = [];
|
|
570
|
+
const walk = (nodes) => {
|
|
571
|
+
for (const node of nodes || []) {
|
|
572
|
+
if (!Array.isArray(node) || typeof node[0] !== "string")
|
|
573
|
+
continue;
|
|
574
|
+
if (IMAGE_TAGS.has(node[0]) && node[1]?.src)
|
|
575
|
+
images.push({ loc: node[1].src });
|
|
576
|
+
walk(node.slice(2));
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
walk(body?.value ?? body?.nodes);
|
|
580
|
+
return images;
|
|
581
|
+
}
|
|
459
582
|
const module$1 = defineNuxtModule({
|
|
460
583
|
meta: {
|
|
461
584
|
name: "@nuxtjs/sitemap",
|
|
@@ -480,6 +603,10 @@ const module$1 = defineNuxtModule({
|
|
|
480
603
|
version: ">=2",
|
|
481
604
|
optional: true
|
|
482
605
|
},
|
|
606
|
+
"@harlan-zw/comark-content": {
|
|
607
|
+
version: ">=0.1.2",
|
|
608
|
+
optional: true
|
|
609
|
+
},
|
|
483
610
|
"@nuxtjs/robots": {
|
|
484
611
|
version: ">=4",
|
|
485
612
|
optional: true
|
|
@@ -673,11 +800,11 @@ const module$1 = defineNuxtModule({
|
|
|
673
800
|
const hasCustomI18nSitemaps = i18nSitemaps.length > 0;
|
|
674
801
|
if (hasCustomI18nSitemaps) {
|
|
675
802
|
for (const { name: name2, cfg } of i18nSitemaps) {
|
|
803
|
+
const { sitemapName: _sitemapName, _route, _isChunking, _chunkSize, _chunkCount, ...inheritedConfig } = cfg;
|
|
676
804
|
for (const locale of resolvedAutoI18n.locales) {
|
|
677
805
|
newSitemaps[`${locale._sitemap}-${name2}`] = {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
...cfg.include?.length && { include: cfg.include }
|
|
806
|
+
...inheritedConfig,
|
|
807
|
+
includeAppSources: true
|
|
681
808
|
};
|
|
682
809
|
}
|
|
683
810
|
}
|
|
@@ -770,20 +897,13 @@ const module$1 = defineNuxtModule({
|
|
|
770
897
|
addServerPlugin(resolve("./runtime/server/plugins/stream-transport"));
|
|
771
898
|
}
|
|
772
899
|
const isNuxtContentDocumentDriven = !!nuxt.options.content?.documentDriven || config.strictNuxtContentPaths;
|
|
773
|
-
const
|
|
774
|
-
const isNuxtContentV3 =
|
|
900
|
+
const contentProvider = await resolveContentProvider(nuxt);
|
|
901
|
+
const isNuxtContentV3 = contentProvider._tag === "NuxtContent" && contentProvider.version === 3;
|
|
902
|
+
const isNuxtContentV2 = contentProvider._tag === "NuxtContent" && contentProvider.version === 2;
|
|
903
|
+
const isComarkContent = contentProvider._tag === "Comark";
|
|
775
904
|
const nuxtV3Collections = /* @__PURE__ */ new Set();
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
if (nuxt.options._installedModules.some((m) => m.meta.name === "Content")) {
|
|
779
|
-
logger.warn("You have loaded `@nuxt/content` before `@nuxtjs/sitemap`, this may cause issues with the integration. Please ensure `@nuxtjs/sitemap` is loaded first.");
|
|
780
|
-
}
|
|
781
|
-
config.exclude.push("/__nuxt_content/**");
|
|
782
|
-
const needsCustomAlias = await hasNuxtModuleCompatibility("@nuxt/content", "<3.6.0");
|
|
783
|
-
if (needsCustomAlias) {
|
|
784
|
-
nuxt.options.alias["#sitemap/content-v3-nitro-path"] = resolve(dirname(resolveModule("@nuxt/content")), "runtime/nitro");
|
|
785
|
-
nuxt.options.alias["@nuxt/content/nitro"] = resolve("./runtime/server/content-compat");
|
|
786
|
-
}
|
|
905
|
+
setupContentRuntime(contentProvider, nuxt);
|
|
906
|
+
const registerContentSitemapHook = (options) => {
|
|
787
907
|
nuxt.hooks.hook("content:file:afterParse", (ctx) => {
|
|
788
908
|
try {
|
|
789
909
|
const content = ctx.content;
|
|
@@ -792,7 +912,7 @@ const module$1 = defineNuxtModule({
|
|
|
792
912
|
ctx.content.sitemap = null;
|
|
793
913
|
return;
|
|
794
914
|
}
|
|
795
|
-
if (!ctx.collection.fields || !("sitemap" in ctx.collection.fields)) {
|
|
915
|
+
if (options.requireCollectionField && (!ctx.collection.fields || !("sitemap" in ctx.collection.fields))) {
|
|
796
916
|
ctx.content.sitemap = null;
|
|
797
917
|
return;
|
|
798
918
|
}
|
|
@@ -805,13 +925,8 @@ const module$1 = defineNuxtModule({
|
|
|
805
925
|
return;
|
|
806
926
|
}
|
|
807
927
|
const images = [];
|
|
808
|
-
if (config.discoverImages)
|
|
809
|
-
images.push(
|
|
810
|
-
...content.body?.value?.filter(
|
|
811
|
-
(c) => ["image", "img", "nuxtimg", "nuxt-img"].includes(c[0])
|
|
812
|
-
).filter((c) => c[1]?.src).map((c) => ({ loc: c[1].src })) || []
|
|
813
|
-
);
|
|
814
|
-
}
|
|
928
|
+
if (config.discoverImages)
|
|
929
|
+
images.push(...discoverContentImages(content.body));
|
|
815
930
|
const lastmod = content.seo?.articleModifiedTime || content.updatedAt;
|
|
816
931
|
const defaults = {
|
|
817
932
|
loc: content.path
|
|
@@ -825,6 +940,8 @@ const module$1 = defineNuxtModule({
|
|
|
825
940
|
logger.warn(`Failed to process sitemap data for content file (collection: ${ctx.collection?.name}, path: ${ctx.content?.path}), skipping.`, e);
|
|
826
941
|
}
|
|
827
942
|
});
|
|
943
|
+
};
|
|
944
|
+
const addContentCallbackVirtuals = () => {
|
|
828
945
|
nuxt.hook("nitro:config", (nitroConfig) => {
|
|
829
946
|
const filterEntries = [];
|
|
830
947
|
if (globalThis.__sitemapCollectionFilters) {
|
|
@@ -842,9 +959,22 @@ ${filterEntries.join("\n")}`;
|
|
|
842
959
|
nitroConfig.virtual["#sitemap/content-on-url"] = `export const onUrlFns = new Map()
|
|
843
960
|
${onUrlEntries.join("\n")}`;
|
|
844
961
|
});
|
|
962
|
+
};
|
|
963
|
+
if (isNuxtContentV3) {
|
|
964
|
+
if (nuxt.options._installedModules.some((m) => m.meta.name === "Content")) {
|
|
965
|
+
logger.warn("You have loaded `@nuxt/content` before `@nuxtjs/sitemap`, this may cause issues with the integration. Please ensure `@nuxtjs/sitemap` is loaded first.");
|
|
966
|
+
}
|
|
967
|
+
config.exclude.push("/__nuxt_content/**");
|
|
968
|
+
const needsCustomAlias = await hasNuxtModuleCompatibility("@nuxt/content", "<3.6.0");
|
|
969
|
+
if (needsCustomAlias) {
|
|
970
|
+
nuxt.options.alias["#sitemap/content-v3-nitro-path"] = resolve(dirname(resolveModule("@nuxt/content")), "runtime/nitro");
|
|
971
|
+
nuxt.options.alias["@nuxt/content/nitro"] = resolve("./runtime/server/content-compat");
|
|
972
|
+
}
|
|
973
|
+
registerContentSitemapHook({ requireCollectionField: true });
|
|
974
|
+
addContentCallbackVirtuals();
|
|
845
975
|
addServerHandler({
|
|
846
976
|
route: "/__sitemap__/nuxt-content-urls.json",
|
|
847
|
-
handler: resolve("./runtime/server/routes/__sitemap__/
|
|
977
|
+
handler: resolve("./runtime/server/routes/__sitemap__/content-urls")
|
|
848
978
|
});
|
|
849
979
|
if (config.strictNuxtContentPaths) {
|
|
850
980
|
logger.warn("You have set `strictNuxtContentPaths: true` but are using @nuxt/content v3. This is not required, please remove it.");
|
|
@@ -857,6 +987,24 @@ ${onUrlEntries.join("\n")}`;
|
|
|
857
987
|
},
|
|
858
988
|
fetch: "/__sitemap__/nuxt-content-urls.json"
|
|
859
989
|
});
|
|
990
|
+
} else if (isComarkContent) {
|
|
991
|
+
registerContentSitemapHook({ requireCollectionField: false });
|
|
992
|
+
addContentCallbackVirtuals();
|
|
993
|
+
addServerHandler({
|
|
994
|
+
route: COMARK_CONTENT_SITEMAP_ROUTE,
|
|
995
|
+
handler: resolve("./runtime/server/routes/__sitemap__/content-urls")
|
|
996
|
+
});
|
|
997
|
+
if (config.strictNuxtContentPaths) {
|
|
998
|
+
logger.warn("You have set `strictNuxtContentPaths: true` but are using comark-content. This is not required, please remove it.");
|
|
999
|
+
}
|
|
1000
|
+
appGlobalSources.push({
|
|
1001
|
+
context: {
|
|
1002
|
+
name: COMARK_CONTENT_SOURCE,
|
|
1003
|
+
description: "Generated from your markdown files.",
|
|
1004
|
+
tips: nuxtV3Collections.size ? [`Parsing the following collections: ${Array.from(nuxtV3Collections).join(", ")}`] : ["No collections found. Set `sitemap: false` on a collection to keep it out."]
|
|
1005
|
+
},
|
|
1006
|
+
fetch: COMARK_CONTENT_SITEMAP_ROUTE
|
|
1007
|
+
});
|
|
860
1008
|
} else if (isNuxtContentV2) {
|
|
861
1009
|
addServerPlugin(resolve("./runtime/server/plugins/nuxt-content-v2"));
|
|
862
1010
|
addServerHandler({
|
|
@@ -1097,6 +1245,10 @@ ${onUrlEntries.join("\n")}`;
|
|
|
1097
1245
|
return r.contentType?.includes("text/html");
|
|
1098
1246
|
};
|
|
1099
1247
|
const generateGlobalSources = async () => {
|
|
1248
|
+
const excludedAppSources = resolveExcludedAppSources(
|
|
1249
|
+
config.excludeAppSources,
|
|
1250
|
+
nuxt.options.sitemap?.excludeAppSources
|
|
1251
|
+
);
|
|
1100
1252
|
const { routeRules: routeRules2 } = generateExtraRoutesFromNuxtConfig();
|
|
1101
1253
|
const nitro = await nitroPromise;
|
|
1102
1254
|
const prerenderedRoutes2 = nitro._prerenderedRoutes || [];
|
|
@@ -1173,7 +1325,7 @@ ${onUrlEntries.join("\n")}`;
|
|
|
1173
1325
|
s.sourceType = "user";
|
|
1174
1326
|
return s;
|
|
1175
1327
|
}),
|
|
1176
|
-
...(
|
|
1328
|
+
...(excludedAppSources === true ? [] : [
|
|
1177
1329
|
...appGlobalSources,
|
|
1178
1330
|
{
|
|
1179
1331
|
context: {
|
|
@@ -1205,7 +1357,7 @@ ${onUrlEntries.join("\n")}`;
|
|
|
1205
1357
|
},
|
|
1206
1358
|
urls: prerenderUrlsFinal
|
|
1207
1359
|
}
|
|
1208
|
-
]).filter((s) => !
|
|
1360
|
+
]).filter((s) => !excludedAppSources.includes(s.context.name) && (!!s.urls?.length || !!s.fetch)).map((s) => {
|
|
1209
1361
|
s.sourceType = "app";
|
|
1210
1362
|
return s;
|
|
1211
1363
|
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URLs from the installed content module's page collections.
|
|
3
|
+
*
|
|
4
|
+
* One route for every provider. `#nuxtseo/content` normalizes the manifest shape
|
|
5
|
+
* and the query builder; the `sitemap` field on each entry is written by the
|
|
6
|
+
* module's `content:file:afterParse` hook.
|
|
7
|
+
*/
|
|
8
|
+
declare const _default: any;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as contentRuntime from "#nuxtseo/content";
|
|
2
|
+
import { defineEventHandler } from "#nuxtseo/h3";
|
|
3
|
+
import { filters } from "#sitemap/content-filters";
|
|
4
|
+
import { onUrlFns } from "#sitemap/content-on-url";
|
|
5
|
+
const { listPageCollections, provider, queryPages } = contentRuntime;
|
|
6
|
+
export default defineEventHandler(async (e) => {
|
|
7
|
+
const collections = (await listPageCollections(e)).filter((collection) => collection.inSitemap && collection.hasField("sitemap")).map((collection) => collection.name);
|
|
8
|
+
const results = await Promise.all(collections.map(async (collection) => {
|
|
9
|
+
const needsAllFields = filters?.has(collection) || onUrlFns?.has(collection);
|
|
10
|
+
const query = queryPages(e, collection).where("path", "IS NOT NULL").where("sitemap", "IS NOT NULL");
|
|
11
|
+
if (!needsAllFields)
|
|
12
|
+
query.select("path", "sitemap");
|
|
13
|
+
try {
|
|
14
|
+
const entries = await query.all();
|
|
15
|
+
const filter = filters?.get(collection);
|
|
16
|
+
return { collection, entries: filter ? entries.filter(filter) : entries };
|
|
17
|
+
} catch (err) {
|
|
18
|
+
const hint = provider === "nuxt-content-v3" ? " On serverless the content DB is restored from a prerendered sql_dump.txt that isn't readable inside the function (nuxt/content#3805). Fix: prerender the sitemap so content URLs resolve at build, or configure a runtime database (D1/Turso/Postgres)." : "";
|
|
19
|
+
console.error(`[@nuxtjs/sitemap] Couldn't query content collection "${collection}" for the sitemap, so its URLs will be missing.${hint}`, err);
|
|
20
|
+
return { collection, entries: [] };
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
return results.flatMap(({ collection, entries }) => {
|
|
24
|
+
const onUrl = onUrlFns?.get(collection);
|
|
25
|
+
return entries.filter((entry) => entry.sitemap !== false && entry.path && !entry.path.endsWith(".navigation")).map((entry) => {
|
|
26
|
+
const url = {
|
|
27
|
+
loc: entry.path,
|
|
28
|
+
...typeof entry.sitemap === "object" && entry.sitemap ? entry.sitemap : {}
|
|
29
|
+
};
|
|
30
|
+
onUrl?.(url, entry, collection);
|
|
31
|
+
return url;
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -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
|
@@ -204,7 +204,7 @@ export interface SitemapSourceResolved extends Omit<SitemapSourceBase, 'urls'> {
|
|
|
204
204
|
message: string;
|
|
205
205
|
}[];
|
|
206
206
|
}
|
|
207
|
-
export type AppSourceContext = 'nuxt:pages' | 'nuxt:prerender' | 'nuxt:route-rules' | '@nuxtjs/i18n:pages' | 'nuxt-i18n-micro:pages' | '@nuxt/content@v2:urls' | '@nuxt/content@v3:urls';
|
|
207
|
+
export type AppSourceContext = 'nuxt:pages' | 'nuxt:prerender' | 'nuxt:route-rules' | '@nuxtjs/i18n:pages' | 'nuxt-i18n-micro:pages' | '@nuxt/content@v2:urls' | '@nuxt/content@v3:urls' | '@harlan-zw/comark-content:urls';
|
|
208
208
|
export type SitemapSourceInput = string | [string, FetchOptions] | SitemapSourceBase | SitemapSourceResolved;
|
|
209
209
|
interface LocaleObject extends Record<string, any> {
|
|
210
210
|
code: string;
|
|
@@ -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",
|
|
@@ -58,42 +58,43 @@
|
|
|
58
58
|
"@nuxt/kit": "^4.5.2",
|
|
59
59
|
"consola": "^3.4.2",
|
|
60
60
|
"defu": "^6.1.7",
|
|
61
|
-
"nuxt-site-config": "^4.2.
|
|
62
|
-
"nuxtseo-shared": "^5.3.
|
|
61
|
+
"nuxt-site-config": "^4.2.3",
|
|
62
|
+
"nuxtseo-shared": "^5.3.14",
|
|
63
63
|
"ofetch": "^1.5.1",
|
|
64
64
|
"pathe": "^2.0.3",
|
|
65
65
|
"pkg-types": "^2.3.1",
|
|
66
66
|
"radix3": "^1.1.2",
|
|
67
67
|
"ufo": "^1.6.4",
|
|
68
68
|
"ultrahtml": "^1.7.0",
|
|
69
|
-
"sitemapd": "^0.2.
|
|
69
|
+
"sitemapd": "^0.2.2"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@antfu/eslint-config": "^9.3.0",
|
|
73
73
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
74
|
+
"@harlan-zw/comark-content": "^0.1.3",
|
|
74
75
|
"@nuxt/content": "^3.15.2",
|
|
75
|
-
"@nuxt/devtools-kit": "4.0.0-alpha.
|
|
76
|
+
"@nuxt/devtools-kit": "4.0.0-alpha.12",
|
|
76
77
|
"@nuxt/module-builder": "^1.0.3",
|
|
77
78
|
"@nuxt/test-utils": "^4.1.0",
|
|
78
79
|
"@nuxt/ui": "^4.10.0",
|
|
79
80
|
"@nuxtjs/i18n": "^10.6.0",
|
|
80
|
-
"@nuxtjs/robots": "^6.1.
|
|
81
|
+
"@nuxtjs/robots": "^6.1.5",
|
|
81
82
|
"@vue/test-utils": "^2.4.11",
|
|
82
83
|
"better-sqlite3": "^13.0.3",
|
|
83
|
-
"bumpp": "^12.2.
|
|
84
|
+
"bumpp": "^12.2.1",
|
|
84
85
|
"eslint": "^10.8.1",
|
|
85
|
-
"eslint-plugin-harlanzw": "^0.
|
|
86
|
-
"happy-dom": "^20.11.
|
|
86
|
+
"eslint-plugin-harlanzw": "^0.20.0",
|
|
87
|
+
"happy-dom": "^20.11.6",
|
|
87
88
|
"nuxt": "^4.5.2",
|
|
88
89
|
"nuxt-i18n-micro": "^3.28.0",
|
|
89
|
-
"nuxtseo-layer-devtools": "^5.3.
|
|
90
|
+
"nuxtseo-layer-devtools": "^5.3.14",
|
|
90
91
|
"std-env": "^4.2.0",
|
|
91
92
|
"typescript": "6.0.3",
|
|
92
93
|
"unbuild": "^3.6.1",
|
|
93
|
-
"vitest": "^4.1.
|
|
94
|
-
"vue-tsc": "^3.3.
|
|
94
|
+
"vitest": "^4.1.11",
|
|
95
|
+
"vue-tsc": "^3.3.10",
|
|
95
96
|
"zod": "^4.4.3",
|
|
96
|
-
"@nuxtjs/sitemap": "8.
|
|
97
|
+
"@nuxtjs/sitemap": "8.5.0"
|
|
97
98
|
},
|
|
98
99
|
"scripts": {
|
|
99
100
|
"lint": "eslint .",
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { queryCollection } from "@nuxt/content/server";
|
|
2
|
-
import manifest from "#content/manifest";
|
|
3
|
-
import { defineEventHandler } from "#nuxtseo/h3";
|
|
4
|
-
import { filters } from "#sitemap/content-filters";
|
|
5
|
-
import { onUrlFns } from "#sitemap/content-on-url";
|
|
6
|
-
export default defineEventHandler(async (e) => {
|
|
7
|
-
const collections = [];
|
|
8
|
-
for (const collection in manifest) {
|
|
9
|
-
if (manifest[collection].fields.sitemap)
|
|
10
|
-
collections.push(collection);
|
|
11
|
-
}
|
|
12
|
-
const contentList = [];
|
|
13
|
-
for (const collection of collections) {
|
|
14
|
-
const needsAllFields = filters?.has(collection) || onUrlFns?.has(collection);
|
|
15
|
-
const query = queryCollection(e, collection).where("path", "IS NOT NULL").where("sitemap", "IS NOT NULL");
|
|
16
|
-
if (!needsAllFields)
|
|
17
|
-
query.select("path", "sitemap");
|
|
18
|
-
contentList.push(
|
|
19
|
-
query.all().then((results2) => {
|
|
20
|
-
const filter = filters?.get(collection);
|
|
21
|
-
return { collection, entries: filter ? results2.filter(filter) : results2 };
|
|
22
|
-
}).catch((err) => {
|
|
23
|
-
console.error(`[@nuxtjs/sitemap] Couldn't query @nuxt/content collection "${collection}" for the sitemap, so its URLs will be missing. On serverless the content DB is restored from a prerendered sql_dump.txt that isn't readable inside the function (nuxt/content#3805). Fix: prerender the sitemap so content URLs resolve at build, or configure a runtime database (D1/Turso/Postgres).`, err);
|
|
24
|
-
return { collection, entries: [] };
|
|
25
|
-
})
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
const results = await Promise.all(contentList);
|
|
29
|
-
return results.flatMap(({ collection, entries }) => {
|
|
30
|
-
const onUrl = onUrlFns?.get(collection);
|
|
31
|
-
return entries.filter((c) => c.sitemap !== false && c.path && !c.path.endsWith(".navigation")).map((c) => {
|
|
32
|
-
const url = {
|
|
33
|
-
loc: c.path,
|
|
34
|
-
...typeof c.sitemap === "object" ? c.sitemap : {}
|
|
35
|
-
};
|
|
36
|
-
onUrl?.(url, c, collection);
|
|
37
|
-
return url;
|
|
38
|
-
});
|
|
39
|
-
}).filter(Boolean);
|
|
40
|
-
});
|