@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.
- package/README.md +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +8 -3
- package/dist/runtime/server/kit.js +9 -2
- 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.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 +70 -0
- package/dist/runtime/server/sitemap/builder/sitemap-index.d.ts +1 -4
- package/dist/runtime/server/sitemap/builder/sitemap-index.js +2 -34
- 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 +8 -0
- package/dist/runtime/server/sitemap/builder/xml.js +35 -6
- package/dist/runtime/server/sitemap/event-handlers.d.ts +3 -3
- package/dist/runtime/server/sitemap/event-handlers.js +18 -10
- package/dist/runtime/server/sitemap/nitro.d.ts +3 -1
- package/dist/runtime/server/sitemap/nitro.js +107 -20
- 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 +15 -4
- package/dist/runtime/server/sitemap/urlset/sort.js +12 -3
- package/dist/runtime/server/sitemap/urlset/sources.js +3 -3
- 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 +41 -19
- package/dist/utils.d.mts +30 -2
- package/dist/utils.d.ts +30 -2
- package/dist/utils.mjs +646 -43
- 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 {
|
|
6
|
-
import {
|
|
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 =
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
53
|
+
e.context._isSitemap = true;
|
|
54
|
+
return output;
|
|
47
55
|
}
|
|
48
56
|
export async function sitemapChildXmlEventHandler(e) {
|
|
49
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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 (
|
|
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,30 @@ 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
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
}
|
|
@@ -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" &&
|
|
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
|
|
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
|
-
)
|
|
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, "&").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
|
}
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -148,11 +148,22 @@ export interface ModuleOptions extends SitemapDefinition {
|
|
|
148
148
|
*/
|
|
149
149
|
experimentalWarmUp?: boolean;
|
|
150
150
|
/**
|
|
151
|
-
*
|
|
151
|
+
* Compress sitemap responses with gzip or deflate when supported by the client.
|
|
152
152
|
*
|
|
153
|
-
* @experimental
|
|
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: {
|