@nuxtjs/sitemap 8.2.2 → 8.3.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.
Files changed (41) hide show
  1. package/README.md +1 -1
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +8 -3
  4. package/dist/runtime/server/kit.js +9 -2
  5. package/dist/runtime/server/plugins/compression.js +57 -13
  6. package/dist/runtime/server/plugins/stream-transport.d.ts +2 -0
  7. package/dist/runtime/server/plugins/stream-transport.js +16 -0
  8. package/dist/runtime/server/routes/__sitemap__/debug.d.ts +1 -0
  9. package/dist/runtime/server/routes/__zero-runtime/sitemap/[sitemap].xml.d.ts +1 -1
  10. package/dist/runtime/server/routes/__zero-runtime/sitemap.xml.d.ts +1 -1
  11. package/dist/runtime/server/routes/__zero-runtime/sitemap_index.xml.d.ts +1 -1
  12. package/dist/runtime/server/routes/sitemap/[sitemap].xml.d.ts +1 -1
  13. package/dist/runtime/server/routes/sitemap.xml.d.ts +1 -1
  14. package/dist/runtime/server/routes/sitemap_index.xml.d.ts +1 -1
  15. package/dist/runtime/server/sitemap/builder/index-xml.d.ts +13 -0
  16. package/dist/runtime/server/sitemap/builder/index-xml.js +70 -0
  17. package/dist/runtime/server/sitemap/builder/sitemap-index.d.ts +1 -4
  18. package/dist/runtime/server/sitemap/builder/sitemap-index.js +2 -34
  19. package/dist/runtime/server/sitemap/builder/sitemap.d.ts +1 -1
  20. package/dist/runtime/server/sitemap/builder/sitemap.js +36 -25
  21. package/dist/runtime/server/sitemap/builder/xml.d.ts +8 -0
  22. package/dist/runtime/server/sitemap/builder/xml.js +35 -6
  23. package/dist/runtime/server/sitemap/event-handlers.d.ts +3 -3
  24. package/dist/runtime/server/sitemap/event-handlers.js +18 -10
  25. package/dist/runtime/server/sitemap/nitro.d.ts +3 -1
  26. package/dist/runtime/server/sitemap/nitro.js +107 -20
  27. package/dist/runtime/server/sitemap/stream.d.ts +30 -0
  28. package/dist/runtime/server/sitemap/stream.js +144 -0
  29. package/dist/runtime/server/sitemap/urlset/normalise.d.ts +6 -1
  30. package/dist/runtime/server/sitemap/urlset/normalise.js +15 -4
  31. package/dist/runtime/server/sitemap/urlset/sort.js +12 -3
  32. package/dist/runtime/server/sitemap/urlset/sources.js +3 -3
  33. package/dist/runtime/server/utils.d.ts +0 -1
  34. package/dist/runtime/server/utils.js +12 -12
  35. package/dist/runtime/types.d.ts +14 -3
  36. package/dist/runtime/utils-pure.d.ts +5 -4
  37. package/dist/runtime/utils-pure.js +41 -19
  38. package/dist/utils.d.mts +30 -2
  39. package/dist/utils.d.ts +30 -2
  40. package/dist/utils.mjs +646 -43
  41. package/package.json +22 -22
@@ -1,4 +1,4 @@
1
1
  import type { H3Event } from 'h3';
2
- export declare function sitemapXmlEventHandler(e: H3Event): Promise<string | void>;
3
- export declare function sitemapIndexXmlEventHandler(e: H3Event): Promise<string>;
4
- export declare function sitemapChildXmlEventHandler(e: H3Event): Promise<string | undefined>;
2
+ export declare function sitemapXmlEventHandler(e: H3Event): Promise<string | void | ReadableStream<Uint8Array<ArrayBufferLike>>>;
3
+ export declare function sitemapIndexXmlEventHandler(e: H3Event): Promise<string | ReadableStream<Uint8Array<ArrayBufferLike>>>;
4
+ export declare function sitemapChildXmlEventHandler(e: H3Event): Promise<string | ReadableStream<Uint8Array<ArrayBufferLike>> | undefined>;
@@ -1,9 +1,10 @@
1
- import { appendHeader, createError, getRouterParam, sendRedirect, setHeader } from "h3";
1
+ import { appendHeader, createError, getRequestURL, getRouterParam, sendRedirect, setHeader } from "h3";
2
2
  import { useNitroApp, useRuntimeConfig } from "nitropack/runtime";
3
3
  import { joinURL, withBase, withLeadingSlash, withoutLeadingSlash, withoutTrailingSlash } from "ufo";
4
4
  import { useSitemapRuntimeConfig } from "../utils.js";
5
- import { buildSitemapIndex, urlsToIndexXml } from "./builder/sitemap-index.js";
6
- import { createSitemap, useNitroUrlResolvers } from "./nitro.js";
5
+ import { urlsToIndexXml, urlsToIndexXmlStream } from "./builder/index-xml.js";
6
+ import { buildSitemapIndex } from "./builder/sitemap-index.js";
7
+ import { createSitemap, renderSitemapOutput, useNitroUrlResolvers } from "./nitro.js";
7
8
  import { getSitemapConfig, parseChunkInfo } from "./utils/chunk.js";
