@nuxtjs/sitemap 7.2.10 → 7.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 (40) hide show
  1. package/dist/client/200.html +10 -9
  2. package/dist/client/404.html +10 -9
  3. package/dist/client/_nuxt/{DDzCRRw4.js → 5vafBU9X.js} +1 -1
  4. package/dist/client/_nuxt/BIHI7g3E.js +1 -0
  5. package/dist/client/_nuxt/Bn78IMkz.js +172 -0
  6. package/dist/client/_nuxt/{DdvkoY3I.js → CT3BV8Rj.js} +1 -1
  7. package/dist/client/_nuxt/Cp-IABpG.js +1 -0
  8. package/dist/client/_nuxt/builds/latest.json +1 -1
  9. package/dist/client/_nuxt/builds/meta/5ecca6e1-2b8a-4fc5-a128-35cbc27bf6d7.json +1 -0
  10. package/dist/client/_nuxt/{entry.BrROiVtQ.css → entry.BzbtAPc0.css} +1 -1
  11. package/dist/client/_nuxt/error-404.D_zhMyJm.css +1 -0
  12. package/dist/client/_nuxt/error-500.rdOYVbxo.css +1 -0
  13. package/dist/client/index.html +10 -9
  14. package/dist/module.cjs +59 -20
  15. package/dist/module.json +2 -2
  16. package/dist/module.mjs +60 -21
  17. package/dist/runtime/server/plugins/warm-up.js +20 -4
  18. package/dist/runtime/server/routes/__sitemap__/debug.js +2 -2
  19. package/dist/runtime/server/routes/sitemap/[sitemap].xml.js +37 -7
  20. package/dist/runtime/server/routes/sitemap_index.xml.js +11 -3
  21. package/dist/runtime/server/sitemap/builder/sitemap-index.d.ts +1 -1
  22. package/dist/runtime/server/sitemap/builder/sitemap-index.js +110 -16
  23. package/dist/runtime/server/sitemap/builder/sitemap.d.ts +1 -1
  24. package/dist/runtime/server/sitemap/builder/sitemap.js +62 -36
  25. package/dist/runtime/server/sitemap/builder/xml.d.ts +2 -3
  26. package/dist/runtime/server/sitemap/builder/xml.js +182 -80
  27. package/dist/runtime/server/sitemap/nitro.js +68 -20
  28. package/dist/runtime/server/sitemap/urlset/normalise.js +21 -19
  29. package/dist/runtime/server/sitemap/urlset/sort.d.ts +1 -1
  30. package/dist/runtime/server/sitemap/urlset/sort.js +9 -15
  31. package/dist/runtime/server/sitemap/utils/chunk.d.ts +10 -0
  32. package/dist/runtime/server/sitemap/utils/chunk.js +66 -0
  33. package/dist/runtime/types.d.ts +44 -0
  34. package/dist/runtime/utils-pure.js +13 -5
  35. package/package.json +23 -22
  36. package/dist/client/_nuxt/BLmTiKMJ.js +0 -1
  37. package/dist/client/_nuxt/BUP090M8.js +0 -172
  38. package/dist/client/_nuxt/builds/meta/3c351607-eab3-459a-b743-aba04e49a80e.json +0 -1
  39. package/dist/client/_nuxt/error-404.BMkETmdU.css +0 -1
  40. package/dist/client/_nuxt/error-500.C3_I-O7u.css +0 -1
