@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.
Files changed (46) hide show
  1. package/dist/content.mjs +11 -8
  2. package/dist/devtools/lib/sitemap/state.ts +4 -1
  3. package/dist/devtools/pages/sitemap/index.vue +2 -2
  4. package/dist/module.json +2 -2
  5. package/dist/module.mjs +29 -45
  6. package/dist/runtime/server/plugins/compression.js +57 -13
  7. package/dist/runtime/server/plugins/stream-transport.d.ts +2 -0
  8. package/dist/runtime/server/plugins/stream-transport.js +16 -0
  9. package/dist/runtime/server/routes/__sitemap__/debug-production.d.ts +3 -3
  10. package/dist/runtime/server/routes/__sitemap__/debug-production.js +43 -27
  11. package/dist/runtime/server/routes/__sitemap__/debug.d.ts +1 -0
  12. package/dist/runtime/server/routes/__zero-runtime/sitemap/[sitemap].xml.d.ts +1 -1
  13. package/dist/runtime/server/routes/__zero-runtime/sitemap.xml.d.ts +1 -1
  14. package/dist/runtime/server/routes/__zero-runtime/sitemap_index.xml.d.ts +1 -1
  15. package/dist/runtime/server/routes/sitemap/[sitemap].xml.d.ts +1 -1
  16. package/dist/runtime/server/routes/sitemap.xml.d.ts +1 -1
  17. package/dist/runtime/server/routes/sitemap_index.xml.d.ts +1 -1
  18. package/dist/runtime/server/sitemap/builder/index-xml.d.ts +13 -0
  19. package/dist/runtime/server/sitemap/builder/index-xml.js +67 -0
  20. package/dist/runtime/server/sitemap/builder/sitemap-index.d.ts +1 -4
  21. package/dist/runtime/server/sitemap/builder/sitemap-index.js +15 -60
  22. package/dist/runtime/server/sitemap/builder/sitemap.d.ts +1 -1
  23. package/dist/runtime/server/sitemap/builder/sitemap.js +36 -25
  24. package/dist/runtime/server/sitemap/builder/xml.d.ts +9 -1
  25. package/dist/runtime/server/sitemap/builder/xml.js +37 -11
  26. package/dist/runtime/server/sitemap/event-handlers.d.ts +3 -3
  27. package/dist/runtime/server/sitemap/event-handlers.js +18 -23
  28. package/dist/runtime/server/sitemap/nitro.d.ts +4 -1
  29. package/dist/runtime/server/sitemap/nitro.js +116 -26
  30. package/dist/runtime/server/sitemap/stream.d.ts +30 -0
  31. package/dist/runtime/server/sitemap/stream.js +144 -0
  32. package/dist/runtime/server/sitemap/urlset/normalise.d.ts +6 -1
  33. package/dist/runtime/server/sitemap/urlset/normalise.js +26 -6
  34. package/dist/runtime/server/sitemap/urlset/sort.js +12 -3
  35. package/dist/runtime/server/sitemap/urlset/sources.js +50 -9
  36. package/dist/runtime/server/utils.d.ts +0 -1
  37. package/dist/runtime/server/utils.js +12 -12
  38. package/dist/runtime/types.d.ts +14 -3
  39. package/dist/runtime/utils-pure.d.ts +5 -4
  40. package/dist/runtime/utils-pure.js +43 -26
  41. package/dist/utils.d.mts +5 -29
  42. package/dist/utils.d.ts +5 -29
  43. package/dist/utils.mjs +3 -418
  44. package/package.json +28 -30
  45. package/dist/runtime/server/kit.d.ts +0 -2
  46. 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 { createNitroRouteRuleMatcher } from "../kit.js";
11
- import { buildSitemapUrls, urlsToXml } from "./builder/sitemap.js";
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 buildSitemapXml(event, definition, resolvers, runtimeConfig) {
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: sitemapUrls, failedSources } = await buildSitemapUrls(definition, resolvers, runtimeConfig, nitro);
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 (autoI18n?.locales && autoI18n?.strategy !== "no_prefix") {
63
- const match = splitForLocales(path, autoI18n.locales.map((l) => l.code));
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
- const hasRobotsDisabled = Object.entries(routeRules.headers || {}).some(([name, value]) => name.toLowerCase() === "x-robots-tag" && value.toLowerCase().includes("noindex"));
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 && sitemapUrls.length > 0) {
79
- logger.warn(`Sitemap had ${sitemapUrls.length} 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.`);
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
- resolvedCtx.urls = resolvedCtx.urls.map((e) => preNormalizeEntry(e, resolvers));
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.map((e) => normaliseEntry(e, defaults, resolvers));
94
- const urls = maybeSort(mergeOnKey(normalizedPreDedupe, "_key").map((e) => normaliseEntry(e, defaults, resolvers)));
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 nitro.hooks.callHook("sitemap:output", ctx);
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: (event, definition) => {
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 async function createSitemap(event, definition, runtimeConfig) {
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
- export declare function normaliseEntry(_e: ResolvedSitemapUrl, defaults: Omit<SitemapUrl, 'loc'>, resolvers?: NitroUrlResolvers): ResolvedSitemapUrl;
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
- return s.replace(/\/(\?|#|$)/, "$1");
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
- /(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/,
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
- return IS_VALID_W3C_DATE.some((r) => r.test(d));
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.split("/").length;
6
- const bSegments = bLoc.split("/").length;
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.localeCompare(bLoc, void 0, { numeric: true });
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 isXmlRequest = parseURL(url).pathname.endsWith(".xml");
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
- responseType: isXmlRequest ? "text" : "json",
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 (typeof res === "object") {
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 (await Promise.all(
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
- )).flat();
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
7
- }
8
5
  export function useSitemapRuntimeConfig(e) {
9
- const clone = JSON.parse(JSON.stringify(staticConfig));
10
- for (const k in clone.sitemaps) {
11
- const sitemap = clone.sitemaps[k];
12
- sitemap.include = normalizeRuntimeFilters(sitemap.include);
13
- sitemap.exclude = normalizeRuntimeFilters(sitemap.exclude);
14
- clone.sitemaps[k] = sitemap;
15
- }
16
- Object.assign(clone, useRuntimeConfig(e).sitemap);
17
- return Object.freeze(clone);
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
  }