8
9
  export async function sitemapXmlEventHandler(e) {
9
10
  const runtimeConfig = useSitemapRuntimeConfig();
@@ -27,9 +28,15 @@ export async function sitemapIndexXmlEventHandler(e) {
27
28
  const indexResolvedCtx = { sitemaps, event: e };
28
29
  await nitro.hooks.callHook("sitemap:index-resolved", indexResolvedCtx);
29
30
  const errorInfo = failedSources.length > 0 ? { messages: failedSources.map((f) => f.error), urls: failedSources.map((f) => f.url) } : void 0;
30
- const output = urlsToIndexXml(indexResolvedCtx.sitemaps, resolvers, runtimeConfig, errorInfo);
31
- const ctx = { sitemap: output, sitemapName: "sitemap", event: e };
32
- await nitro.hooks.callHook("sitemap:output", ctx);
31
+ const output = await renderSitemapOutput(
32
+ nitro,
33
+ e,
34
+ "sitemap",
35
+ () => urlsToIndexXml(indexResolvedCtx.sitemaps, resolvers, runtimeConfig, errorInfo),
36
+ () => urlsToIndexXmlStream(indexResolvedCtx.sitemaps, resolvers, runtimeConfig, errorInfo),
37
+ !!runtimeConfig.experimentalStreaming && !import.meta.prerender,
38
+ runtimeConfig.debug
39
+ );
33
40
  setHeader(e, "Content-Type", "text/xml; charset=UTF-8");
34
41
  if (runtimeConfig.cacheMaxAgeSeconds) {
35
42
  setHeader(e, "Cache-Control", `public, max-age=${runtimeConfig.cacheMaxAgeSeconds}, s-maxage=${runtimeConfig.cacheMaxAgeSeconds}, stale-while-revalidate=3600`);
@@ -43,17 +50,18 @@ export async function sitemapIndexXmlEventHandler(e) {
43
50
  } else {
44
51
  setHeader(e, "Cache-Control", `no-cache, no-store`);
45
52
  }
46
- return ctx.sitemap;
53
+ e.context._isSitemap = true;
54
+ return output;
47
55
  }
48
56
  export async function sitemapChildXmlEventHandler(e) {
49
- if (!e.path.endsWith(".xml"))
57
+ const pathname = getRequestURL(e).pathname;
58
+ if (!pathname.endsWith(".xml"))
50
59
  return;
51
60
  const runtimeConfig = useSitemapRuntimeConfig(e);
52
61
  const { sitemaps } = runtimeConfig;
53
62
  let sitemapName = getRouterParam(e, "sitemap");
54
63
  if (!sitemapName) {
55
- const path = e.path;
56
- const match = path.match(/(?:\/__sitemap__\/)?(.+)\.xml$/);
64
+ const match = pathname.match(/(?:\/__sitemap__\/)?(.+)\.xml$/);
57
65
  if (match)
58
66
  sitemapName = match[1];
59
67
  }
@@ -1,4 +1,6 @@
1
1
  import type { H3Event } from 'h3';
2
+ import type { NitroApp } from 'nitropack/types';
2
3
  import type { ModuleRuntimeConfig, NitroUrlResolvers, SitemapDefinition } from '../../types.js';
3
4
  export declare function useNitroUrlResolvers(e: H3Event): NitroUrlResolvers;
4
- export declare function createSitemap(event: H3Event, definition: SitemapDefinition, runtimeConfig: ModuleRuntimeConfig): Promise<string>;
5
+ export declare function renderSitemapOutput(nitro: NitroApp, event: H3Event, sitemapName: string, renderString: () => string, renderStream: () => ReadableStream<Uint8Array>, shouldStream: boolean, debug: boolean): Promise<string | ReadableStream<Uint8Array>>;
6
+ export declare function createSitemap(event: H3Event, definition: SitemapDefinition, runtimeConfig: ModuleRuntimeConfig): Promise<string | ReadableStream<Uint8Array<ArrayBufferLike>>>;
@@ -8,7 +8,8 @@ import { createSitePathResolver } from "#site-config/server/composables/utils";
8
8
  import staticConfig from "#sitemap-virtual/static-config.mjs";
9
9
  import { logger, mergeOnKey, splitForLocales } from "../../utils-pure.js";
10
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,7 +44,8 @@ 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,
@@ -52,6 +54,8 @@ async function buildSitemapXml(event, definition, resolvers, runtimeConfig) {
52
54
  }
53
55
  const routeRuleMatcher = createNitroRouteRuleMatcher();
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,30 @@ 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
208
  export async function createSitemap(event, definition, runtimeConfig) {
137
209
  const resolvers = useNitroUrlResolvers(event);
210
+ const shouldStream = !!runtimeConfig.experimentalStreaming && !import.meta.prerender;
138
211
  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);
212
+ let xml;
213
+ if (shouldStream) {
214
+ const { errorInfo, sitemapName, urls } = shouldCache ? await buildSitemapRenderPlanCached(event, definition, resolvers, runtimeConfig) : await buildSitemapRenderPlan(event, definition, resolvers, runtimeConfig);
215
+ xml = await renderSitemapOutput(
216
+ useNitroApp(),
217
+ event,
218
+ sitemapName,
219
+ () => urlsToXml(urls, resolvers, runtimeConfig, errorInfo),
220
+ () => urlsToXmlStream(urls, resolvers, runtimeConfig, errorInfo),
221
+ true,
222
+ runtimeConfig.debug
223
+ );
224
+ } else {
225
+ xml = shouldCache ? await buildSitemapXmlCached(event, definition, resolvers, runtimeConfig) : await buildSitemapXml(event, definition, resolvers, runtimeConfig);
226
+ }
140
227
  setHeader(event, "Content-Type", "text/xml; charset=UTF-8");
141
228
  if (runtimeConfig.cacheMaxAgeSeconds) {
142
229
  setHeader(event, "Cache-Control", `public, max-age=${runtimeConfig.cacheMaxAgeSeconds}, s-maxage=${runtimeConfig.cacheMaxAgeSeconds}, stale-while-revalidate=3600`);
@@ -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
@@ -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
  }
@@ -86,7 +86,7 @@ export async function fetchDataSource(input, event) {
86
86
  let urls = [];
87
87
  if (typeof res === "object") {
88
88
  urls = res.urls || res;
89
- } else if (typeof res === "string" && parseURL(url).pathname.endsWith(".xml")) {
89
+ } else if (typeof res === "string" && isXmlRequest) {
90
90
  const result = await parseSitemapXml(res);
91
91
  urls = result.urls;
92
92
  }
@@ -150,7 +150,7 @@ export async function childSitemapSources(definition) {
150
150
  return [...m.sources[definition.sitemapName] || []];
151
151
  }
152
152
  export async function resolveSitemapSources(sources, event) {
153
- return (await Promise.all(
153
+ return await Promise.all(
154
154
  sources.map((source) => {
155
155
  const normalized = normalizeSourceInput(source);
156
156
  if ("urls" in normalized) {
@@ -167,5 +167,5 @@ export async function resolveSitemapSources(sources, event) {
167
167
  error: "Invalid source"
168
168
  };
169
169
  })
170
- )).flat();
170
+ );
171
171
  }
@@ -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
  }
@@ -148,11 +148,22 @@ export interface ModuleOptions extends SitemapDefinition {
148
148
  */
149
149
  experimentalWarmUp?: boolean;
150
150
  /**
151
- * Send the Sitemap as a compressed stream supporting gzip, brolti, etc.
151
+ * Compress sitemap responses with gzip or deflate when supported by the client.
152
152
  *
153
- * @experimental Will be enabled by default in v5 (if stable)
153
+ * @experimental This option may change in a future release.
154
154
  */
155
155
  experimentalCompression?: boolean;
156
+ /**
157
+ * Serialize dynamic sitemap responses as a pull-driven stream instead of building
158
+ * the complete XML string in memory first. Prerendered sitemaps remain buffered.
159
+ *
160
+ * Accessing `ctx.sitemap` in the `sitemap:output` hook buffers that response for
161
+ * backwards compatibility.
162
+ *
163
+ * @default false
164
+ * @experimental This option may change in a future release.
165
+ */
166
+ experimentalStreaming?: boolean;
156
167
  /**
157
168
  * When enabled, sitemap generation only runs during prerendering.
158
169
  * The sitemap building code is tree-shaken from the runtime bundle.
@@ -227,7 +238,7 @@ export interface AutoI18nConfig {
227
238
  strategy: 'prefix' | 'prefix_except_default' | 'prefix_and_default' | 'no_prefix';
228
239
  pages?: Record<string, Record<string, string | false>>;
229
240
  }
230
- export interface ModuleRuntimeConfig extends Pick<ModuleOptions, 'sitemapsPathPrefix' | 'cacheMaxAgeSeconds' | 'sitemapName' | 'excludeAppSources' | 'sortEntries' | 'defaultSitemapsChunkSize' | 'xslColumns' | 'xslTips' | 'debug' | 'discoverImages' | 'discoverVideos' | 'autoLastmod' | 'xsl' | 'credits' | 'minify'> {
241
+ export interface ModuleRuntimeConfig extends Pick<ModuleOptions, 'sitemapsPathPrefix' | 'cacheMaxAgeSeconds' | 'sitemapName' | 'excludeAppSources' | 'sortEntries' | 'defaultSitemapsChunkSize' | 'xslColumns' | 'xslTips' | 'debug' | 'discoverImages' | 'discoverVideos' | 'autoLastmod' | 'xsl' | 'credits' | 'minify' | 'experimentalStreaming'> {
231
242
  version: string;
232
243
  isNuxtContentDocumentDriven: boolean;
233
244
  sitemaps: {