@@ -1,86 +1,188 @@
1
- function resolveKey(k) {
2
- switch (k) {
3
- case "images":
4
- return "image";
5
- case "videos":
6
- return "video";
7
- // news & others?
8
- case "news":
9
- return "news";
10
- default:
11
- return k;
12
- }
1
+ export function escapeValueForXml(value) {
2
+ if (value === true || value === false)
3
+ return value ? "yes" : "no";
4
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
13
5
  }
14
- function handleObject(key, obj) {
15
- return [
16
- ` <${key}:${key}>`,
17
- ...Object.entries(obj).map(([sk, sv]) => {
18
- if (key === "video" && Array.isArray(sv)) {
19
- return sv.map((v) => {
20
- if (typeof v === "string") {
21
- return [
22
- ` `,
23
- `<${key}:${sk}>`,
24
- escapeValueForXml(v),
25
- `</${key}:${sk}>`
26
- ].join("");
6
+ const URLSET_OPENING_TAG = '<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd http://www.google.com/schemas/sitemap-image/1.1 http://www.google.com/schemas/sitemap-image/1.1/sitemap-image.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
7
+ function buildUrlXml(url) {
8
+ const capacity = 50;
9
+ const parts = Array.from({ length: capacity });
10
+ let partIndex = 0;
11
+ parts[partIndex++] = " <url>";
12
+ if (url.loc) {
13
+ parts[partIndex++] = ` <loc>${escapeValueForXml(url.loc)}</loc>`;
14
+ }
15
+ if (url.lastmod) {
16
+ parts[partIndex++] = ` <lastmod>${url.lastmod}</lastmod>`;
17
+ }
18
+ if (url.changefreq) {
19
+ parts[partIndex++] = ` <changefreq>${url.changefreq}</changefreq>`;
20
+ }
21
+ if (url.priority !== void 0) {
22
+ const priorityValue = Number.parseFloat(String(url.priority));
23
+ const formattedPriority = priorityValue % 1 === 0 ? String(priorityValue) : priorityValue.toFixed(1);
24
+ parts[partIndex++] = ` <priority>${formattedPriority}</priority>`;
25
+ }
26
+ const keys = Object.keys(url).filter((k) => !k.startsWith("_") && !["loc", "lastmod", "changefreq", "priority"].includes(k));
27
+ for (const key of keys) {
28
+ const value = url[key];
29
+ if (value === void 0 || value === null) continue;
30
+ switch (key) {
31
+ case "alternatives":
32
+ if (Array.isArray(value) && value.length > 0) {
33
+ for (const alt of value) {
34
+ const attrs = Object.entries(alt).map(([k, v]) => `${k}="${escapeValueForXml(v)}"`).join(" ");
35
+ parts[partIndex++] = ` <xhtml:link rel="alternate" ${attrs} />`;
27
36
  }
28
- const attributes = Object.entries(v).filter(([ssk]) => ssk !== sk).map(([ssk, ssv]) => `${ssk}="${escapeValueForXml(ssv)}"`).join(" ");
29
- return [
30
- ` <${key}:${sk} ${attributes}>`,
31
- // value is the same sk
32
- v[sk],
33
- `</${key}:${sk}>`
34
- ].join("");
35
- }).join("\n");
36
- }
37
- if (typeof sv === "object") {
38
- if (key === "video") {
39
- const attributes = Object.entries(sv).filter(([ssk]) => ssk !== sk).map(([ssk, ssv]) => `${ssk}="${escapeValueForXml(ssv)}"`).join(" ");
40
- return [
41
- ` <${key}:${sk} ${attributes}>`,
42
- // value is the same sk
43
- sv[sk],
44
- `</${key}:${sk}>`
45
- ].join("");
46
37
  }
47
- return [
48
- ` <${key}:${sk}>`,
49
- ...Object.entries(sv).map(([ssk, ssv]) => ` <${key}:${ssk}>${escapeValueForXml(ssv)}</${key}:${ssk}>`),
50
- ` </${key}:${sk}>`
51
- ].join("\n");
52
- }
53
- return ` <${key}:${sk}>${escapeValueForXml(sv)}</${key}:${sk}>`;
54
- }),
55
- ` </${key}:${key}>`
56
- ].join("\n");
57
- }
58
- function handleArray(key, arr) {
59
- if (arr.length === 0)
60
- return false;
61
- key = resolveKey(key);
62
- if (key === "alternatives") {
63
- return arr.map((obj) => [
64
- ` <xhtml:link rel="alternate" ${Object.entries(obj).map(([sk, sv]) => `${sk}="${escapeValueForXml(sv)}"`).join(" ")} />`
65
- ].join("\n")).join("\n");
38
+ break;
39
+ case "images":
40
+ if (Array.isArray(value) && value.length > 0) {
41
+ for (const img of value) {
42
+ parts[partIndex++] = " <image:image>";
43
+ parts[partIndex++] = ` <image:loc>${escapeValueForXml(img.loc)}</image:loc>`;
44
+ if (img.title) parts[partIndex++] = ` <image:title>${escapeValueForXml(img.title)}</image:title>`;
45
+ if (img.caption) parts[partIndex++] = ` <image:caption>${escapeValueForXml(img.caption)}</image:caption>`;
46
+ if (img.geo_location) parts[partIndex++] = ` <image:geo_location>${escapeValueForXml(img.geo_location)}</image:geo_location>`;
47
+ if (img.license) parts[partIndex++] = ` <image:license>${escapeValueForXml(img.license)}</image:license>`;
48
+ parts[partIndex++] = " </image:image>";
49
+ }
50
+ }
51
+ break;
52
+ case "videos":
53
+ if (Array.isArray(value) && value.length > 0) {
54
+ for (const video of value) {
55
+ parts[partIndex++] = " <video:video>";
56
+ parts[partIndex++] = ` <video:title>${escapeValueForXml(video.title)}</video:title>`;
57
+ if (video.thumbnail_loc) {
58
+ parts[partIndex++] = ` <video:thumbnail_loc>${escapeValueForXml(video.thumbnail_loc)}</video:thumbnail_loc>`;
59
+ }
60
+ parts[partIndex++] = ` <video:description>${escapeValueForXml(video.description)}</video:description>`;
61
+ if (video.content_loc) {
62
+ parts[partIndex++] = ` <video:content_loc>${escapeValueForXml(video.content_loc)}</video:content_loc>`;
63
+ }
64
+ if (video.player_loc) {
65
+ const attrs = video.player_loc.allow_embed ? ' allow_embed="yes"' : "";
66
+ const autoplay = video.player_loc.autoplay ? ' autoplay="yes"' : "";
67
+ parts[partIndex++] = ` <video:player_loc${attrs}${autoplay}>${escapeValueForXml(video.player_loc)}</video:player_loc>`;
68
+ }
69
+ if (video.duration !== void 0) {
70
+ parts[partIndex++] = ` <video:duration>${video.duration}</video:duration>`;
71
+ }
72
+ if (video.expiration_date) {
73
+ parts[partIndex++] = ` <video:expiration_date>${video.expiration_date}</video:expiration_date>`;
74
+ }
75
+ if (video.rating !== void 0) {
76
+ parts[partIndex++] = ` <video:rating>${video.rating}</video:rating>`;
77
+ }
78
+ if (video.view_count !== void 0) {
79
+ parts[partIndex++] = ` <video:view_count>${video.view_count}</video:view_count>`;
80
+ }
81
+ if (video.publication_date) {
82
+ parts[partIndex++] = ` <video:publication_date>${video.publication_date}</video:publication_date>`;
83
+ }
84
+ if (video.family_friendly !== void 0) {
85
+ parts[partIndex++] = ` <video:family_friendly>${video.family_friendly === "yes" || video.family_friendly === true ? "yes" : "no"}</video:family_friendly>`;
86
+ }
87
+ if (video.restriction) {
88
+ const relationship = video.restriction.relationship || "allow";
89
+ parts[partIndex++] = ` <video:restriction relationship="${relationship}">${escapeValueForXml(video.restriction.restriction)}</video:restriction>`;
90
+ }
91
+ if (video.platform) {
92
+ const relationship = video.platform.relationship || "allow";
93
+ parts[partIndex++] = ` <video:platform relationship="${relationship}">${escapeValueForXml(video.platform.platform)}</video:platform>`;
94
+ }
95
+ if (video.requires_subscription !== void 0) {
96
+ parts[partIndex++] = ` <video:requires_subscription>${video.requires_subscription === "yes" || video.requires_subscription === true ? "yes" : "no"}</video:requires_subscription>`;
97
+ }
98
+ if (video.price) {
99
+ const prices = Array.isArray(video.price) ? video.price : [video.price];
100
+ for (const price of prices) {
101
+ const attrs = [];
102
+ if (price.currency) attrs.push(`currency="${price.currency}"`);
103
+ if (price.type) attrs.push(`type="${price.type}"`);
104
+ const attrsStr = attrs.length > 0 ? " " + attrs.join(" ") : "";
105
+ parts[partIndex++] = ` <video:price${attrsStr}>${escapeValueForXml(price.price)}</video:price>`;
106
+ }
107
+ }
108
+ if (video.uploader) {
109
+ const info = video.uploader.info ? ` info="${escapeValueForXml(video.uploader.info)}"` : "";
110
+ parts[partIndex++] = ` <video:uploader${info}>${escapeValueForXml(video.uploader.uploader)}</video:uploader>`;
111
+ }
112
+ if (video.live !== void 0) {
113
+ parts[partIndex++] = ` <video:live>${video.live === "yes" || video.live === true ? "yes" : "no"}</video:live>`;
114
+ }
115
+ if (video.tag) {
116
+ const tags = Array.isArray(video.tag) ? video.tag : [video.tag];
117
+ for (const tag of tags) {
118
+ parts[partIndex++] = ` <video:tag>${escapeValueForXml(tag)}</video:tag>`;
119
+ }
120
+ }
121
+ if (video.category) {
122
+ parts[partIndex++] = ` <video:category>${escapeValueForXml(video.category)}</video:category>`;
123
+ }
124
+ if (video.gallery_loc) {
125
+ const title = video.gallery_loc.title ? ` title="${escapeValueForXml(video.gallery_loc.title)}"` : "";
126
+ parts[partIndex++] = ` <video:gallery_loc${title}>${escapeValueForXml(video.gallery_loc)}</video:gallery_loc>`;
127
+ }
128
+ parts[partIndex++] = " </video:video>";
129
+ }
130
+ }
131
+ break;
132
+ case "news":
133
+ if (value) {
134
+ parts[partIndex++] = " <news:news>";
135
+ parts[partIndex++] = " <news:publication>";
136
+ parts[partIndex++] = ` <news:name>${escapeValueForXml(value.publication.name)}</news:name>`;
137
+ parts[partIndex++] = ` <news:language>${escapeValueForXml(value.publication.language)}</news:language>`;
138
+ parts[partIndex++] = " </news:publication>";
139
+ if (value.title) {
140
+ parts[partIndex++] = ` <news:title>${escapeValueForXml(value.title)}</news:title>`;
141
+ }
142
+ if (value.publication_date) {
143
+ parts[partIndex++] = ` <news:publication_date>${value.publication_date}</news:publication_date>`;
144
+ }
145
+ if (value.access) {
146
+ parts[partIndex++] = ` <news:access>${value.access}</news:access>`;
147
+ }
148
+ if (value.genres) {
149
+ parts[partIndex++] = ` <news:genres>${escapeValueForXml(value.genres)}</news:genres>`;
150
+ }
151
+ if (value.keywords) {
152
+ parts[partIndex++] = ` <news:keywords>${escapeValueForXml(value.keywords)}</news:keywords>`;
153
+ }
154
+ if (value.stock_tickers) {
155
+ parts[partIndex++] = ` <news:stock_tickers>${escapeValueForXml(value.stock_tickers)}</news:stock_tickers>`;
156
+ }
157
+ parts[partIndex++] = " </news:news>";
158
+ }
159
+ break;
160
+ }
66
161
  }
67
- return arr.map((obj) => handleObject(key, obj)).join("\n");
68
- }
69
- export function handleEntry(k, e) {
70
- return Array.isArray(e[k]) ? handleArray(k, e[k]) : typeof e[k] === "object" ? handleObject(k, e[k]) : ` <${k}>${escapeValueForXml(e[k])}</${k}>`;
162
+ parts[partIndex++] = " </url>";
163
+ return parts.slice(0, partIndex).join("\n");
71
164
  }
72
- export function wrapSitemapXml(input, resolvers, options) {
73
- const xsl = options.xsl ? resolvers.relativeBaseUrlResolver(options.xsl) : false;
74
- const credits = options.credits;
75
- input.unshift(`<?xml version="1.0" encoding="UTF-8"?>${xsl ? `<?xml-stylesheet type="text/xsl" href="${xsl}"?>` : ""}`);
76
- if (credits)
77
- input.push(`<!-- XML Sitemap generated by @nuxtjs/sitemap v${options.version} at ${(/* @__PURE__ */ new Date()).toISOString()} -->`);
78
- if (options.minify)
79
- return input.join("").replace(/(?<!<[^>]*)\s(?![^<]*>)/g, "");
80
- return input.join("\n");
81
- }
82
- export function escapeValueForXml(value) {
83
- if (value === true || value === false)
84
- return value ? "yes" : "no";
85
- return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
165
+ export function urlsToXml(urls, resolvers, { version, xsl, credits, minify }) {
166
+ const estimatedSize = urls.length + 5;
167
+ const xmlParts = Array.from({ length: estimatedSize });
168
+ let partIndex = 0;
169
+ const xslHref = xsl ? resolvers.relativeBaseUrlResolver(xsl) : false;
170
+ if (xslHref) {
171
+ xmlParts[partIndex++] = `<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet type="text/xsl" href="${xslHref}"?>`;
172
+ } else {
173
+ xmlParts[partIndex++] = '<?xml version="1.0" encoding="UTF-8"?>';
174
+ }
175
+ xmlParts[partIndex++] = URLSET_OPENING_TAG;
176
+ for (const url of urls) {
177
+ xmlParts[partIndex++] = buildUrlXml(url);
178
+ }
179
+ xmlParts[partIndex++] = "</urlset>";
180
+ if (credits) {
181
+ xmlParts[partIndex++] = `<!-- XML Sitemap generated by @nuxtjs/sitemap v${version} at ${(/* @__PURE__ */ new Date()).toISOString()} -->`;
182
+ }
183
+ const xmlContent = xmlParts.slice(0, partIndex);
184
+ if (minify) {
185
+ return xmlContent.join("").replace(/(?<!<[^>]*)\s(?![^<]*>)/g, "");
186
+ }
187
+ return xmlContent.join("\n");
86
188
  }
@@ -1,12 +1,12 @@
1
- import { getQuery, setHeader, createError } from "h3";
1
+ import { getQuery, setHeader, createError, getHeader } from "h3";
2
2
  import { fixSlashes } from "nuxt-site-config/urls";
3
3
  import { defu } from "defu";
4
- import { useNitroApp } from "nitropack/runtime";
4
+ import { useNitroApp, defineCachedFunction } from "nitropack/runtime";
5
5
  import { logger, mergeOnKey, splitForLocales } from "../../utils-pure.js";
6
6
  import { createNitroRouteRuleMatcher } from "../kit.js";
7
7
  import { buildSitemapUrls, urlsToXml } from "./builder/sitemap.js";
8
8
  import { normaliseEntry, preNormalizeEntry } from "./urlset/normalise.js";
9
- import { sortSitemapUrls } from "./urlset/sort.js";
9
+ import { sortInPlace } from "./urlset/sort.js";
10
10
  import { getPathRobotConfig } from "#imports";
11
11
  import { useSiteConfig } from "#site-config/server/composables/useSiteConfig";
12
12
  import { createSitePathResolver } from "#site-config/server/composables/utils";
@@ -26,7 +26,7 @@ export function useNitroUrlResolvers(e) {
26
26
  relativeBaseUrlResolver: createSitePathResolver(e, { absolute: false, withBase: true })
27
27
  };
28
28
  }
29
- export async function createSitemap(event, definition, runtimeConfig) {
29
+ async function buildSitemapXml(event, definition, resolvers, runtimeConfig) {
30
30
  const { sitemapName } = definition;
31
31
  const nitro = useNitroApp();
32
32
  if (import.meta.prerender) {
@@ -41,14 +41,15 @@ export async function createSitemap(event, definition, runtimeConfig) {
41
41
  });
42
42
  }
43
43
  }
44
- const resolvers = useNitroUrlResolvers(event);
45
- let sitemapUrls = await buildSitemapUrls(definition, resolvers, runtimeConfig, nitro);
44
+ const sitemapUrls = await buildSitemapUrls(definition, resolvers, runtimeConfig, nitro);
46
45
  const routeRuleMatcher = createNitroRouteRuleMatcher();
47
46
  const { autoI18n } = runtimeConfig;
48
- sitemapUrls = sitemapUrls.map((u) => {
47
+ let validCount = 0;
48
+ for (let i = 0; i < sitemapUrls.length; i++) {
49
+ const u = sitemapUrls[i];
49
50
  const path = u._path?.pathname || u.loc;
50
51
  if (!getPathRobotConfig(event, { path, skipSiteIndexable: true }).indexable)
51
- return false;
52
+ continue;
52
53
  let routeRules = routeRuleMatcher(path);
53
54
  if (autoI18n?.locales && autoI18n?.strategy !== "no_prefix") {
54
55
  const match = splitForLocales(path, autoI18n.locales.map((l) => l.code));
@@ -57,15 +58,15 @@ export async function createSitemap(event, definition, runtimeConfig) {
57
58
  routeRules = defu(routeRules, routeRuleMatcher(pathWithoutPrefix));
58
59
  }
59
60
  if (routeRules.sitemap === false)
60
- return false;
61
- if (typeof routeRules.robots !== "undefined" && !routeRules.robots) {
62
- return false;
63
- }
61
+ continue;
62
+ if (typeof routeRules.robots !== "undefined" && !routeRules.robots)
63
+ continue;
64
64
  const hasRobotsDisabled = Object.entries(routeRules.headers || {}).some(([name, value]) => name.toLowerCase() === "x-robots-tag" && value.toLowerCase().includes("noindex"));
65
65
  if (routeRules.redirect || hasRobotsDisabled)
66
- return false;
67
- return routeRules.sitemap ? defu(u, routeRules.sitemap) : u;
68
- }).filter(Boolean);
66
+ continue;
67
+ sitemapUrls[validCount++] = routeRules.sitemap ? defu(u, routeRules.sitemap) : u;
68
+ }
69
+ sitemapUrls.length = validCount;
69
70
  const locSize = sitemapUrls.length;
70
71
  const resolvedCtx = {
71
72
  urls: sitemapUrls,
@@ -76,17 +77,64 @@ export async function createSitemap(event, definition, runtimeConfig) {
76
77
  if (resolvedCtx.urls.length !== locSize) {
77
78
  resolvedCtx.urls = resolvedCtx.urls.map((e) => preNormalizeEntry(e, resolvers));
78
79
  }
79
- const maybeSort = (urls2) => runtimeConfig.sortEntries ? sortSitemapUrls(urls2) : urls2;
80
+ const maybeSort = (urls2) => runtimeConfig.sortEntries ? sortInPlace(urls2) : urls2;
80
81
  const normalizedPreDedupe = resolvedCtx.urls.map((e) => normaliseEntry(e, definition.defaults, resolvers));
81
82
  const urls = maybeSort(mergeOnKey(normalizedPreDedupe, "_key").map((e) => normaliseEntry(e, definition.defaults, resolvers)));
83
+ if (definition._isChunking && definition.sitemapName.includes("-")) {
84
+ const parts = definition.sitemapName.split("-");
85
+ const lastPart = parts.pop();
86
+ if (!Number.isNaN(Number(lastPart))) {
87
+ const chunkIndex = Number(lastPart);
88
+ const baseSitemapName = parts.join("-");
89
+ if (urls.length === 0 && chunkIndex > 0) {
90
+ throw createError({
91
+ statusCode: 404,
92
+ message: `Sitemap chunk ${chunkIndex} for "${baseSitemapName}" does not exist.`
93
+ });
94
+ }
95
+ }
96
+ }
82
97
  const sitemap = urlsToXml(urls, resolvers, runtimeConfig);
83
98
  const ctx = { sitemap, sitemapName, event };
84
99
  await nitro.hooks.callHook("sitemap:output", ctx);
100
+ return ctx.sitemap;
101
+ }
102
+ const buildSitemapXmlCached = defineCachedFunction(
103
+ buildSitemapXml,
104
+ {
105
+ name: "sitemap:xml",
106
+ group: "sitemap",
107
+ maxAge: 60 * 10,
108
+ // Default 10 minutes
109
+ base: "sitemap",
110
+ // Use the sitemap storage
111
+ getKey: (event, definition) => {
112
+ const host = getHeader(event, "host") || getHeader(event, "x-forwarded-host") || "";
113
+ const proto = getHeader(event, "x-forwarded-proto") || "https";
114
+ const sitemapName = definition.sitemapName || "default";
115
+ return `${sitemapName}-${proto}-${host}`;
116
+ },
117
+ swr: true
118
+ // Enable stale-while-revalidate
119
+ }
120
+ );
121
+ export async function createSitemap(event, definition, runtimeConfig) {
122
+ const resolvers = useNitroUrlResolvers(event);
123
+ const shouldCache = !import.meta.dev && runtimeConfig.cacheMaxAgeSeconds > 0;
124
+ const xml = shouldCache ? await buildSitemapXmlCached(event, definition, resolvers, runtimeConfig) : await buildSitemapXml(event, definition, resolvers, runtimeConfig);
85
125
  setHeader(event, "Content-Type", "text/xml; charset=UTF-8");
86
- if (runtimeConfig.cacheMaxAgeSeconds)
87
- setHeader(event, "Cache-Control", `public, max-age=${runtimeConfig.cacheMaxAgeSeconds}, must-revalidate`);
88
- else
126
+ if (runtimeConfig.cacheMaxAgeSeconds) {
127
+ setHeader(event, "Cache-Control", `public, max-age=${runtimeConfig.cacheMaxAgeSeconds}, s-maxage=${runtimeConfig.cacheMaxAgeSeconds}, stale-while-revalidate=3600`);
128
+ const now = /* @__PURE__ */ new Date();
129
+ setHeader(event, "X-Sitemap-Generated", now.toISOString());
130
+ setHeader(event, "X-Sitemap-Cache-Duration", `${runtimeConfig.cacheMaxAgeSeconds}s`);
131
+ const expiryTime = new Date(now.getTime() + runtimeConfig.cacheMaxAgeSeconds * 1e3);
132
+ setHeader(event, "X-Sitemap-Cache-Expires", expiryTime.toISOString());
133
+ const remainingSeconds = Math.floor((expiryTime.getTime() - now.getTime()) / 1e3);
134
+ setHeader(event, "X-Sitemap-Cache-Remaining", `${remainingSeconds}s`);
135
+ } else {
89
136
  setHeader(event, "Cache-Control", `no-cache, no-store`);
137
+ }
90
138
  event.context._isSitemap = true;
91
- return ctx.sitemap;
139
+ return xml;
92
140
  }
@@ -75,29 +75,31 @@ export function normaliseEntry(_e, defaults, resolvers) {
75
75
  delete e.lastmod;
76
76
  e.loc = resolve(e.loc, resolvers);
77
77
  if (e.alternatives) {
78
- e.alternatives = mergeOnKey(e.alternatives.map((e2) => {
79
- const a = { ...e2 };
80
- if (typeof a.href === "string")
81
- a.href = resolve(a.href, resolvers);
82
- else if (typeof a.href === "object" && a.href)
83
- a.href = resolve(a.href.href, resolvers);
84
- return a;
85
- }), "hreflang");
78
+ const alternatives = e.alternatives;
79
+ for (let i = 0; i < alternatives.length; i++) {
80
+ const alt = alternatives[i];
81
+ if (typeof alt.href === "string") {
82
+ alt.href = resolve(alt.href, resolvers);
83
+ } else if (typeof alt.href === "object" && alt.href) {
84
+ alt.href = resolve(alt.href.href, resolvers);
85
+ }
86
+ }
87
+ e.alternatives = mergeOnKey(alternatives, "hreflang");
86
88
  }
87
89
  if (e.images) {
88
- e.images = mergeOnKey(e.images.map((i) => {
89
- i = { ...i };
90
- i.loc = resolve(i.loc, resolvers);
91
- return i;
92
- }), "loc");
90
+ const images = e.images;
91
+ for (let i = 0; i < images.length; i++) {
92
+ images[i].loc = resolve(images[i].loc, resolvers);
93
+ }
94
+ e.images = mergeOnKey(images, "loc");
93
95
  }
94
96
  if (e.videos) {
95
- e.videos = e.videos.map((v) => {
96
- v = { ...v };
97
- if (v.content_loc)
98
- v.content_loc = resolve(v.content_loc, resolvers);
99
- return v;
100
- });
97
+ const videos = e.videos;
98
+ for (let i = 0; i < videos.length; i++) {
99
+ if (videos[i].content_loc) {
100
+ videos[i].content_loc = resolve(videos[i].content_loc, resolvers);
101
+ }
102
+ }
101
103
  }
102
104
  return e;
103
105
  }
@@ -1,2 +1,2 @@
1
1
  import type { ResolvedSitemapUrl, SitemapUrlInput } from '../../../types.js';
2
- export declare function sortSitemapUrls<T extends SitemapUrlInput[] | ResolvedSitemapUrl[]>(urls: T): T;
2
+ export declare function sortInPlace<T extends SitemapUrlInput[] | ResolvedSitemapUrl[]>(urls: T): T;
@@ -1,19 +1,13 @@
1
- export function sortSitemapUrls(urls) {
2
- return urls.sort(
3
- (a, b) => {
4
- const aLoc = typeof a === "string" ? a : a.loc;
5
- const bLoc = typeof b === "string" ? b : b.loc;
6
- return aLoc.localeCompare(bLoc, void 0, { numeric: true });
7
- }
8
- ).sort((a, b) => {
9
- const aLoc = (typeof a === "string" ? a : a.loc) || "";
10
- const bLoc = (typeof b === "string" ? b : b.loc) || "";
1
+ export function sortInPlace(urls) {
2
+ urls.sort((a, b) => {
3
+ const aLoc = typeof a === "string" ? a : a.loc;
4
+ const bLoc = typeof b === "string" ? b : b.loc;
11
5
  const aSegments = aLoc.split("/").length;
12
6
  const bSegments = bLoc.split("/").length;
13
- if (aSegments > bSegments)
14
- return 1;
15
- if (aSegments < bSegments)
16
- return -1;
17
- return 0;
7
+ if (aSegments !== bSegments) {
8
+ return aSegments - bSegments;
9
+ }
10
+ return aLoc.localeCompare(bLoc, void 0, { numeric: true });
18
11
  });
12
+ return urls;
19
13
  }
@@ -0,0 +1,10 @@
1
+ import type { ModuleRuntimeConfig, SitemapDefinition } from '../../../types.js';
2
+ export interface ChunkInfo {
3
+ isChunked: boolean;
4
+ baseSitemapName: string;
5
+ chunkIndex?: number;
6
+ chunkSize: number;
7
+ }
8
+ export declare function parseChunkInfo(sitemapName: string, sitemaps: ModuleRuntimeConfig['sitemaps'], defaultChunkSize?: number): ChunkInfo;
9
+ export declare function getSitemapConfig(sitemapName: string, sitemaps: ModuleRuntimeConfig['sitemaps'], defaultChunkSize?: number): SitemapDefinition;
10
+ export declare function sliceUrlsForChunk<T>(urls: T[], sitemapName: string, sitemaps: ModuleRuntimeConfig['sitemaps'], defaultChunkSize?: number): T[];
@@ -0,0 +1,66 @@
1
+ export function parseChunkInfo(sitemapName, sitemaps, defaultChunkSize = 1e3) {
2
+ if (typeof sitemaps.chunks !== "undefined" && !Number.isNaN(Number(sitemapName))) {
3
+ return {
4
+ isChunked: true,
5
+ baseSitemapName: "sitemap",
6
+ chunkIndex: Number(sitemapName),
7
+ chunkSize: defaultChunkSize
8
+ };
9
+ }
10
+ if (sitemapName.includes("-")) {
11
+ const parts = sitemapName.split("-");
12
+ const lastPart = parts.pop();
13
+ if (!Number.isNaN(Number(lastPart))) {
14
+ const baseSitemapName = parts.join("-");
15
+ const baseSitemap = sitemaps[baseSitemapName];
16
+ if (baseSitemap && (baseSitemap.chunks || baseSitemap._isChunking)) {
17
+ const chunkSize = typeof baseSitemap.chunks === "number" ? baseSitemap.chunks : baseSitemap.chunkSize || defaultChunkSize;
18
+ return {
19
+ isChunked: true,
20
+ baseSitemapName,
21
+ chunkIndex: Number(lastPart),
22
+ chunkSize
23
+ };
24
+ }
25
+ }
26
+ }
27
+ return {
28
+ isChunked: false,
29
+ baseSitemapName: sitemapName,
30
+ chunkIndex: void 0,
31
+ chunkSize: defaultChunkSize
32
+ };
33
+ }
34
+ export function getSitemapConfig(sitemapName, sitemaps, defaultChunkSize = 1e3) {
35
+ const chunkInfo = parseChunkInfo(sitemapName, sitemaps, defaultChunkSize);
36
+ if (chunkInfo.isChunked) {
37
+ if (chunkInfo.baseSitemapName === "sitemap" && typeof sitemaps.chunks !== "undefined") {
38
+ return {
39
+ ...sitemaps.chunks,
40
+ sitemapName,
41
+ _isChunking: true,
42
+ _chunkSize: chunkInfo.chunkSize
43
+ };
44
+ }
45
+ const baseSitemap = sitemaps[chunkInfo.baseSitemapName];
46
+ if (baseSitemap) {
47
+ return {
48
+ ...baseSitemap,
49
+ sitemapName,
50
+ // Use the full name with chunk index
51
+ _isChunking: true,
52
+ _chunkSize: chunkInfo.chunkSize
53
+ };
54
+ }
55
+ }
56
+ return sitemaps[sitemapName];
57
+ }
58
+ export function sliceUrlsForChunk(urls, sitemapName, sitemaps, defaultChunkSize = 1e3) {
59
+ const chunkInfo = parseChunkInfo(sitemapName, sitemaps, defaultChunkSize);
60
+ if (chunkInfo.isChunked && chunkInfo.chunkIndex !== void 0) {
61
+ const startIndex = chunkInfo.chunkIndex * chunkInfo.chunkSize;
62
+ const endIndex = (chunkInfo.chunkIndex + 1) * chunkInfo.chunkSize;
63
+ return urls.slice(startIndex, endIndex);
64
+ }
65
+ return urls;
66
+ }
@@ -211,6 +211,7 @@ export interface AutoI18nConfig {
211
211
  })[];
212
212
  defaultLocale: string;
213
213
  strategy: 'prefix' | 'prefix_except_default' | 'prefix_and_default' | 'no_prefix';
214
+ pages?: Record<string, Record<string, string | false>>;
214
215
  }
215
216
  export interface ModuleRuntimeConfig extends Pick<ModuleOptions, 'sitemapsPathPrefix' | 'cacheMaxAgeSeconds' | 'sitemapName' | 'excludeAppSources' | 'sortEntries' | 'defaultSitemapsChunkSize' | 'xslColumns' | 'xslTips' | 'debug' | 'discoverImages' | 'discoverVideos' | 'autoLastmod' | 'xsl' | 'credits' | 'minify'> {
216
217
  version: string;
@@ -291,10 +292,49 @@ export interface SitemapDefinition {
291
292
  * Additional sources of URLs to include in the sitemap.
292
293
  */
293
294
  sources?: SitemapSourceInput[];
295
+ /**
296
+ * Whether to enable chunking for this sitemap.
297
+ *
298
+ * - `true`: Enable with default chunk size from `defaultSitemapsChunkSize`
299
+ * - `number`: Enable with specific chunk size (must be > 0)
300
+ * - `false` or `undefined`: Disable chunking
301
+ *
302
+ * Note: Chunking only applies to sitemaps with sources. URLs provided directly
303
+ * are not chunked.
304
+ *
305
+ * @default false
306
+ * @example true
307
+ * @example 5000
308
+ */
309
+ chunks?: boolean | number;
310
+ /**
311
+ * The maximum number of URLs per chunk when chunking is enabled.
312
+ * Takes precedence over the `chunks` property when both are specified.
313
+ * Also overrides the global `defaultSitemapsChunkSize`.
314
+ *
315
+ * Must be a positive integer.
316
+ *
317
+ * @default 1000
318
+ * @example 500
319
+ * @example 10000
320
+ */
321
+ chunkSize?: number;
294
322
  /**
295
323
  * @internal
296
324
  */
297
325
  _route?: string;
326
+ /**
327
+ * @internal
328
+ */
329
+ _isChunking?: boolean;
330
+ /**
331
+ * @internal
332
+ */
333
+ _chunkSize?: number;
334
+ /**
335
+ * @internal
336
+ */
337
+ _chunkCount?: number;
298
338
  }
299
339
  interface NitroBaseHook {
300
340
  event: H3Event;
@@ -314,6 +354,10 @@ export interface SitemapOutputHookCtx extends NitroBaseHook {
314
354
  sitemapName: string;
315
355
  sitemap: string;
316
356
  }
357
+ export interface SitemapSourcesHookCtx extends NitroBaseHook {
358
+ sitemapName: string;
359
+ sources: (SitemapSourceBase | SitemapSourceResolved)[];
360
+ }
317
361
  export type Changefreq = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
318
362
  export interface SitemapUrl {
319
363
  loc: string;