@saasmakers/ui 1.4.38 → 1.4.40

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.
@@ -0,0 +1,145 @@
1
+ import { safeGetHeader } from "../../utils/h3-compat.js";
2
+ import { defineCachedFunction } from "nitropack/runtime";
3
+ import { joinURL, withQuery } from "ufo";
4
+ import staticConfig from "#sitemap-virtual/static-config.mjs";
5
+ import { normaliseDate } from "../urlset/normalise.js";
6
+ import { getResolvedSitemapUrls } from "./sitemap.js";
7
+ import { escapeValueForXml } from "./xml.js";
8
+ const SERVER_CACHE_MAX_AGE = staticConfig.cacheMaxAgeSeconds || 60 * 10;
9
+ const buildSitemapIndexCached = defineCachedFunction(
10
+ async (event, resolvers, runtimeConfig, nitro) => {
11
+ return buildSitemapIndexInternal(resolvers, runtimeConfig, nitro);
12
+ },
13
+ {
14
+ name: "sitemap:index",
15
+ group: "sitemap",
16
+ maxAge: SERVER_CACHE_MAX_AGE,
17
+ base: "sitemap",
18
+ // Use the sitemap storage
19
+ getKey: (event) => {
20
+ const host = safeGetHeader(event, "host") || safeGetHeader(event, "x-forwarded-host") || "";
21
+ const proto = safeGetHeader(event, "x-forwarded-proto") || "https";
22
+ return `sitemap-index-${proto}-${host}`;
23
+ },
24
+ swr: true
25
+ // Enable stale-while-revalidate
26
+ }
27
+ );
28
+ async function buildSitemapIndexInternal(resolvers, runtimeConfig, nitro) {
29
+ const {
30
+ sitemaps,
31
+ autoLastmod,
32
+ defaultSitemapsChunkSize,
33
+ sitemapsPathPrefix
34
+ } = runtimeConfig;
35
+ if (!sitemaps)
36
+ throw new Error("Attempting to build a sitemap index without required `sitemaps` configuration.");
37
+ const nonChunkedNames = [];
38
+ const allFailedSources = [];
39
+ for (const sitemapName in sitemaps) {
40
+ if (sitemapName === "index" || sitemapName === "chunks")
41
+ continue;
42
+ const sitemapConfig = sitemaps[sitemapName];
43
+ if (sitemapConfig.chunks || sitemapConfig._isChunking) {
44
+ sitemapConfig._isChunking = true;
45
+ sitemapConfig._chunkSize = sitemapConfig.chunkSize || (typeof sitemapConfig.chunks === "number" ? sitemapConfig.chunks : defaultSitemapsChunkSize || 1e3);
46
+ } else {
47
+ nonChunkedNames.push(sitemapName);
48
+ }
49
+ }
50
+ const indexLastmod = autoLastmod ? normaliseDate(/* @__PURE__ */ new Date()) : void 0;
51
+ const entries = [];
52
+ if (typeof sitemaps.chunks !== "undefined") {
53
+ const sitemap = sitemaps.chunks;
54
+ const resolved = await getResolvedSitemapUrls(sitemap, "sitemap", true, resolvers, runtimeConfig, nitro);
55
+ allFailedSources.push(...resolved.failedSources);
56
+ const chunkCount = Math.ceil(resolved.urls.length / defaultSitemapsChunkSize);
57
+ for (let i = 0; i < chunkCount; i++) {
58
+ const entry = {
59
+ _sitemapName: String(i),
60
+ sitemap: resolvers.canonicalUrlResolver(joinURL(sitemapsPathPrefix || "", `/${i}.xml`))
61
+ };
62
+ if (indexLastmod)
63
+ entry.lastmod = indexLastmod;
64
+ entries.push(entry);
65
+ }
66
+ }
67
+ for (const name of nonChunkedNames) {
68
+ const entry = {
69
+ _sitemapName: name,
70
+ sitemap: resolvers.canonicalUrlResolver(joinURL(sitemapsPathPrefix || "", `/${name}.xml`))
71
+ };
72
+ if (indexLastmod)
73
+ entry.lastmod = indexLastmod;
74
+ entries.push(entry);
75
+ }
76
+ for (const sitemapName in sitemaps) {
77
+ const sitemapConfig = sitemaps[sitemapName];
78
+ if (sitemapName !== "index" && sitemapConfig._isChunking) {
79
+ const chunkSize = sitemapConfig._chunkSize || defaultSitemapsChunkSize || 1e3;
80
+ let chunkCount;
81
+ if (typeof sitemapConfig.chunkCount === "number" && sitemapConfig.chunkCount > 0) {
82
+ chunkCount = sitemapConfig.chunkCount;
83
+ } else {
84
+ const resolved = await getResolvedSitemapUrls(sitemapConfig, sitemapName, true, resolvers, runtimeConfig, nitro);
85
+ allFailedSources.push(...resolved.failedSources);
86
+ chunkCount = Math.ceil(resolved.urls.length / chunkSize);
87
+ }
88
+ sitemapConfig._chunkCount = chunkCount;
89
+ for (let i = 0; i < chunkCount; i++) {
90
+ const chunkName = `${sitemapName}-${i}`;
91
+ const entry = {
92
+ _sitemapName: chunkName,
93
+ sitemap: resolvers.canonicalUrlResolver(joinURL(sitemapsPathPrefix || "", `/${chunkName}.xml`))
94
+ };
95
+ if (indexLastmod)
96
+ entry.lastmod = indexLastmod;
97
+ entries.push(entry);
98
+ }
99
+ }
100
+ }
101
+ if (sitemaps.index) {
102
+ entries.push(...sitemaps.index.sitemaps.map((entry) => {
103
+ return typeof entry === "string" ? { sitemap: entry } : entry;
104
+ }));
105
+ }
106
+ return { entries, failedSources: allFailedSources };
107
+ }
108
+ export function urlsToIndexXml(sitemaps, resolvers, { version, xsl, credits, minify }, errorInfo) {
109
+ const sitemapXml = sitemaps.map((e) => [
110
+ " <sitemap>",
111
+ ` <loc>${escapeValueForXml(e.sitemap)}</loc>`,
112
+ // lastmod is optional
113
+ e.lastmod ? ` <lastmod>${escapeValueForXml(e.lastmod)}</lastmod>` : false,
114
+ " </sitemap>"
115
+ ].filter(Boolean).join("\n")).join("\n");
116
+ const xmlParts = [
117
+ '<?xml version="1.0" encoding="UTF-8"?>'
118
+ ];
119
+ if (xsl) {
120
+ let relativeBaseUrl = resolvers.relativeBaseUrlResolver?.(xsl) ?? xsl;
121
+ if (errorInfo && errorInfo.messages.length > 0) {
122
+ relativeBaseUrl = withQuery(relativeBaseUrl, {
123
+ errors: "true",
124
+ error_messages: errorInfo.messages,
125
+ error_urls: errorInfo.urls
126
+ });
127
+ }
128
+ xmlParts.push(`<?xml-stylesheet type="text/xsl" href="${escapeValueForXml(relativeBaseUrl)}"?>`);
129
+ }
130
+ xmlParts.push(
131
+ '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
132
+ sitemapXml,
133
+ "</sitemapindex>"
134
+ );
135
+ if (credits) {
136
+ xmlParts.push(`<!-- XML Sitemap Index generated by @nuxtjs/sitemap v${version} at ${(/* @__PURE__ */ new Date()).toISOString()} -->`);
137
+ }
138
+ return minify ? xmlParts.join("").replace(/(?<!<[^>]*)\s(?![^<]*>)/g, "") : xmlParts.join("\n");
139
+ }
140
+ export async function buildSitemapIndex(resolvers, runtimeConfig, nitro) {
141
+ if (!import.meta.dev && !import.meta.prerender && typeof runtimeConfig.cacheMaxAgeSeconds === "number" && runtimeConfig.cacheMaxAgeSeconds > 0 && resolvers.event) {
142
+ return buildSitemapIndexCached(resolvers.event, resolvers, runtimeConfig, nitro);
143
+ }
144
+ return buildSitemapIndexInternal(resolvers, runtimeConfig, nitro);
145
+ }
@@ -0,0 +1,276 @@
1
+ import { safeGetHeader } from "../../utils/h3-compat.js";
2
+ import { defineCachedFunction, useRuntimeConfig } from "nitropack/runtime";
3
+ import { resolveSitePath } from "nuxt-site-config/urls";
4
+ import { joinURL, withHttps } from "ufo";
5
+ import staticConfig from "#sitemap-virtual/static-config.mjs";
6
+ import { applyDynamicParams, createPathFilter, findPageMapping, logger, resolveI18nSitemapLocaleKey, splitForLocales } from "../../../utils-pure.js";
7
+ import { preNormalizeEntry } from "../urlset/normalise.js";
8
+ import { sortInPlace } from "../urlset/sort.js";
9
+ import { childSitemapSources, globalSitemapSources, resolveSitemapSources } from "../urlset/sources.js";
10
+ import { parseChunkInfo, sliceUrlsForChunk } from "../utils/chunk.js";
11
+ const SERVER_CACHE_MAX_AGE = staticConfig.cacheMaxAgeSeconds || 60 * 10;
12
+ export function resolveSitemapEntries(sitemap, urls, runtimeConfig, resolvers, baseURL) {
13
+ const {
14
+ autoI18n,
15
+ isI18nMapped
16
+ } = runtimeConfig;
17
+ const filterPath = createPathFilter({
18
+ include: sitemap.include,
19
+ exclude: sitemap.exclude
20
+ }, baseURL || "/");
21
+ const _urls = urls.map((_e) => {
22
+ const e = preNormalizeEntry(_e, resolvers);
23
+ if (!e.loc || !filterPath(e.loc))
24
+ return false;
25
+ return e;
26
+ }).filter(Boolean);
27
+ let validI18nUrlsForTransform = [];
28
+ const withoutPrefixPaths = {};
29
+ if (autoI18n && autoI18n.strategy !== "no_prefix") {
30
+ const localeCodes = autoI18n.locales.map((l) => l.code);
31
+ const localeByCode = new Map(autoI18n.locales.map((l) => [l.code, l]));
32
+ const isPrefixStrategy = autoI18n.strategy === "prefix";
33
+ const isPrefixExceptOrAndDefault = autoI18n.strategy === "prefix_and_default" || autoI18n.strategy === "prefix_except_default";
34
+ const xDefaultAndLocales = [{ code: "x-default", _hreflang: "x-default" }, ...autoI18n.locales];
35
+ const defaultLocale = autoI18n.defaultLocale;
36
+ const hasPages = !!autoI18n.pages;
37
+ const hasDifferentDomains = !!autoI18n.differentDomains;
38
+ validI18nUrlsForTransform = _urls.map((_e, i) => {
39
+ if (_e._abs)
40
+ return false;
41
+ const split = splitForLocales(_e._relativeLoc, localeCodes);
42
+ let localeCode = split[0];
43
+ const pathWithoutPrefix = split[1];
44
+ if (!localeCode)
45
+ localeCode = defaultLocale;
46
+ const e = _e;
47
+ e._pathWithoutPrefix = pathWithoutPrefix;
48
+ const locale = localeByCode.get(localeCode);
49
+ if (!locale)
50
+ return false;
51
+ e._locale = locale;
52
+ e._index = i;
53
+ e._key = `${e._sitemap || ""}${e._path?.pathname || "/"}${e._path?.search || ""}`;
54
+ withoutPrefixPaths[pathWithoutPrefix] = withoutPrefixPaths[pathWithoutPrefix] || [];
55
+ if (!withoutPrefixPaths[pathWithoutPrefix].some((e2) => e2._locale.code === locale.code))
56
+ withoutPrefixPaths[pathWithoutPrefix].push(e);
57
+ return e;
58
+ }).filter(Boolean);
59
+ for (const e of validI18nUrlsForTransform) {
60
+ if (!e._i18nTransform && !e.alternatives?.length) {
61
+ const alternatives = (withoutPrefixPaths[e._pathWithoutPrefix] || []).map((u) => {
62
+ const entries = [];
63
+ if (u._locale.code === defaultLocale) {
64
+ entries.push({
65
+ href: u.loc,
66
+ hreflang: "x-default"
67
+ });
68
+ }
69
+ entries.push({
70
+ href: u.loc,
71
+ hreflang: u._locale._hreflang || defaultLocale
72
+ });
73
+ return entries;
74
+ }).flat().filter(Boolean);
75
+ if (alternatives.length)
76
+ e.alternatives = alternatives;
77
+ } else if (e._i18nTransform) {
78
+ delete e._i18nTransform;
79
+ if (hasDifferentDomains) {
80
+ const defLocale = localeByCode.get(defaultLocale);
81
+ e.alternatives = [
82
+ {
83
+ ...defLocale,
84
+ code: "x-default"
85
+ },
86
+ ...autoI18n.locales.filter((l) => !!l.domain)
87
+ ].map((locale) => {
88
+ return {
89
+ hreflang: locale._hreflang,
90
+ href: joinURL(withHttps(locale.domain), e._pathWithoutPrefix)
91
+ };
92
+ });
93
+ } else {
94
+ const pageMatch = hasPages ? findPageMapping(e._pathWithoutPrefix, autoI18n.pages) : null;
95
+ const pathSearch = e._path?.search || "";
96
+ const pathWithoutPrefix = e._pathWithoutPrefix;
97
+ for (const l of autoI18n.locales) {
98
+ let loc = pathWithoutPrefix;
99
+ if (pageMatch && pageMatch.mappings[l.code] !== void 0) {
100
+ const customPath = pageMatch.mappings[l.code];
101
+ if (customPath === false)
102
+ continue;
103
+ if (typeof customPath === "string") {
104
+ loc = customPath[0] === "/" ? customPath : `/${customPath}`;
105
+ loc = applyDynamicParams(loc, pageMatch.paramSegments);
106
+ if (isPrefixStrategy || isPrefixExceptOrAndDefault && l.code !== defaultLocale)
107
+ loc = joinURL(`/${l.code}`, loc);
108
+ }
109
+ } else if (!hasDifferentDomains && !(isPrefixExceptOrAndDefault && l.code === defaultLocale)) {
110
+ loc = joinURL(`/${l.code}`, pathWithoutPrefix);
111
+ }
112
+ const _sitemap = isI18nMapped ? l._sitemap : void 0;
113
+ const alternatives = [];
114
+ for (const locale of xDefaultAndLocales) {
115
+ const code = locale.code === "x-default" ? defaultLocale : locale.code;
116
+ const isDefault = locale.code === "x-default" || locale.code === defaultLocale;
117
+ let href = pathWithoutPrefix;
118
+ if (pageMatch && pageMatch.mappings[code] !== void 0) {
119
+ const customPath = pageMatch.mappings[code];
120
+ if (customPath === false)
121
+ continue;
122
+ if (typeof customPath === "string") {
123
+ href = customPath[0] === "/" ? customPath : `/${customPath}`;
124
+ href = applyDynamicParams(href, pageMatch.paramSegments);
125
+ if (isPrefixStrategy || isPrefixExceptOrAndDefault && !isDefault)
126
+ href = joinURL("/", code, href);
127
+ }
128
+ } else if (isPrefixStrategy) {
129
+ href = joinURL("/", code, pathWithoutPrefix);
130
+ } else if (isPrefixExceptOrAndDefault && !isDefault) {
131
+ href = joinURL("/", code, pathWithoutPrefix);
132
+ }
133
+ if (!filterPath(href))
134
+ continue;
135
+ alternatives.push({
136
+ hreflang: locale._hreflang,
137
+ href
138
+ });
139
+ }
140
+ const { _index: _, ...rest } = e;
141
+ const newEntry = preNormalizeEntry({
142
+ _sitemap,
143
+ ...rest,
144
+ _key: `${_sitemap || ""}${loc || "/"}${pathSearch}`,
145
+ _locale: l,
146
+ loc,
147
+ alternatives
148
+ }, resolvers);
149
+ if (e._locale.code === newEntry._locale.code) {
150
+ _urls[e._index] = newEntry;
151
+ e._index = void 0;
152
+ } else {
153
+ _urls.push(newEntry);
154
+ }
155
+ }
156
+ }
157
+ }
158
+ if (isI18nMapped) {
159
+ e._sitemap = e._sitemap || e._locale._sitemap;
160
+ e._key = `${e._sitemap || ""}${e.loc || "/"}${e._path?.search || ""}`;
161
+ }
162
+ if (e._index)
163
+ _urls[e._index] = e;
164
+ }
165
+ }
166
+ return _urls;
167
+ }
168
+ export async function buildResolvedSitemapUrls(effectiveSitemap, matchName, isChunked, resolvers, runtimeConfig, nitro) {
169
+ const { sitemaps, autoI18n, isI18nMapped, isMultiSitemap, sortEntries } = runtimeConfig;
170
+ let sourcesInput = effectiveSitemap.includeAppSources ? [...await globalSitemapSources(), ...await childSitemapSources(effectiveSitemap)] : await childSitemapSources(effectiveSitemap);
171
+ if (nitro && resolvers.event) {
172
+ const ctx = {
173
+ event: resolvers.event,
174
+ sitemapName: matchName,
175
+ sources: sourcesInput
176
+ };
177
+ await nitro.hooks.callHook("sitemap:sources", ctx);
178
+ sourcesInput = ctx.sources;
179
+ }
180
+ const sources = await resolveSitemapSources(sourcesInput, resolvers.event);
181
+ const failedSources = sources.filter((source) => source.error && source._isFailure).map((source) => ({
182
+ url: typeof source.fetch === "string" ? source.fetch : source.fetch?.[0] || "unknown",
183
+ error: source.error || "Unknown error"
184
+ }));
185
+ const resolvedCtx = {
186
+ urls: sources.flatMap((s) => s.urls),
187
+ sitemapName: matchName,
188
+ event: resolvers.event
189
+ };
190
+ await nitro?.hooks.callHook("sitemap:input", resolvedCtx);
191
+ const enhancedUrls = resolveSitemapEntries(effectiveSitemap, resolvedCtx.urls, { autoI18n, isI18nMapped }, resolvers, useRuntimeConfig().app.baseURL);
192
+ const localeSitemapKeys = isI18nMapped && autoI18n ? autoI18n.locales.map((l) => l._sitemap) : [];
193
+ if (isMultiSitemap) {
194
+ const sitemapNames = Object.keys(sitemaps).filter((k) => k !== "index");
195
+ const warnedSitemaps = nitro?._sitemapWarnedSitemaps || /* @__PURE__ */ new Set();
196
+ for (const e of enhancedUrls) {
197
+ const hasMatchingSitemap = typeof e._sitemap === "string" && (sitemapNames.includes(e._sitemap) || isI18nMapped && sitemapNames.some((name) => resolveI18nSitemapLocaleKey(name, localeSitemapKeys) === e._sitemap));
198
+ if (typeof e._sitemap === "string" && !hasMatchingSitemap) {
199
+ if (!warnedSitemaps.has(e._sitemap)) {
200
+ warnedSitemaps.add(e._sitemap);
201
+ logger.error(`Sitemap \`${e._sitemap}\` not found in sitemap config. Available sitemaps: ${sitemapNames.join(", ")}. Entry \`${e.loc}\` will be omitted.`);
202
+ }
203
+ }
204
+ }
205
+ if (nitro) {
206
+ nitro._sitemapWarnedSitemaps = warnedSitemaps;
207
+ }
208
+ }
209
+ const filteredUrls = enhancedUrls.filter((e) => {
210
+ if (e._sitemap === false)
211
+ return false;
212
+ if (isMultiSitemap && e._sitemap && matchName) {
213
+ if (isChunked)
214
+ return e._sitemap === matchName;
215
+ if (e._sitemap === matchName)
216
+ return true;
217
+ if (isI18nMapped)
218
+ return e._sitemap === resolveI18nSitemapLocaleKey(matchName, localeSitemapKeys);
219
+ return false;
220
+ }
221
+ return true;
222
+ });
223
+ const urls = sortEntries ? sortInPlace(filteredUrls) : filteredUrls;
224
+ return { urls, failedSources };
225
+ }
226
+ export const buildResolvedSitemapUrlsCached = defineCachedFunction(
227
+ async (_event, effectiveSitemap, matchName, isChunked, resolvers, runtimeConfig, nitro) => buildResolvedSitemapUrls(effectiveSitemap, matchName, isChunked, resolvers, runtimeConfig, nitro),
228
+ {
229
+ name: "sitemap:resolved-urls",
230
+ group: "sitemap",
231
+ base: "sitemap",
232
+ maxAge: SERVER_CACHE_MAX_AGE,
233
+ getKey: (event, _effectiveSitemap, matchName, isChunked) => {
234
+ const host = safeGetHeader(event, "host") || safeGetHeader(event, "x-forwarded-host") || "";
235
+ const proto = safeGetHeader(event, "x-forwarded-proto") || "https";
236
+ return `resolved-${isChunked ? "chunked-" : ""}${matchName}-${proto}-${host}`;
237
+ },
238
+ swr: true
239
+ }
240
+ );
241
+ export async function getResolvedSitemapUrls(effectiveSitemap, matchName, isChunked, resolvers, runtimeConfig, nitro) {
242
+ const event = resolvers.event;
243
+ const shouldCache = !import.meta.dev && !import.meta.prerender && typeof runtimeConfig.cacheMaxAgeSeconds === "number" && runtimeConfig.cacheMaxAgeSeconds > 0;
244
+ if (shouldCache && event) {
245
+ return buildResolvedSitemapUrlsCached(event, effectiveSitemap, matchName, isChunked, resolvers, runtimeConfig, nitro);
246
+ }
247
+ return buildResolvedSitemapUrls(effectiveSitemap, matchName, isChunked, resolvers, runtimeConfig, nitro);
248
+ }
249
+ export async function buildSitemapUrls(sitemap, resolvers, runtimeConfig, nitro) {
250
+ const { sitemaps, autoI18n, defaultSitemapsChunkSize } = runtimeConfig;
251
+ const chunkSize = defaultSitemapsChunkSize || void 0;
252
+ const chunkInfo = parseChunkInfo(sitemap.sitemapName, sitemaps, chunkSize);
253
+ if (autoI18n?.differentDomains) {
254
+ const domain = autoI18n.locales.find((e) => e.language === sitemap.sitemapName || e.code === sitemap.sitemapName)?.domain;
255
+ if (domain) {
256
+ const _tester = resolvers.canonicalUrlResolver;
257
+ resolvers.canonicalUrlResolver = (path) => resolveSitePath(path, {
258
+ absolute: true,
259
+ withBase: false,
260
+ siteUrl: withHttps(domain),
261
+ trailingSlash: _tester("/test/").endsWith("/"),
262
+ base: "/"
263
+ });
264
+ }
265
+ }
266
+ let effectiveSitemap = sitemap;
267
+ const baseSitemapName = chunkInfo.baseSitemapName;
268
+ if (chunkInfo.isChunked && baseSitemapName !== sitemap.sitemapName && sitemaps[baseSitemapName]) {
269
+ effectiveSitemap = sitemaps[baseSitemapName];
270
+ }
271
+ const matchName = chunkInfo.isChunked ? baseSitemapName : sitemap.sitemapName;
272
+ const resolved = await getResolvedSitemapUrls(effectiveSitemap, matchName, chunkInfo.isChunked, resolvers, runtimeConfig, nitro);
273
+ const urls = sliceUrlsForChunk(resolved.urls, sitemap.sitemapName, sitemaps, chunkSize);
274
+ return { urls, failedSources: resolved.failedSources };
275
+ }
276
+ export { urlsToXml } from "./xml.js";
@@ -0,0 +1,87 @@
1
+ import { createError, getRouterParam, sendRedirect } from "h3";
2
+ import { safeAppendHeader, safeSetHeader } from "../utils/h3-compat.js";
3
+ import { useNitroApp, useRuntimeConfig } from "nitropack/runtime";
4
+ import { joinURL, withBase, withLeadingSlash, withoutLeadingSlash, withoutTrailingSlash } from "ufo";
5
+ import { useSitemapRuntimeConfig } from "../utils.js";
6
+ import { buildSitemapIndex, urlsToIndexXml } from "./builder/sitemap-index.js";
7
+ import { createSitemap, useNitroUrlResolvers } from "./nitro.js";
8
+ import { getSitemapConfig, parseChunkInfo } from "./utils/chunk.js";
9
+ export async function sitemapXmlEventHandler(e) {
10
+ const runtimeConfig = useSitemapRuntimeConfig();
11
+ const { sitemaps } = runtimeConfig;
12
+ if ("index" in sitemaps)
13
+ return sendRedirect(e, withBase("/sitemap_index.xml", useRuntimeConfig().app.baseURL), import.meta.dev ? 302 : 301);
14
+ return createSitemap(e, Object.values(sitemaps)[0], runtimeConfig);
15
+ }
16
+ export async function sitemapIndexXmlEventHandler(e) {
17
+ const runtimeConfig = useSitemapRuntimeConfig();
18
+ const nitro = useNitroApp();
19
+ const resolvers = useNitroUrlResolvers(e);
20
+ const { entries: sitemaps, failedSources } = await buildSitemapIndex(resolvers, runtimeConfig, nitro);
21
+ if (import.meta.prerender) {
22
+ safeAppendHeader(
23
+ e,
24
+ "x-nitro-prerender",
25
+ sitemaps.filter((entry) => !!entry._sitemapName).map((entry) => encodeURIComponent(joinURL(runtimeConfig.sitemapsPathPrefix || "", `/${entry._sitemapName}.xml`))).join(", ")
26
+ );
27
+ }
28
+ const indexResolvedCtx = { sitemaps, event: e };
29
+ await nitro.hooks.callHook("sitemap:index-resolved", indexResolvedCtx);
30
+ const errorInfo = failedSources.length > 0 ? { messages: failedSources.map((f) => f.error), urls: failedSources.map((f) => f.url) } : void 0;
31
+ const output = urlsToIndexXml(indexResolvedCtx.sitemaps, resolvers, runtimeConfig, errorInfo);
32
+ const ctx = { sitemap: output, sitemapName: "sitemap", event: e };
33
+ await nitro.hooks.callHook("sitemap:output", ctx);
34
+ safeSetHeader(e, "Content-Type", "text/xml; charset=UTF-8");
35
+ if (runtimeConfig.cacheMaxAgeSeconds) {
36
+ safeSetHeader(e, "Cache-Control", `public, max-age=${runtimeConfig.cacheMaxAgeSeconds}, s-maxage=${runtimeConfig.cacheMaxAgeSeconds}, stale-while-revalidate=3600`);
37
+ const now = /* @__PURE__ */ new Date();
38
+ safeSetHeader(e, "X-Sitemap-Generated", now.toISOString());
39
+ safeSetHeader(e, "X-Sitemap-Cache-Duration", `${runtimeConfig.cacheMaxAgeSeconds}s`);
40
+ const expiryTime = new Date(now.getTime() + runtimeConfig.cacheMaxAgeSeconds * 1e3);
41
+ safeSetHeader(e, "X-Sitemap-Cache-Expires", expiryTime.toISOString());
42
+ const remainingSeconds = Math.floor((expiryTime.getTime() - now.getTime()) / 1e3);
43
+ safeSetHeader(e, "X-Sitemap-Cache-Remaining", `${remainingSeconds}s`);
44
+ } else {
45
+ safeSetHeader(e, "Cache-Control", `no-cache, no-store`);
46
+ }
47
+ return ctx.sitemap;
48
+ }
49
+ export async function sitemapChildXmlEventHandler(e) {
50
+ if (!e.path.endsWith(".xml"))
51
+ return;
52
+ const runtimeConfig = useSitemapRuntimeConfig(e);
53
+ const { sitemaps } = runtimeConfig;
54
+ let sitemapName = getRouterParam(e, "sitemap");
55
+ if (!sitemapName) {
56
+ const path = e.path;
57
+ const match = path.match(/(?:\/__sitemap__\/)?(.+)\.xml$/);
58
+ if (match)
59
+ sitemapName = match[1];
60
+ }
61
+ if (!sitemapName)
62
+ throw createError({ statusCode: 400, message: "Invalid sitemap request" });
63
+ sitemapName = sitemapName.replace(/\.xml$/, "");
64
+ sitemapName = withLeadingSlash(sitemapName);
65
+ if (sitemapName.startsWith("/__sitemap__/"))
66
+ sitemapName = sitemapName.replace("/__sitemap__/", "/");
67
+ if (runtimeConfig.sitemapsPathPrefix) {
68
+ const prefix = withLeadingSlash(runtimeConfig.sitemapsPathPrefix);
69
+ if (sitemapName.startsWith(prefix))
70
+ sitemapName = sitemapName.replace(prefix, "/");
71
+ }
72
+ sitemapName = withoutLeadingSlash(withoutTrailingSlash(sitemapName));
73
+ const chunkInfo = parseChunkInfo(sitemapName, sitemaps, runtimeConfig.defaultSitemapsChunkSize);
74
+ const isAutoChunked = typeof sitemaps.chunks !== "undefined" && !Number.isNaN(Number(sitemapName));
75
+ const sitemapExists = sitemapName in sitemaps || chunkInfo.baseSitemapName in sitemaps || isAutoChunked;
76
+ if (!sitemapExists)
77
+ throw createError({ statusCode: 404, message: `Sitemap "${sitemapName}" not found.` });
78
+ if (chunkInfo.isChunked && chunkInfo.chunkIndex !== void 0) {
79
+ const baseSitemap = sitemaps[chunkInfo.baseSitemapName];
80
+ if (baseSitemap && !baseSitemap.chunks && !baseSitemap._isChunking)
81
+ throw createError({ statusCode: 404, message: `Sitemap "${chunkInfo.baseSitemapName}" does not support chunking.` });
82
+ if (baseSitemap?._chunkCount !== void 0 && chunkInfo.chunkIndex >= baseSitemap._chunkCount)
83
+ throw createError({ statusCode: 404, message: `Chunk ${chunkInfo.chunkIndex} does not exist for sitemap "${chunkInfo.baseSitemapName}".` });
84
+ }
85
+ const sitemapConfig = getSitemapConfig(sitemapName, sitemaps, runtimeConfig.defaultSitemapsChunkSize || void 0);
86
+ return createSitemap(e, sitemapConfig, runtimeConfig);
87
+ }
@@ -0,0 +1,157 @@
1
+ import { defu } from "defu";
2
+ import { createError } from "h3";
3
+ import { safeGetHeader, safeGetQuery, safeSetHeader } from "../utils/h3-compat.js";
4
+ import { defineCachedFunction, useNitroApp } from "nitropack/runtime";
5
+ import { fixSlashes } from "nuxt-site-config/urls";
6
+ import { getPathRobotConfig } from "#internal/nuxt-robots/getPathRobotConfig";
7
+ import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
8
+ import { createSitePathResolver } from "#site-config/server/composables/utils";
9
+ import staticConfig from "#sitemap-virtual/static-config.mjs";
10
+ import { logger, mergeOnKey, splitForLocales } from "../../utils-pure.js";
11
+ import { createNitroRouteRuleMatcher } from "../kit.js";
12
+ import { buildSitemapUrls, urlsToXml } from "./builder/sitemap.js";
13
+ import { normaliseEntry, preNormalizeEntry } from "./urlset/normalise.js";
14
+ import { sortInPlace } from "./urlset/sort.js";
15
+ const SERVER_CACHE_MAX_AGE = staticConfig.cacheMaxAgeSeconds || 60 * 10;
16
+
17
+ export function useNitroUrlResolvers(e) {
18
+ const canonicalQuery = safeGetQuery(e).canonical;
19
+ const isShowingCanonical = typeof canonicalQuery !== "undefined" && canonicalQuery !== "false";
20
+ const siteConfig = getSiteConfig(e);
21
+ return {
22
+ event: e,
23
+ fixSlashes: (path) => fixSlashes(siteConfig.trailingSlash, path),
24
+ // we need these as they depend on the nitro event
25
+ canonicalUrlResolver: createSitePathResolver(e, {
26
+ canonical: isShowingCanonical || !import.meta.dev,
27
+ absolute: true,
28
+ withBase: true
29
+ }),
30
+ relativeBaseUrlResolver: createSitePathResolver(e, { absolute: false, withBase: true })
31
+ };
32
+ }
33
+ async function buildSitemapXml(event, definition, resolvers, runtimeConfig) {
34
+ const { sitemapName } = definition;
35
+ const nitro = useNitroApp();
36
+ if (import.meta.prerender) {
37
+ const config = getSiteConfig(event);
38
+ if (!config.url && !nitro._sitemapWarned) {
39
+ nitro._sitemapWarned = true;
40
+ logger.error("Sitemap Site URL missing!");
41
+ logger.info("To fix this please add `{ site: { url: 'site.com' } }` to your Nuxt config or a `NUXT_PUBLIC_SITE_URL=site.com` to your .env. Learn more at https://nuxtseo.com/site-config/getting-started/how-it-works");
42
+ throw createError({
43
+ statusMessage: "You must provide a site URL to prerender a sitemap.",
44
+ statusCode: 500
45
+ });
46
+ }
47
+ }
48
+ const { urls: sitemapUrls, failedSources } = await buildSitemapUrls(definition, resolvers, runtimeConfig, nitro);
49
+ if (import.meta.prerender && failedSources.length) {
50
+ throw createError({
51
+ statusCode: 500,
52
+ message: `Sitemap generation failed due to ${failedSources.length} failed sources: ${failedSources.map((s) => `"${s.url}" (${s.error})`).join(", ")}`
53
+ });
54
+ }
55
+ const routeRuleMatcher = createNitroRouteRuleMatcher();
56
+ const { autoI18n } = runtimeConfig;
57
+ let validCount = 0;
58
+ for (let i = 0; i < sitemapUrls.length; i++) {
59
+ const u = sitemapUrls[i];
60
+ const path = u._path?.pathname || u.loc;
61
+ if (!getPathRobotConfig(event, { path, skipSiteIndexable: true }).indexable)
62
+ continue;
63
+ let routeRules = routeRuleMatcher(path);
64
+ if (autoI18n?.locales && autoI18n?.strategy !== "no_prefix") {
65
+ const match = splitForLocales(path, autoI18n.locales.map((l) => l.code));
66
+ const pathWithoutPrefix = match[1];
67
+ if (pathWithoutPrefix && pathWithoutPrefix !== path)
68
+ routeRules = defu(routeRules, routeRuleMatcher(pathWithoutPrefix));
69
+ }
70
+ if (routeRules.sitemap === false)
71
+ continue;
72
+ if (typeof routeRules.robots !== "undefined" && !routeRules.robots)
73
+ continue;
74
+ const hasRobotsDisabled = Object.entries(routeRules.headers || {}).some(([name, value]) => name.toLowerCase() === "x-robots-tag" && value.toLowerCase().includes("noindex"));
75
+ if (routeRules.redirect || hasRobotsDisabled)
76
+ continue;
77
+ sitemapUrls[validCount++] = routeRules.sitemap ? defu(u, routeRules.sitemap) : u;
78
+ }
79
+ sitemapUrls.length = validCount;
80
+ if (import.meta.dev && validCount === 0 && sitemapUrls.length > 0) {
81
+ 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.`);
82
+ }
83
+ const locSize = sitemapUrls.length;
84
+ const resolvedCtx = {
85
+ urls: sitemapUrls,
86
+ sitemapName,
87
+ event
88
+ };
89
+ await nitro.hooks.callHook("sitemap:resolved", resolvedCtx);
90
+ if (resolvedCtx.urls.length !== locSize) {
91
+ resolvedCtx.urls = resolvedCtx.urls.map((e) => preNormalizeEntry(e, resolvers));
92
+ }
93
+ const maybeSort = (urls2) => runtimeConfig.sortEntries ? sortInPlace(urls2) : urls2;
94
+ const defaults = definition.defaults || {};
95
+ const normalizedPreDedupe = resolvedCtx.urls.map((e) => normaliseEntry(e, defaults, resolvers));
96
+ const urls = maybeSort(mergeOnKey(normalizedPreDedupe, "_key").map((e) => normaliseEntry(e, defaults, resolvers)));
97
+ if (definition._isChunking && definition.sitemapName.includes("-")) {
98
+ const parts = definition.sitemapName.split("-");
99
+ const lastPart = parts.pop();
100
+ if (!Number.isNaN(Number(lastPart))) {
101
+ const chunkIndex = Number(lastPart);
102
+ const baseSitemapName = parts.join("-");
103
+ if (urls.length === 0 && chunkIndex > 0) {
104
+ throw createError({
105
+ statusCode: 404,
106
+ message: `Sitemap chunk ${chunkIndex} for "${baseSitemapName}" does not exist.`
107
+ });
108
+ }
109
+ }
110
+ }
111
+ const errorInfo = failedSources.length > 0 ? {
112
+ messages: failedSources.map((f) => f.error),
113
+ urls: failedSources.map((f) => f.url)
114
+ } : void 0;
115
+ const sitemap = urlsToXml(urls, resolvers, runtimeConfig, errorInfo);
116
+ const ctx = { sitemap, sitemapName, event };
117
+ await nitro.hooks.callHook("sitemap:output", ctx);
118
+ return ctx.sitemap;
119
+ }
120
+ const buildSitemapXmlCached = defineCachedFunction(
121
+ buildSitemapXml,
122
+ {
123
+ name: "sitemap:xml",
124
+ group: "sitemap",
125
+ maxAge: SERVER_CACHE_MAX_AGE,
126
+ base: "sitemap",
127
+ // Use the sitemap storage
128
+ getKey: (event, definition) => {
129
+ const host = safeGetHeader(event, "host") || safeGetHeader(event, "x-forwarded-host") || "";
130
+ const proto = safeGetHeader(event, "x-forwarded-proto") || "https";
131
+ const sitemapName = definition.sitemapName || "default";
132
+ return `${sitemapName}-${proto}-${host}`;
133
+ },
134
+ swr: true
135
+ // Enable stale-while-revalidate
136
+ }
137
+ );
138
+ export async function createSitemap(event, definition, runtimeConfig) {
139
+ const resolvers = useNitroUrlResolvers(event);
140
+ const shouldCache = !import.meta.dev && !import.meta.prerender && typeof runtimeConfig.cacheMaxAgeSeconds === "number" && runtimeConfig.cacheMaxAgeSeconds > 0;
141
+ const xml = shouldCache ? await buildSitemapXmlCached(event, definition, resolvers, runtimeConfig) : await buildSitemapXml(event, definition, resolvers, runtimeConfig);
142
+ safeSetHeader(event, "Content-Type", "text/xml; charset=UTF-8");
143
+ if (runtimeConfig.cacheMaxAgeSeconds) {
144
+ safeSetHeader(event, "Cache-Control", `public, max-age=${runtimeConfig.cacheMaxAgeSeconds}, s-maxage=${runtimeConfig.cacheMaxAgeSeconds}, stale-while-revalidate=3600`);
145
+ const now = /* @__PURE__ */ new Date();
146
+ safeSetHeader(event, "X-Sitemap-Generated", now.toISOString());
147
+ safeSetHeader(event, "X-Sitemap-Cache-Duration", `${runtimeConfig.cacheMaxAgeSeconds}s`);
148
+ const expiryTime = new Date(now.getTime() + runtimeConfig.cacheMaxAgeSeconds * 1e3);
149
+ safeSetHeader(event, "X-Sitemap-Cache-Expires", expiryTime.toISOString());
150
+ const remainingSeconds = Math.floor((expiryTime.getTime() - now.getTime()) / 1e3);
151
+ safeSetHeader(event, "X-Sitemap-Cache-Remaining", `${remainingSeconds}s`);
152
+ } else {
153
+ safeSetHeader(event, "Cache-Control", `no-cache, no-store`);
154
+ }
155
+ event.context._isSitemap = true;
156
+ return xml;
157
+ }