@nuxtjs/sitemap 8.2.3 → 8.3.1
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/content.mjs +11 -8
- package/dist/devtools/lib/sitemap/state.ts +4 -1
- package/dist/devtools/pages/sitemap/index.vue +2 -2
- package/dist/module.json +2 -2
- package/dist/module.mjs +29 -45
- package/dist/runtime/server/plugins/compression.js +57 -13
- package/dist/runtime/server/plugins/stream-transport.d.ts +2 -0
- package/dist/runtime/server/plugins/stream-transport.js +16 -0
- package/dist/runtime/server/routes/__sitemap__/debug-production.d.ts +3 -3
- package/dist/runtime/server/routes/__sitemap__/debug-production.js +43 -27
- package/dist/runtime/server/routes/__sitemap__/debug.d.ts +1 -0
- package/dist/runtime/server/routes/__zero-runtime/sitemap/[sitemap].xml.d.ts +1 -1
- package/dist/runtime/server/routes/__zero-runtime/sitemap.xml.d.ts +1 -1
- package/dist/runtime/server/routes/__zero-runtime/sitemap_index.xml.d.ts +1 -1
- package/dist/runtime/server/routes/sitemap/[sitemap].xml.d.ts +1 -1
- package/dist/runtime/server/routes/sitemap.xml.d.ts +1 -1
- package/dist/runtime/server/routes/sitemap_index.xml.d.ts +1 -1
- package/dist/runtime/server/sitemap/builder/index-xml.d.ts +13 -0
- package/dist/runtime/server/sitemap/builder/index-xml.js +67 -0
- package/dist/runtime/server/sitemap/builder/sitemap-index.d.ts +1 -4
- package/dist/runtime/server/sitemap/builder/sitemap-index.js +15 -60
- package/dist/runtime/server/sitemap/builder/sitemap.d.ts +1 -1
- package/dist/runtime/server/sitemap/builder/sitemap.js +36 -25
- package/dist/runtime/server/sitemap/builder/xml.d.ts +9 -1
- package/dist/runtime/server/sitemap/builder/xml.js +37 -11
- package/dist/runtime/server/sitemap/event-handlers.d.ts +3 -3
- package/dist/runtime/server/sitemap/event-handlers.js +18 -23
- package/dist/runtime/server/sitemap/nitro.d.ts +4 -1
- package/dist/runtime/server/sitemap/nitro.js +116 -26
- package/dist/runtime/server/sitemap/stream.d.ts +30 -0
- package/dist/runtime/server/sitemap/stream.js +144 -0
- package/dist/runtime/server/sitemap/urlset/normalise.d.ts +6 -1
- package/dist/runtime/server/sitemap/urlset/normalise.js +26 -6
- package/dist/runtime/server/sitemap/urlset/sort.js +12 -3
- package/dist/runtime/server/sitemap/urlset/sources.js +50 -9
- package/dist/runtime/server/utils.d.ts +0 -1
- package/dist/runtime/server/utils.js +12 -12
- package/dist/runtime/types.d.ts +14 -3
- package/dist/runtime/utils-pure.d.ts +5 -4
- package/dist/runtime/utils-pure.js +43 -26
- package/dist/utils.d.mts +5 -29
- package/dist/utils.d.ts +5 -29
- package/dist/utils.mjs +3 -418
- package/package.json +28 -30
- package/dist/runtime/server/kit.d.ts +0 -2
- package/dist/runtime/server/kit.js +0 -23
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { defu } from "defu";
|
|
2
2
|
import { createError, getHeader, getQuery, setHeader } from "h3";
|
|
3
|
-
import { defineCachedFunction, useNitroApp } from "nitropack/runtime";
|
|
3
|
+
import { defineCachedFunction, useNitroApp, useRuntimeConfig } from "nitropack/runtime";
|
|
4
4
|
import { fixSlashes } from "nuxt-site-config/urls";
|
|
5
|
+
import { createNitroRouteRuleMatcher } from "nuxtseo-shared/server";
|
|
5
6
|
import { getPathRobotConfig } from "#internal/nuxt-robots/getPathRobotConfig";
|
|
6
7
|
import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
|
|
7
8
|
import { createSitePathResolver } from "#site-config/server/composables/utils";
|
|
8
9
|
import staticConfig from "#sitemap-virtual/static-config.mjs";
|
|
9
10
|
import { logger, mergeOnKey, splitForLocales } from "../../utils-pure.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
11
|
+
import { buildSitemapUrls, urlsToXml, urlsToXmlStream } from "./builder/sitemap.js";
|
|
12
|
+
import { createChunkedXmlStream } from "./stream.js";
|
|
12
13
|
import { normaliseEntry, preNormalizeEntry } from "./urlset/normalise.js";
|
|
13
14
|
import { sortInPlace } from "./urlset/sort.js";
|
|
14
15
|
const SERVER_CACHE_MAX_AGE = staticConfig.cacheMaxAgeSeconds || 60 * 10;
|
|
@@ -28,7 +29,7 @@ export function useNitroUrlResolvers(e) {
|
|
|
28
29
|
relativeBaseUrlResolver: createSitePathResolver(e, { absolute: false, withBase: true })
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
|
-
async function
|
|
32
|
+
async function buildSitemapRenderPlan(event, definition, resolvers, runtimeConfig) {
|
|
32
33
|
const { sitemapName } = definition;
|
|
33
34
|
const nitro = useNitroApp();
|
|
34
35
|
if (import.meta.prerender) {
|
|
@@ -43,15 +44,18 @@ async function buildSitemapXml(event, definition, resolvers, runtimeConfig) {
|
|
|
43
44
|
});
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
|
-
const { urls:
|
|
47
|
+
const { urls: resolvedSitemapUrls, failedSources } = await buildSitemapUrls(definition, resolvers, runtimeConfig, nitro);
|
|
48
|
+
const sitemapUrls = resolvedSitemapUrls.slice();
|
|
47
49
|
if (import.meta.prerender && failedSources.length) {
|
|
48
50
|
throw createError({
|
|
49
51
|
statusCode: 500,
|
|
50
52
|
message: `Sitemap generation failed due to ${failedSources.length} failed sources: ${failedSources.map((s) => `"${s.url}" (${s.error})`).join(", ")}`
|
|
51
53
|
});
|
|
52
54
|
}
|
|
53
|
-
const routeRuleMatcher = createNitroRouteRuleMatcher();
|
|
55
|
+
const routeRuleMatcher = createNitroRouteRuleMatcher(useRuntimeConfig(event));
|
|
54
56
|
const { autoI18n } = runtimeConfig;
|
|
57
|
+
const localeCodes = autoI18n?.locales && autoI18n.strategy !== "no_prefix" ? new Set(autoI18n.locales.map((l) => l.code)) : void 0;
|
|
58
|
+
const sourceCount = sitemapUrls.length;
|
|
55
59
|
let validCount = 0;
|
|
56
60
|
for (let i = 0; i < sitemapUrls.length; i++) {
|
|
57
61
|
const u = sitemapUrls[i];
|
|
@@ -59,8 +63,8 @@ async function buildSitemapXml(event, definition, resolvers, runtimeConfig) {
|
|
|
59
63
|
if (!getPathRobotConfig(event, { path, skipSiteIndexable: true }).indexable)
|
|
60
64
|
continue;
|
|
61
65
|
let routeRules = routeRuleMatcher(path);
|
|
62
|
-
if (
|
|
63
|
-
const match = splitForLocales(path,
|
|
66
|
+
if (localeCodes) {
|
|
67
|
+
const match = splitForLocales(path, localeCodes);
|
|
64
68
|
const pathWithoutPrefix = match[1];
|
|
65
69
|
if (pathWithoutPrefix && pathWithoutPrefix !== path)
|
|
66
70
|
routeRules = defu(routeRules, routeRuleMatcher(pathWithoutPrefix));
|
|
@@ -69,14 +73,23 @@ async function buildSitemapXml(event, definition, resolvers, runtimeConfig) {
|
|
|
69
73
|
continue;
|
|
70
74
|
if (typeof routeRules.robots !== "undefined" && !routeRules.robots)
|
|
71
75
|
continue;
|
|
72
|
-
|
|
76
|
+
let hasRobotsDisabled = false;
|
|
77
|
+
const headers = routeRules.headers;
|
|
78
|
+
if (headers) {
|
|
79
|
+
for (const name in headers) {
|
|
80
|
+
if (name.toLowerCase() === "x-robots-tag" && headers[name].toLowerCase().includes("noindex")) {
|
|
81
|
+
hasRobotsDisabled = true;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
73
86
|
if (routeRules.redirect || hasRobotsDisabled)
|
|
74
87
|
continue;
|
|
75
88
|
sitemapUrls[validCount++] = routeRules.sitemap ? defu(u, routeRules.sitemap) : u;
|
|
76
89
|
}
|
|
77
90
|
sitemapUrls.length = validCount;
|
|
78
|
-
if (import.meta.dev && validCount === 0 &&
|
|
79
|
-
logger.warn(`Sitemap had ${
|
|
91
|
+
if (import.meta.dev && validCount === 0 && sourceCount > 0) {
|
|
92
|
+
logger.warn(`Sitemap had ${sourceCount} URLs that were all filtered out. This may be due to a robots rules blocking these URLs from indexing. Check your /** route rules or robots.txt configuration.`);
|
|
80
93
|
}
|
|
81
94
|
const locSize = sitemapUrls.length;
|
|
82
95
|
const resolvedCtx = {
|
|
@@ -86,12 +99,28 @@ async function buildSitemapXml(event, definition, resolvers, runtimeConfig) {
|
|
|
86
99
|
};
|
|
87
100
|
await nitro.hooks.callHook("sitemap:resolved", resolvedCtx);
|
|
88
101
|
if (resolvedCtx.urls.length !== locSize) {
|
|
89
|
-
|
|
102
|
+
for (let i = 0; i < resolvedCtx.urls.length; i++)
|
|
103
|
+
resolvedCtx.urls[i] = preNormalizeEntry(resolvedCtx.urls[i], resolvers);
|
|
90
104
|
}
|
|
91
105
|
const maybeSort = (urls2) => runtimeConfig.sortEntries ? sortInPlace(urls2) : urls2;
|
|
92
|
-
const defaults = definition.defaults
|
|
93
|
-
const normalizedPreDedupe = resolvedCtx.urls
|
|
94
|
-
const
|
|
106
|
+
const defaults = definition.defaults;
|
|
107
|
+
const normalizedPreDedupe = resolvedCtx.urls;
|
|
108
|
+
const firstLastmod = normalizedPreDedupe[0]?.lastmod ?? defaults?.lastmod;
|
|
109
|
+
let cacheLastmod = normalizedPreDedupe.length > 1 && !!firstLastmod;
|
|
110
|
+
for (let i = 1; cacheLastmod && i < Math.min(normalizedPreDedupe.length, 8); i++)
|
|
111
|
+
cacheLastmod = (normalizedPreDedupe[i].lastmod ?? defaults?.lastmod) === firstLastmod;
|
|
112
|
+
const normaliseCache = cacheLastmod ? {} : void 0;
|
|
113
|
+
for (let i = 0; i < normalizedPreDedupe.length; i++)
|
|
114
|
+
normalizedPreDedupe[i] = normaliseEntry(normalizedPreDedupe[i], defaults, resolvers, normaliseCache);
|
|
115
|
+
const duplicateKeys = /* @__PURE__ */ new Set();
|
|
116
|
+
const urls = mergeOnKey(normalizedPreDedupe, "_key", (key) => duplicateKeys.add(key));
|
|
117
|
+
if (duplicateKeys.size) {
|
|
118
|
+
for (let i = 0; i < urls.length; i++) {
|
|
119
|
+
if (duplicateKeys.has(urls[i]._key))
|
|
120
|
+
urls[i] = normaliseEntry(urls[i], defaults, resolvers, normaliseCache);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
maybeSort(urls);
|
|
95
124
|
if (definition._isChunking && definition.sitemapName.includes("-")) {
|
|
96
125
|
const parts = definition.sitemapName.split("-");
|
|
97
126
|
const lastPart = parts.pop();
|
|
@@ -110,11 +139,59 @@ async function buildSitemapXml(event, definition, resolvers, runtimeConfig) {
|
|
|
110
139
|
messages: failedSources.map((f) => f.error),
|
|
111
140
|
urls: failedSources.map((f) => f.url)
|
|
112
141
|
} : void 0;
|
|
142
|
+
return { errorInfo, sitemapName, urls };
|
|
143
|
+
}
|
|
144
|
+
export async function renderSitemapOutput(nitro, event, sitemapName, renderString, renderStream, shouldStream, debug) {
|
|
145
|
+
if (!shouldStream) {
|
|
146
|
+
const ctx2 = { sitemap: renderString(), sitemapName, event };
|
|
147
|
+
await nitro.hooks.callHook("sitemap:output", ctx2);
|
|
148
|
+
return ctx2.sitemap;
|
|
149
|
+
}
|
|
150
|
+
let buffered = false;
|
|
151
|
+
let sitemap;
|
|
152
|
+
const ctx = { sitemapName, event };
|
|
153
|
+
Object.defineProperty(ctx, "sitemap", {
|
|
154
|
+
configurable: true,
|
|
155
|
+
enumerable: true,
|
|
156
|
+
get() {
|
|
157
|
+
buffered = true;
|
|
158
|
+
sitemap ??= renderString();
|
|
159
|
+
return sitemap;
|
|
160
|
+
},
|
|
161
|
+
set(value) {
|
|
162
|
+
buffered = true;
|
|
163
|
+
sitemap = value;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
await nitro.hooks.callHook("sitemap:output", ctx);
|
|
167
|
+
if (debug)
|
|
168
|
+
setHeader(event, "X-Sitemap-Render-Mode", buffered ? "buffered-hook" : "stream");
|
|
169
|
+
return buffered ? createChunkedXmlStream([sitemap]) : renderStream();
|
|
170
|
+
}
|
|
171
|
+
async function buildSitemapXml(event, definition, resolvers, runtimeConfig) {
|
|
172
|
+
const { errorInfo, sitemapName, urls } = await buildSitemapRenderPlan(event, definition, resolvers, runtimeConfig);
|
|
113
173
|
const sitemap = urlsToXml(urls, resolvers, runtimeConfig, errorInfo);
|
|
114
174
|
const ctx = { sitemap, sitemapName, event };
|
|
115
|
-
await
|
|
175
|
+
await useNitroApp().hooks.callHook("sitemap:output", ctx);
|
|
116
176
|
return ctx.sitemap;
|
|
117
177
|
}
|
|
178
|
+
function getSitemapCacheKey(event, definition) {
|
|
179
|
+
const host = getHeader(event, "host") || getHeader(event, "x-forwarded-host") || "";
|
|
180
|
+
const proto = getHeader(event, "x-forwarded-proto") || "https";
|
|
181
|
+
const sitemapName = definition.sitemapName || "default";
|
|
182
|
+
return `${sitemapName}-${proto}-${host}`;
|
|
183
|
+
}
|
|
184
|
+
const buildSitemapRenderPlanCached = defineCachedFunction(
|
|
185
|
+
buildSitemapRenderPlan,
|
|
186
|
+
{
|
|
187
|
+
name: "sitemap:render-plan",
|
|
188
|
+
group: "sitemap",
|
|
189
|
+
maxAge: SERVER_CACHE_MAX_AGE,
|
|
190
|
+
base: "sitemap",
|
|
191
|
+
getKey: getSitemapCacheKey,
|
|
192
|
+
swr: true
|
|
193
|
+
}
|
|
194
|
+
);
|
|
118
195
|
const buildSitemapXmlCached = defineCachedFunction(
|
|
119
196
|
buildSitemapXml,
|
|
120
197
|
{
|
|
@@ -123,20 +200,12 @@ const buildSitemapXmlCached = defineCachedFunction(
|
|
|
123
200
|
maxAge: SERVER_CACHE_MAX_AGE,
|
|
124
201
|
base: "sitemap",
|
|
125
202
|
// Use the sitemap storage
|
|
126
|
-
getKey:
|
|
127
|
-
const host = getHeader(event, "host") || getHeader(event, "x-forwarded-host") || "";
|
|
128
|
-
const proto = getHeader(event, "x-forwarded-proto") || "https";
|
|
129
|
-
const sitemapName = definition.sitemapName || "default";
|
|
130
|
-
return `${sitemapName}-${proto}-${host}`;
|
|
131
|
-
},
|
|
203
|
+
getKey: getSitemapCacheKey,
|
|
132
204
|
swr: true
|
|
133
205
|
// Enable stale-while-revalidate
|
|
134
206
|
}
|
|
135
207
|
);
|
|
136
|
-
export
|
|
137
|
-
const resolvers = useNitroUrlResolvers(event);
|
|
138
|
-
const shouldCache = !import.meta.dev && !import.meta.prerender && typeof runtimeConfig.cacheMaxAgeSeconds === "number" && runtimeConfig.cacheMaxAgeSeconds > 0;
|
|
139
|
-
const xml = shouldCache ? await buildSitemapXmlCached(event, definition, resolvers, runtimeConfig) : await buildSitemapXml(event, definition, resolvers, runtimeConfig);
|
|
208
|
+
export function setSitemapResponseHeaders(event, runtimeConfig) {
|
|
140
209
|
setHeader(event, "Content-Type", "text/xml; charset=UTF-8");
|
|
141
210
|
if (runtimeConfig.cacheMaxAgeSeconds) {
|
|
142
211
|
setHeader(event, "Cache-Control", `public, max-age=${runtimeConfig.cacheMaxAgeSeconds}, s-maxage=${runtimeConfig.cacheMaxAgeSeconds}, stale-while-revalidate=3600`);
|
|
@@ -151,5 +220,26 @@ export async function createSitemap(event, definition, runtimeConfig) {
|
|
|
151
220
|
setHeader(event, "Cache-Control", `no-cache, no-store`);
|
|
152
221
|
}
|
|
153
222
|
event.context._isSitemap = true;
|
|
223
|
+
}
|
|
224
|
+
export async function createSitemap(event, definition, runtimeConfig) {
|
|
225
|
+
const resolvers = useNitroUrlResolvers(event);
|
|
226
|
+
const shouldStream = !!runtimeConfig.experimentalStreaming && !import.meta.prerender;
|
|
227
|
+
const shouldCache = !import.meta.dev && !import.meta.prerender && typeof runtimeConfig.cacheMaxAgeSeconds === "number" && runtimeConfig.cacheMaxAgeSeconds > 0;
|
|
228
|
+
let xml;
|
|
229
|
+
if (shouldStream) {
|
|
230
|
+
const { errorInfo, sitemapName, urls } = shouldCache ? await buildSitemapRenderPlanCached(event, definition, resolvers, runtimeConfig) : await buildSitemapRenderPlan(event, definition, resolvers, runtimeConfig);
|
|
231
|
+
xml = await renderSitemapOutput(
|
|
232
|
+
useNitroApp(),
|
|
233
|
+
event,
|
|
234
|
+
sitemapName,
|
|
235
|
+
() => urlsToXml(urls, resolvers, runtimeConfig, errorInfo),
|
|
236
|
+
() => urlsToXmlStream(urls, resolvers, runtimeConfig, errorInfo),
|
|
237
|
+
true,
|
|
238
|
+
runtimeConfig.debug
|
|
239
|
+
);
|
|
240
|
+
} else {
|
|
241
|
+
xml = shouldCache ? await buildSitemapXmlCached(event, definition, resolvers, runtimeConfig) : await buildSitemapXml(event, definition, resolvers, runtimeConfig);
|
|
242
|
+
}
|
|
243
|
+
setSitemapResponseHeaders(event, runtimeConfig);
|
|
154
244
|
return xml;
|
|
155
245
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare const DEFAULT_XML_STREAM_CHUNK_SIZE: number;
|
|
2
|
+
export type SitemapCompressionEncoding = 'gzip' | 'deflate';
|
|
3
|
+
interface NodeResponseWritable {
|
|
4
|
+
once: (event: 'drain', listener: () => void) => unknown;
|
|
5
|
+
write: (chunk: Uint8Array) => boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface NodeResponseStream {
|
|
8
|
+
abort: (reason?: unknown) => void;
|
|
9
|
+
on: {
|
|
10
|
+
(event: 'end', listener: () => void): NodeResponseStream;
|
|
11
|
+
(event: 'error', listener: (error: unknown) => void): NodeResponseStream;
|
|
12
|
+
};
|
|
13
|
+
pipe: (destination: NodeResponseWritable) => NodeResponseWritable;
|
|
14
|
+
}
|
|
15
|
+
export declare function isReadableStream(value: unknown): value is ReadableStream<Uint8Array>;
|
|
16
|
+
export declare function hasNonIdentityEncoding(value: unknown): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Adapt a Web stream to the small Node stream surface H3 consumes. H3 v1 writes
|
|
19
|
+
* Web streams without waiting for `drain`; this adapter keeps production Node
|
|
20
|
+
* responses pull-driven and forwards disconnects to the Web stream's `cancel()`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createNodeResponseStream(source: ReadableStream<Uint8Array>, onCancelError: (error: unknown) => void): NodeResponseStream;
|
|
23
|
+
/**
|
|
24
|
+
* Convert synchronous XML fragments into a pull-driven byte stream. Fragments are
|
|
25
|
+
* batched so large sitemaps do not enqueue one tiny chunk per URL.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createChunkedXmlStream(chunks: Iterable<string>, targetChunkSize?: number): ReadableStream<Uint8Array>;
|
|
28
|
+
/** Select the best compression format supported by CompressionStream. */
|
|
29
|
+
export declare function negotiateCompressionEncoding(acceptEncoding: string): SitemapCompressionEncoding | null;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
export const DEFAULT_XML_STREAM_CHUNK_SIZE = 64 * 1024;
|
|
2
|
+
export function isReadableStream(value) {
|
|
3
|
+
return !!value && typeof value === "object" && typeof value.getReader === "function";
|
|
4
|
+
}
|
|
5
|
+
export function hasNonIdentityEncoding(value) {
|
|
6
|
+
return (Array.isArray(value) ? value : [value]).flatMap((encoding) => String(encoding || "").split(",")).some((encoding) => encoding.trim().toLowerCase() !== "identity" && encoding.trim() !== "");
|
|
7
|
+
}
|
|
8
|
+
export function createNodeResponseStream(source, onCancelError) {
|
|
9
|
+
const reader = source.getReader();
|
|
10
|
+
const endListeners = /* @__PURE__ */ new Set();
|
|
11
|
+
const errorListeners = /* @__PURE__ */ new Set();
|
|
12
|
+
let aborted = false;
|
|
13
|
+
let pumping = false;
|
|
14
|
+
let releaseBackpressure;
|
|
15
|
+
const stream = {
|
|
16
|
+
abort(reason) {
|
|
17
|
+
if (aborted)
|
|
18
|
+
return;
|
|
19
|
+
aborted = true;
|
|
20
|
+
releaseBackpressure?.();
|
|
21
|
+
releaseBackpressure = void 0;
|
|
22
|
+
void reader.cancel(reason).catch(onCancelError);
|
|
23
|
+
},
|
|
24
|
+
on(event, listener) {
|
|
25
|
+
if (event === "end")
|
|
26
|
+
endListeners.add(listener);
|
|
27
|
+
else
|
|
28
|
+
errorListeners.add(listener);
|
|
29
|
+
return stream;
|
|
30
|
+
},
|
|
31
|
+
pipe(destination) {
|
|
32
|
+
if (pumping)
|
|
33
|
+
return destination;
|
|
34
|
+
pumping = true;
|
|
35
|
+
void (async () => {
|
|
36
|
+
try {
|
|
37
|
+
while (true) {
|
|
38
|
+
if (aborted)
|
|
39
|
+
break;
|
|
40
|
+
const result = await reader.read();
|
|
41
|
+
if (result.done)
|
|
42
|
+
break;
|
|
43
|
+
if (!destination.write(result.value)) {
|
|
44
|
+
await new Promise((resolve) => {
|
|
45
|
+
releaseBackpressure = resolve;
|
|
46
|
+
destination.once("drain", resolve);
|
|
47
|
+
});
|
|
48
|
+
releaseBackpressure = void 0;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!aborted) {
|
|
52
|
+
for (const listener of endListeners)
|
|
53
|
+
listener();
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (!aborted) {
|
|
57
|
+
for (const listener of errorListeners)
|
|
58
|
+
listener(error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
})();
|
|
62
|
+
return destination;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
return stream;
|
|
66
|
+
}
|
|
67
|
+
export function createChunkedXmlStream(chunks, targetChunkSize = DEFAULT_XML_STREAM_CHUNK_SIZE) {
|
|
68
|
+
const iterator = chunks[Symbol.iterator]();
|
|
69
|
+
const encoder = new TextEncoder();
|
|
70
|
+
const chunkSize = Math.max(1, Math.floor(targetChunkSize));
|
|
71
|
+
let complete = false;
|
|
72
|
+
let pending;
|
|
73
|
+
let pendingOffset = 0;
|
|
74
|
+
return new ReadableStream({
|
|
75
|
+
pull(controller) {
|
|
76
|
+
if (complete)
|
|
77
|
+
return;
|
|
78
|
+
let output = "";
|
|
79
|
+
while (output.length < chunkSize) {
|
|
80
|
+
if (!pending) {
|
|
81
|
+
const next = iterator.next();
|
|
82
|
+
if (next.done) {
|
|
83
|
+
complete = true;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
pending = next.value;
|
|
87
|
+
pendingOffset = 0;
|
|
88
|
+
if (!pending)
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const remaining = chunkSize - output.length;
|
|
92
|
+
let end = Math.min(pending.length, pendingOffset + remaining);
|
|
93
|
+
if (end < pending.length && end > pendingOffset && pending.charCodeAt(end - 1) >= 55296 && pending.charCodeAt(end - 1) <= 56319) {
|
|
94
|
+
end--;
|
|
95
|
+
if (end === pendingOffset) {
|
|
96
|
+
if (output)
|
|
97
|
+
break;
|
|
98
|
+
end = Math.min(pending.length, pendingOffset + 2);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
output += pending.slice(pendingOffset, end);
|
|
102
|
+
pendingOffset = end;
|
|
103
|
+
if (pendingOffset === pending.length)
|
|
104
|
+
pending = void 0;
|
|
105
|
+
}
|
|
106
|
+
if (output)
|
|
107
|
+
controller.enqueue(encoder.encode(output));
|
|
108
|
+
if (complete)
|
|
109
|
+
controller.close();
|
|
110
|
+
},
|
|
111
|
+
cancel() {
|
|
112
|
+
complete = true;
|
|
113
|
+
iterator.return?.();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
export function negotiateCompressionEncoding(acceptEncoding) {
|
|
118
|
+
if (!acceptEncoding.trim())
|
|
119
|
+
return null;
|
|
120
|
+
const explicit = /* @__PURE__ */ new Map();
|
|
121
|
+
let wildcard;
|
|
122
|
+
for (const value of acceptEncoding.split(",")) {
|
|
123
|
+
const [rawEncoding, ...parameters] = value.trim().toLowerCase().split(";");
|
|
124
|
+
if (!rawEncoding)
|
|
125
|
+
continue;
|
|
126
|
+
let quality = 1;
|
|
127
|
+
for (const parameter of parameters) {
|
|
128
|
+
const [key, rawValue] = parameter.trim().split("=");
|
|
129
|
+
if (key === "q") {
|
|
130
|
+
const parsed = Number.parseFloat(rawValue || "");
|
|
131
|
+
quality = Number.isFinite(parsed) && parsed >= 0 && parsed <= 1 ? parsed : 0;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (rawEncoding === "*")
|
|
135
|
+
wildcard = quality;
|
|
136
|
+
else
|
|
137
|
+
explicit.set(rawEncoding, quality);
|
|
138
|
+
}
|
|
139
|
+
const gzipQuality = explicit.get("gzip") ?? wildcard ?? 0;
|
|
140
|
+
const deflateQuality = explicit.get("deflate") ?? wildcard ?? 0;
|
|
141
|
+
if (gzipQuality <= 0 && deflateQuality <= 0)
|
|
142
|
+
return null;
|
|
143
|
+
return gzipQuality >= deflateQuality ? "gzip" : "deflate";
|
|
144
|
+
}
|
|
@@ -2,6 +2,11 @@ import type { NitroUrlResolvers, ResolvedSitemapUrl, SitemapUrl, SitemapUrlInput
|
|
|
2
2
|
export declare function validateSitemapUrl(url: SitemapUrlInput): string[];
|
|
3
3
|
export declare function preNormalizeEntry(_e: SitemapUrl | string, resolvers?: NitroUrlResolvers): ResolvedSitemapUrl;
|
|
4
4
|
export declare function isEncoded(url: string): boolean;
|
|
5
|
-
|
|
5
|
+
interface NormaliseCache {
|
|
6
|
+
lastmodInput?: string | Date;
|
|
7
|
+
lastmodOutput?: string | false;
|
|
8
|
+
}
|
|
9
|
+
export declare function normaliseEntry(_e: ResolvedSitemapUrl, defaults?: Omit<SitemapUrl, 'loc'>, resolvers?: NitroUrlResolvers, cache?: NormaliseCache): ResolvedSitemapUrl;
|
|
6
10
|
export declare function isValidW3CDate(d: string): boolean;
|
|
7
11
|
export declare function normaliseDate(date: string | Date): string;
|
|
12
|
+
export {};
|
|
@@ -40,7 +40,14 @@ function resolve(s, resolvers) {
|
|
|
40
40
|
return resolvers.canonicalUrlResolver(str);
|
|
41
41
|
}
|
|
42
42
|
function removeTrailingSlash(s) {
|
|
43
|
-
|
|
43
|
+
let pathEnd = s.length;
|
|
44
|
+
const queryIndex = s.indexOf("?");
|
|
45
|
+
if (queryIndex !== -1)
|
|
46
|
+
pathEnd = queryIndex;
|
|
47
|
+
const hashIndex = s.indexOf("#");
|
|
48
|
+
if (hashIndex !== -1 && hashIndex < pathEnd)
|
|
49
|
+
pathEnd = hashIndex;
|
|
50
|
+
return pathEnd > 0 && s.charCodeAt(pathEnd - 1) === 47 ? s.slice(0, pathEnd - 1) + s.slice(pathEnd) : s;
|
|
44
51
|
}
|
|
45
52
|
export function preNormalizeEntry(_e, resolvers) {
|
|
46
53
|
const input = typeof _e === "string" ? { loc: _e } : { ..._e };
|
|
@@ -86,15 +93,19 @@ export function isEncoded(url) {
|
|
|
86
93
|
return false;
|
|
87
94
|
}
|
|
88
95
|
}
|
|
89
|
-
export function normaliseEntry(_e, defaults, resolvers) {
|
|
90
|
-
const e = defu(_e, defaults);
|
|
96
|
+
export function normaliseEntry(_e, defaults, resolvers, cache) {
|
|
97
|
+
const e = defaults ? defu(_e, defaults) : { ..._e };
|
|
91
98
|
if (import.meta.dev) {
|
|
92
99
|
const warnings = validateSitemapUrl(e);
|
|
93
100
|
if (warnings.length)
|
|
94
101
|
e._warnings = (e._warnings || []).concat(warnings);
|
|
95
102
|
}
|
|
96
103
|
if (e.lastmod) {
|
|
97
|
-
const date = normaliseDate(e.lastmod);
|
|
104
|
+
const date = cache?.lastmodInput === e.lastmod ? cache.lastmodOutput : normaliseDate(e.lastmod);
|
|
105
|
+
if (cache && cache.lastmodInput !== e.lastmod) {
|
|
106
|
+
cache.lastmodInput = e.lastmod;
|
|
107
|
+
cache.lastmodOutput = date;
|
|
108
|
+
}
|
|
98
109
|
if (date)
|
|
99
110
|
e.lastmod = date;
|
|
100
111
|
else
|
|
@@ -133,13 +144,22 @@ export function normaliseEntry(_e, defaults, resolvers) {
|
|
|
133
144
|
return e;
|
|
134
145
|
}
|
|
135
146
|
const IS_VALID_W3C_DATE = [
|
|
136
|
-
|
|
147
|
+
/^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)$/,
|
|
148
|
+
/^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z)$/,
|
|
149
|
+
/^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z)$/,
|
|
137
150
|
/^\d{4}-[01]\d-[0-3]\d$/,
|
|
138
151
|
/^\d{4}-[01]\d$/,
|
|
139
152
|
/^\d{4}$/
|
|
140
153
|
];
|
|
141
154
|
export function isValidW3CDate(d) {
|
|
142
|
-
|
|
155
|
+
if (!IS_VALID_W3C_DATE.some((r) => r.test(d)))
|
|
156
|
+
return false;
|
|
157
|
+
const [year, month, day] = d.slice(0, 10).split("-").map(Number);
|
|
158
|
+
if (month !== void 0 && (month < 1 || month > 12))
|
|
159
|
+
return false;
|
|
160
|
+
if (day !== void 0 && (day < 1 || day > new Date(year, month, 0).getDate()))
|
|
161
|
+
return false;
|
|
162
|
+
return true;
|
|
143
163
|
}
|
|
144
164
|
export function normaliseDate(d) {
|
|
145
165
|
if (typeof d === "string") {
|
|
@@ -1,13 +1,22 @@
|
|
|
1
|
+
const naturalCompare = new Intl.Collator(void 0, { numeric: true }).compare;
|
|
2
|
+
function countPathSegments(loc) {
|
|
3
|
+
let segments = 1;
|
|
4
|
+
for (let i = 0; i < loc.length; i++) {
|
|
5
|
+
if (loc.charCodeAt(i) === 47)
|
|
6
|
+
segments++;
|
|
7
|
+
}
|
|
8
|
+
return segments;
|
|
9
|
+
}
|
|
1
10
|
export function sortInPlace(urls) {
|
|
2
11
|
urls.sort((a, b) => {
|
|
3
12
|
const aLoc = typeof a === "string" ? a : a.loc;
|
|
4
13
|
const bLoc = typeof b === "string" ? b : b.loc;
|
|
5
|
-
const aSegments = aLoc
|
|
6
|
-
const bSegments = bLoc
|
|
14
|
+
const aSegments = countPathSegments(aLoc);
|
|
15
|
+
const bSegments = countPathSegments(bLoc);
|
|
7
16
|
if (aSegments !== bSegments) {
|
|
8
17
|
return aSegments - bSegments;
|
|
9
18
|
}
|
|
10
|
-
return aLoc
|
|
19
|
+
return naturalCompare(aLoc, bLoc);
|
|
11
20
|
});
|
|
12
21
|
return urls;
|
|
13
22
|
}
|
|
@@ -1,8 +1,39 @@
|
|
|
1
|
-
import { parseSitemapXml } from "@nuxtjs/sitemap/utils";
|
|
2
1
|
import { defu } from "defu";
|
|
3
2
|
import { getRequestHost } from "h3";
|
|
3
|
+
import { collectSitemap } from "sitemapd/parse";
|
|
4
4
|
import { parseURL } from "ufo";
|
|
5
5
|
import { logger } from "../../../utils-pure.js";
|
|
6
|
+
const changeFrequencies = /* @__PURE__ */ new Set([
|
|
7
|
+
"always",
|
|
8
|
+
"hourly",
|
|
9
|
+
"daily",
|
|
10
|
+
"weekly",
|
|
11
|
+
"monthly",
|
|
12
|
+
"yearly",
|
|
13
|
+
"never"
|
|
14
|
+
]);
|
|
15
|
+
function readerEntryToSitemapInput(entry) {
|
|
16
|
+
const priority = entry.priority === void 0 ? void 0 : Number.parseFloat(entry.priority);
|
|
17
|
+
const changefreq = entry.changefreq && changeFrequencies.has(entry.changefreq) ? entry.changefreq : void 0;
|
|
18
|
+
return {
|
|
19
|
+
loc: entry.loc,
|
|
20
|
+
...entry.lastmod ? { lastmod: entry.lastmod } : {},
|
|
21
|
+
...changefreq ? { changefreq } : {},
|
|
22
|
+
...priority !== void 0 && Number.isFinite(priority) ? { priority } : {},
|
|
23
|
+
...entry.extensions?.alternatives ? { alternatives: entry.extensions.alternatives.map(({ hreflang, href }) => ({ hreflang, href })) } : {},
|
|
24
|
+
...entry.extensions?.images ? {
|
|
25
|
+
images: entry.extensions.images.map((image) => ({
|
|
26
|
+
loc: image.loc,
|
|
27
|
+
...image.caption ? { caption: image.caption } : {},
|
|
28
|
+
...image.geoLocation ? { geo_location: image.geoLocation } : {},
|
|
29
|
+
...image.title ? { title: image.title } : {},
|
|
30
|
+
...image.license ? { license: image.license } : {}
|
|
31
|
+
}))
|
|
32
|
+
} : {},
|
|
33
|
+
...entry.extensions?.videos ? { videos: entry.extensions.videos } : {},
|
|
34
|
+
...entry.extensions?.news ? { news: entry.extensions.news } : {}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
6
37
|
export function normalizeSourceInput(source) {
|
|
7
38
|
if (typeof source === "string") {
|
|
8
39
|
return { context: { name: "hook" }, fetch: source };
|
|
@@ -48,7 +79,9 @@ export async function fetchDataSource(input, event) {
|
|
|
48
79
|
const abortRequestTimeout = setTimeout(() => timeoutController.abort(), timeout);
|
|
49
80
|
try {
|
|
50
81
|
let isMaybeErrorResponse = false;
|
|
51
|
-
const
|
|
82
|
+
const pathname = parseURL(url).pathname.toLowerCase();
|
|
83
|
+
const isGzUrl = pathname.endsWith(".gz");
|
|
84
|
+
const isXmlRequest = pathname.endsWith(".xml") || isGzUrl;
|
|
52
85
|
const mergedHeaders = defu(
|
|
53
86
|
options?.headers,
|
|
54
87
|
{
|
|
@@ -58,7 +91,10 @@ export async function fetchDataSource(input, event) {
|
|
|
58
91
|
);
|
|
59
92
|
const fetchOptions = {
|
|
60
93
|
...options,
|
|
61
|
-
|
|
94
|
+
// Fetch XML sources as raw bytes so we can detect and decompress a gzip body
|
|
95
|
+
// (either a `.gz` URL, or a server that serves gzip without Content-Encoding)
|
|
96
|
+
// before it's mangled by a UTF-8 text decode.
|
|
97
|
+
responseType: isXmlRequest ? "arrayBuffer" : "json",
|
|
62
98
|
signal: timeoutController.signal,
|
|
63
99
|
headers: mergedHeaders,
|
|
64
100
|
// Use ofetch's built-in retry for external sources
|
|
@@ -84,11 +120,16 @@ export async function fetchDataSource(input, event) {
|
|
|
84
120
|
};
|
|
85
121
|
}
|
|
86
122
|
let urls = [];
|
|
87
|
-
if (
|
|
123
|
+
if (isXmlRequest) {
|
|
124
|
+
const bytes = res instanceof Uint8Array ? res : new Uint8Array(res);
|
|
125
|
+
const result = await collectSitemap(bytes);
|
|
126
|
+
if (result._tag !== "document")
|
|
127
|
+
throw new Error(result.issues.map((issue) => issue.message).join("; ") || "Invalid sitemap document");
|
|
128
|
+
if (result.document._tag !== "urlset")
|
|
129
|
+
throw new Error("Sitemap URL source must be a URL set, not a sitemap index");
|
|
130
|
+
urls = result.document.entries.map(readerEntryToSitemapInput);
|
|
131
|
+
} else if (typeof res === "object") {
|
|
88
132
|
urls = res.urls || res;
|
|
89
|
-
} else if (typeof res === "string" && parseURL(url).pathname.endsWith(".xml")) {
|
|
90
|
-
const result = await parseSitemapXml(res);
|
|
91
|
-
urls = result.urls;
|
|
92
133
|
}
|
|
93
134
|
return {
|
|
94
135
|
...input,
|
|
@@ -150,7 +191,7 @@ export async function childSitemapSources(definition) {
|
|
|
150
191
|
return [...m.sources[definition.sitemapName] || []];
|
|
151
192
|
}
|
|
152
193
|
export async function resolveSitemapSources(sources, event) {
|
|
153
|
-
return
|
|
194
|
+
return await Promise.all(
|
|
154
195
|
sources.map((source) => {
|
|
155
196
|
const normalized = normalizeSourceInput(source);
|
|
156
197
|
if ("urls" in normalized) {
|
|
@@ -167,5 +208,5 @@ export async function resolveSitemapSources(sources, event) {
|
|
|
167
208
|
error: "Invalid source"
|
|
168
209
|
};
|
|
169
210
|
})
|
|
170
|
-
)
|
|
211
|
+
);
|
|
171
212
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { H3Event } from 'h3';
|
|
2
2
|
import type { ModuleRuntimeConfig } from '../types.js';
|
|
3
3
|
export * from '../utils-pure.js';
|
|
4
|
-
export declare function xmlEscape(str: string | number | boolean | Date): string;
|
|
5
4
|
export declare function useSitemapRuntimeConfig(e?: H3Event): ModuleRuntimeConfig;
|
|
@@ -2,17 +2,17 @@ import { useRuntimeConfig } from "nitropack/runtime";
|
|
|
2
2
|
import staticConfig from "#sitemap-virtual/static-config.mjs";
|
|
3
3
|
import { normalizeRuntimeFilters } from "../utils-pure.js";
|
|
4
4
|
export * from "../utils-pure.js";
|
|
5
|
-
export function xmlEscape(str) {
|
|
6
|
-
return String(str).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
7
|
-
}
|
|
8
5
|
export function useSitemapRuntimeConfig(e) {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Object.
|
|
17
|
-
|
|
6
|
+
const sitemaps = Object.fromEntries(
|
|
7
|
+
Object.entries(staticConfig.sitemaps).map(([name, sitemap]) => [name, {
|
|
8
|
+
...sitemap,
|
|
9
|
+
include: normalizeRuntimeFilters("include" in sitemap ? sitemap.include : void 0),
|
|
10
|
+
exclude: normalizeRuntimeFilters("exclude" in sitemap ? sitemap.exclude : void 0)
|
|
11
|
+
}])
|
|
12
|
+
);
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
...staticConfig,
|
|
15
|
+
sitemaps,
|
|
16
|
+
...useRuntimeConfig(e).sitemap
|
|
17
|
+
});
|
|
18
18
|
}
|