@nuxtjs/sitemap 7.3.0 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/client/200.html +8 -8
  2. package/dist/client/404.html +8 -8
  3. package/dist/client/_nuxt/ChEizYIG.js +172 -0
  4. package/dist/client/_nuxt/{lL_X76lO.js → SQMF8ibg.js} +1 -1
  5. package/dist/client/_nuxt/builds/latest.json +1 -1
  6. package/dist/client/_nuxt/builds/meta/048e1f4c-a575-4a94-bbab-d777afe4c585.json +1 -0
  7. package/dist/client/_nuxt/{DJVkgDQ2.js → cqJZcoo0.js} +1 -1
  8. package/dist/client/_nuxt/{entry.CgW0_noo.css → entry.BzbtAPc0.css} +1 -1
  9. package/dist/client/_nuxt/error-404.DljSaiyF.css +1 -0
  10. package/dist/client/_nuxt/error-500.DbX9fggi.css +1 -0
  11. package/dist/client/index.html +8 -8
  12. package/dist/module.cjs +30 -20
  13. package/dist/module.json +1 -1
  14. package/dist/module.mjs +30 -20
  15. package/dist/runtime/server/content-compat.d.ts +1 -0
  16. package/dist/runtime/server/content-compat.js +2 -0
  17. package/dist/runtime/server/routes/__sitemap__/nuxt-content-urls-v3.d.ts +1 -1
  18. package/dist/runtime/server/routes/__sitemap__/nuxt-content-urls-v3.js +4 -2
  19. package/dist/runtime/server/routes/sitemap.xsl.js +37 -13
  20. package/dist/runtime/server/routes/sitemap_index.xml.js +6 -2
  21. package/dist/runtime/server/sitemap/builder/sitemap-index.d.ts +11 -2
  22. package/dist/runtime/server/sitemap/builder/sitemap-index.js +23 -5
  23. package/dist/runtime/server/sitemap/builder/sitemap.d.ts +7 -1
  24. package/dist/runtime/server/sitemap/builder/sitemap.js +10 -5
  25. package/dist/runtime/server/sitemap/builder/xml.d.ts +4 -1
  26. package/dist/runtime/server/sitemap/builder/xml.js +13 -4
  27. package/dist/runtime/server/sitemap/nitro.js +7 -3
  28. package/dist/runtime/server/sitemap/urlset/normalise.js +4 -3
  29. package/dist/runtime/server/sitemap/urlset/sources.js +62 -17
  30. package/dist/runtime/server/utils.d.ts +1 -0
  31. package/dist/runtime/server/utils.js +3 -0
  32. package/dist/runtime/types.d.ts +1 -0
  33. package/package.json +16 -15
  34. package/dist/client/_nuxt/SmY-NWqO.js +0 -172
  35. package/dist/client/_nuxt/builds/meta/e48bfd5b-6605-4bbc-a466-32a664787616.json +0 -1
  36. package/dist/client/_nuxt/error-404.CtcyoHAN.css +0 -1
  37. package/dist/client/_nuxt/error-500.BIlfyoPk.css +0 -1
@@ -1,6 +1,6 @@
1
- import { defineEventHandler, getHeader, setHeader } from "h3";
1
+ import { defineEventHandler, getHeader, setHeader, getQuery as h3GetQuery } from "h3";
2
2
  import { getQuery, parseURL, withQuery } from "ufo";
3
- import { useSitemapRuntimeConfig } from "../utils.js";
3
+ import { useSitemapRuntimeConfig, xmlEscape } from "../utils.js";
4
4
  import { useSiteConfig } from "#site-config/server/composables/useSiteConfig";
5
5
  import { createSitePathResolver } from "#site-config/server/composables/utils";
6
6
  export default defineEventHandler(async (e) => {
@@ -23,17 +23,37 @@ export default defineEventHandler(async (e) => {
23
23
  const isShowingCanonical = typeof canonicalQuery !== "undefined" && canonicalQuery !== "false";
24
24
  const conditionalTips = [
25
25
  'You are looking at a <a href="https://developer.mozilla.org/en-US/docs/Web/XSLT/Transforming_XML_with_XSLT/An_Overview" style="color: #398465" target="_blank">XML stylesheet</a>. Read the <a href="https://nuxtseo.com/sitemap/guides/customising-ui" style="color: #398465" target="_blank">docs</a> to learn how to customize it. View the page source to see the raw XML.',
26
- `URLs missing? Check Nuxt Devtools Sitemap tab (or the <a href="${withQuery("/__sitemap__/debug.json", { sitemap: sitemapName })}" style="color: #398465" target="_blank">debug endpoint</a>).`
26
+ `URLs missing? Check Nuxt Devtools Sitemap tab (or the <a href="${xmlEscape(withQuery("/__sitemap__/debug.json", { sitemap: sitemapName }))}" style="color: #398465" target="_blank">debug endpoint</a>).`
27
27
  ];
28
+ const fetchErrors = [];
29
+ const xslQuery = h3GetQuery(e);
30
+ if (xslQuery.error_messages) {
31
+ const errorMessages = xslQuery.error_messages;
32
+ const errorUrls = xslQuery.error_urls;
33
+ if (errorMessages) {
34
+ const messages = Array.isArray(errorMessages) ? errorMessages : [errorMessages];
35
+ const urls = Array.isArray(errorUrls) ? errorUrls : errorUrls ? [errorUrls] : [];
36
+ messages.forEach((msg, i) => {
37
+ const errorParts = [xmlEscape(msg)];
38
+ if (urls[i]) {
39
+ errorParts.push(xmlEscape(urls[i]));
40
+ }
41
+ fetchErrors.push(`<strong style="color: #dc2626;">Error ${i + 1}:</strong> ${errorParts.join(" - ")}`);
42
+ });
43
+ }
44
+ }
28
45
  if (!isShowingCanonical) {
29
46
  const canonicalPreviewUrl = withQuery(referrer, { canonical: "" });
30
- conditionalTips.push(`Your canonical site URL is <strong>${siteUrl}</strong>.`);
31
- conditionalTips.push(`You can preview your canonical sitemap by visiting <a href="${canonicalPreviewUrl}" style="color: #398465; white-space: nowrap;">${fixPath(canonicalPreviewUrl)}?canonical</a>`);
47
+ conditionalTips.push(`Your canonical site URL is <strong>${xmlEscape(siteUrl)}</strong>.`);
48
+ conditionalTips.push(`You can preview your canonical sitemap by visiting <a href="${xmlEscape(canonicalPreviewUrl)}" style="color: #398465; white-space: nowrap;">${xmlEscape(fixPath(canonicalPreviewUrl))}?canonical</a>`);
32
49
  } else {
33
- conditionalTips.push(`You are viewing the canonical sitemap. You can switch to using the request origin: <a href="${fixPath(referrer)}" style="color: #398465; white-space: nowrap ">${fixPath(referrer)}</a>`);
50
+ conditionalTips.push(`You are viewing the canonical sitemap. You can switch to using the request origin: <a href="${xmlEscape(fixPath(referrer))}" style="color: #398465; white-space: nowrap ">${xmlEscape(fixPath(referrer))}</a>`);
34
51
  }
35
- const tips = conditionalTips.map((t) => `<li><p>${t}</p></li>`).join("\n");
36
- const showTips = import.meta.dev && xslTips !== false;
52
+ const hasRuntimeErrors = fetchErrors.length > 0;
53
+ const showDevTips = import.meta.dev && xslTips !== false;
54
+ const showSidebar = showDevTips || hasRuntimeErrors;
55
+ const devTips = showDevTips ? conditionalTips.map((t) => `<li><p>${t}</p></li>`).join("\n") : "";
56
+ const runtimeErrors = hasRuntimeErrors ? fetchErrors.map((t) => `<li><p>${t}</p></li>`).join("\n") : "";
37
57
  let columns = [...xslColumns];
38
58
  if (!columns.length) {
39
59
  columns = [
@@ -97,12 +117,12 @@ export default defineEventHandler(async (e) => {
97
117
  }
98
118
 
99
119
  .expl a {
100
- color: #398465
120
+ color: #398465;
101
121
  font-weight: 600;
102
122
  }
103
123
 
104
124
  .expl a:visited {
105
- color: #398465
125
+ color: #398465;
106
126
  }
107
127
 
108
128
  a {
@@ -153,8 +173,8 @@ export default defineEventHandler(async (e) => {
153
173
  <div>
154
174
  <div id="content">
155
175
  <h1 class="text-2xl mb-3">XML Sitemap</h1>
156
- <h2>${title}</h2>
157
- ${isNotIndexButHasIndex ? `<p style="font-size: 12px; margin-bottom: 1rem;"><a href="${fixPath("/sitemap_index.xml")}">${fixPath("/sitemap_index.xml")}</a></p>` : ""}
176
+ <h2>${xmlEscape(title)}</h2>
177
+ ${isNotIndexButHasIndex ? `<p style="font-size: 12px; margin-bottom: 1rem;"><a href="${xmlEscape(fixPath("/sitemap_index.xml"))}">${xmlEscape(fixPath("/sitemap_index.xml"))}</a></p>` : ""}
158
178
  <xsl:if test="count(sitemap:sitemapindex/sitemap:sitemap) &gt; 0">
159
179
  <p class="expl" style="margin-bottom: 1rem;">
160
180
  This XML Sitemap Index file contains
@@ -221,7 +241,11 @@ export default defineEventHandler(async (e) => {
221
241
  </xsl:if>
222
242
  </div>
223
243
  </div>
224
- ${showTips ? `<div class="w-30 top-2 shadow rounded p-5 right-2" style="margin: 0 auto;"><p><strong>Sitemap Tips (development only)</strong></p><ul style="margin: 1rem; padding: 0;">${tips}</ul><p style="margin-top: 1rem;">${creditName}</p></div>` : ""}
244
+ ${showSidebar ? `<div class="w-30 top-2 shadow rounded p-5 right-2" style="margin: 0 auto;">
245
+ ${showDevTips ? `<div><p><strong>Development Tips</strong></p><ul style="margin: 1rem 0; padding: 0;">${devTips}</ul></div>` : ""}
246
+ ${hasRuntimeErrors ? `<div${showDevTips ? ' style="margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid #e5e7eb;"' : ""}><p><strong style="color: #dc2626;">Runtime Errors</strong></p><ul style="margin: 1rem 0; padding: 0;">${runtimeErrors}</ul></div>` : ""}
247
+ ${showDevTips ? `<p style="margin-top: 1rem;">${creditName}</p>` : ""}
248
+ </div>` : ""}
225
249
  </div>
226
250
  </body>
227
251
  </html>
@@ -8,7 +8,7 @@ export default defineEventHandler(async (e) => {
8
8
  const runtimeConfig = useSitemapRuntimeConfig();
9
9
  const nitro = useNitroApp();
10
10
  const resolvers = useNitroUrlResolvers(e);
11
- const sitemaps = await buildSitemapIndex(resolvers, runtimeConfig, nitro);
11
+ const { entries: sitemaps, failedSources } = await buildSitemapIndex(resolvers, runtimeConfig, nitro);
12
12
  if (import.meta.prerender) {
13
13
  appendHeader(
14
14
  e,
@@ -18,7 +18,11 @@ export default defineEventHandler(async (e) => {
18
18
  }
19
19
  const indexResolvedCtx = { sitemaps, event: e };
20
20
  await nitro.hooks.callHook("sitemap:index-resolved", indexResolvedCtx);
21
- const output = urlsToIndexXml(indexResolvedCtx.sitemaps, resolvers, runtimeConfig);
21
+ const errorInfo = failedSources.length > 0 ? {
22
+ messages: failedSources.map((f) => f.error),
23
+ urls: failedSources.map((f) => f.url)
24
+ } : void 0;
25
+ const output = urlsToIndexXml(indexResolvedCtx.sitemaps, resolvers, runtimeConfig, errorInfo);
22
26
  const ctx = { sitemap: output, sitemapName: "sitemap", event: e };
23
27
  await nitro.hooks.callHook("sitemap:output", ctx);
24
28
  setHeader(e, "Content-Type", "text/xml; charset=UTF-8");
@@ -1,4 +1,13 @@
1
1
  import type { NitroApp } from 'nitropack/types';
2
2
  import type { ModuleRuntimeConfig, NitroUrlResolvers, SitemapIndexEntry } from '../../../types.js';
3
- export declare function urlsToIndexXml(sitemaps: SitemapIndexEntry[], resolvers: NitroUrlResolvers, { version, xsl, credits, minify }: Pick<ModuleRuntimeConfig, 'version' | 'xsl' | 'credits' | 'minify'>): string;
4
- export declare function buildSitemapIndex(resolvers: NitroUrlResolvers, runtimeConfig: ModuleRuntimeConfig, nitro?: NitroApp): Promise<SitemapIndexEntry[]>;
3
+ export declare function urlsToIndexXml(sitemaps: SitemapIndexEntry[], resolvers: NitroUrlResolvers, { version, xsl, credits, minify }: Pick<ModuleRuntimeConfig, 'version' | 'xsl' | 'credits' | 'minify'>, errorInfo?: {
4
+ messages: string[];
5
+ urls: string[];
6
+ }): string;
7
+ export declare function buildSitemapIndex(resolvers: NitroUrlResolvers, runtimeConfig: ModuleRuntimeConfig, nitro?: NitroApp): Promise<{
8
+ entries: SitemapIndexEntry[];
9
+ failedSources: Array<{
10
+ url: string;
11
+ error: string;
12
+ }>;
13
+ }>;
@@ -1,5 +1,5 @@
1
1
  import { defu } from "defu";
2
- import { joinURL } from "ufo";
2
+ import { joinURL, withQuery } from "ufo";
3
3
  import { defineCachedFunction } from "nitropack/runtime";
4
4
  import { getHeader } from "h3";
5
5
  import { normaliseDate } from "../urlset/normalise.js";
@@ -45,6 +45,7 @@ async function buildSitemapIndexInternal(resolvers, runtimeConfig, nitro) {
45
45
  return sortEntries ? sortInPlace(urls) : urls;
46
46
  }
47
47
  const chunks = {};
48
+ const allFailedSources = [];
48
49
  for (const sitemapName in sitemaps) {
49
50
  if (sitemapName === "index" || sitemapName === "chunks") continue;
50
51
  const sitemapConfig = sitemaps[sitemapName];
@@ -68,6 +69,11 @@ async function buildSitemapIndexInternal(resolvers, runtimeConfig, nitro) {
68
69
  sourcesInput = ctx.sources;
69
70
  }
70
71
  const sources = await resolveSitemapSources(sourcesInput, resolvers.event);
72
+ const failedSources = sources.filter((source) => source.error && source._isFailure).map((source) => ({
73
+ url: typeof source.fetch === "string" ? source.fetch : source.fetch?.[0] || "unknown",
74
+ error: source.error || "Unknown error"
75
+ }));
76
+ allFailedSources.push(...failedSources);
71
77
  const resolvedCtx = {
72
78
  urls: sources.flatMap((s) => s.urls),
73
79
  sitemapName: sitemap.sitemapName,
@@ -113,6 +119,11 @@ async function buildSitemapIndexInternal(resolvers, runtimeConfig, nitro) {
113
119
  sourcesInput = ctx.sources;
114
120
  }
115
121
  const sources = await resolveSitemapSources(sourcesInput, resolvers.event);
122
+ const failedSources = sources.filter((source) => source.error && source._isFailure).map((source) => ({
123
+ url: typeof source.fetch === "string" ? source.fetch : source.fetch?.[0] || "unknown",
124
+ error: source.error || "Unknown error"
125
+ }));
126
+ allFailedSources.push(...failedSources);
116
127
  const resolvedCtx = {
117
128
  urls: sources.flatMap((s) => s.urls),
118
129
  sitemapName: sitemapConfig.sitemapName,
@@ -144,9 +155,9 @@ async function buildSitemapIndexInternal(resolvers, runtimeConfig, nitro) {
144
155
  return typeof entry === "string" ? { sitemap: entry } : entry;
145
156
  }));
146
157
  }
147
- return entries;
158
+ return { entries, failedSources: allFailedSources };
148
159
  }
149
- export function urlsToIndexXml(sitemaps, resolvers, { version, xsl, credits, minify }) {
160
+ export function urlsToIndexXml(sitemaps, resolvers, { version, xsl, credits, minify }, errorInfo) {
150
161
  const sitemapXml = sitemaps.map((e) => [
151
162
  " <sitemap>",
152
163
  ` <loc>${escapeValueForXml(e.sitemap)}</loc>`,
@@ -158,7 +169,14 @@ export function urlsToIndexXml(sitemaps, resolvers, { version, xsl, credits, min
158
169
  '<?xml version="1.0" encoding="UTF-8"?>'
159
170
  ];
160
171
  if (xsl) {
161
- const relativeBaseUrl = resolvers.relativeBaseUrlResolver?.(xsl) ?? xsl;
172
+ let relativeBaseUrl = resolvers.relativeBaseUrlResolver?.(xsl) ?? xsl;
173
+ if (errorInfo && errorInfo.messages.length > 0) {
174
+ relativeBaseUrl = withQuery(relativeBaseUrl, {
175
+ errors: "true",
176
+ error_messages: errorInfo.messages,
177
+ error_urls: errorInfo.urls
178
+ });
179
+ }
162
180
  xmlParts.push(`<?xml-stylesheet type="text/xsl" href="${escapeValueForXml(relativeBaseUrl)}"?>`);
163
181
  }
164
182
  xmlParts.push(
@@ -172,7 +190,7 @@ export function urlsToIndexXml(sitemaps, resolvers, { version, xsl, credits, min
172
190
  return minify ? xmlParts.join("").replace(/(?<!<[^>]*)\s(?![^<]*>)/g, "") : xmlParts.join("\n");
173
191
  }
174
192
  export async function buildSitemapIndex(resolvers, runtimeConfig, nitro) {
175
- if (!import.meta.dev && !!runtimeConfig.cacheMaxAgeSeconds && runtimeConfig.cacheMaxAgeSeconds > 0 && resolvers.event) {
193
+ if (!import.meta.dev && typeof runtimeConfig.cacheMaxAgeSeconds === "number" && runtimeConfig.cacheMaxAgeSeconds > 0 && resolvers.event) {
176
194
  return buildSitemapIndexCached(resolvers.event, resolvers, runtimeConfig, nitro);
177
195
  }
178
196
  return buildSitemapIndexInternal(resolvers, runtimeConfig, nitro);
@@ -6,5 +6,11 @@ export interface NormalizedI18n extends ResolvedSitemapUrl {
6
6
  _index?: number;
7
7
  }
8
8
  export declare function resolveSitemapEntries(sitemap: SitemapDefinition, urls: SitemapUrlInput[], runtimeConfig: Pick<ModuleRuntimeConfig, 'autoI18n' | 'isI18nMapped'>, resolvers?: NitroUrlResolvers): ResolvedSitemapUrl[];
9
- export declare function buildSitemapUrls(sitemap: SitemapDefinition, resolvers: NitroUrlResolvers, runtimeConfig: ModuleRuntimeConfig, nitro?: NitroApp): Promise<ResolvedSitemapUrl[]>;
9
+ export declare function buildSitemapUrls(sitemap: SitemapDefinition, resolvers: NitroUrlResolvers, runtimeConfig: ModuleRuntimeConfig, nitro?: NitroApp): Promise<{
10
+ urls: ResolvedSitemapUrl[];
11
+ failedSources: Array<{
12
+ url: string;
13
+ error: string;
14
+ }>;
15
+ }>;
10
16
  export { urlsToXml } from './xml.js';
@@ -182,11 +182,11 @@ export async function buildSitemapUrls(sitemap, resolvers, runtimeConfig, nitro)
182
182
  defaultSitemapsChunkSize
183
183
  } = runtimeConfig;
184
184
  const chunkInfo = parseChunkInfo(sitemap.sitemapName, sitemaps, defaultSitemapsChunkSize);
185
- function maybeSort(urls) {
186
- return sortEntries ? sortInPlace(urls) : urls;
185
+ function maybeSort(urls2) {
186
+ return sortEntries ? sortInPlace(urls2) : urls2;
187
187
  }
188
- function maybeSlice(urls) {
189
- return sliceUrlsForChunk(urls, sitemap.sitemapName, sitemaps, defaultSitemapsChunkSize);
188
+ function maybeSlice(urls2) {
189
+ return sliceUrlsForChunk(urls2, sitemap.sitemapName, sitemaps, defaultSitemapsChunkSize);
190
190
  }
191
191
  if (autoI18n?.differentDomains) {
192
192
  const domain = autoI18n.locales.find((e) => [e.language, e.code].includes(sitemap.sitemapName))?.domain;
@@ -218,6 +218,10 @@ export async function buildSitemapUrls(sitemap, resolvers, runtimeConfig, nitro)
218
218
  sourcesInput = ctx.sources;
219
219
  }
220
220
  const sources = await resolveSitemapSources(sourcesInput, resolvers.event);
221
+ const failedSources = sources.filter((source) => source.error && source._isFailure).map((source) => ({
222
+ url: typeof source.fetch === "string" ? source.fetch : source.fetch?.[0] || "unknown",
223
+ error: source.error || "Unknown error"
224
+ }));
221
225
  const resolvedCtx = {
222
226
  urls: sources.flatMap((s) => s.urls),
223
227
  sitemapName: sitemap.sitemapName,
@@ -231,6 +235,7 @@ export async function buildSitemapUrls(sitemap, resolvers, runtimeConfig, nitro)
231
235
  return true;
232
236
  });
233
237
  const sortedUrls = maybeSort(filteredUrls);
234
- return maybeSlice(sortedUrls);
238
+ const urls = maybeSlice(sortedUrls);
239
+ return { urls, failedSources };
235
240
  }
236
241
  export { urlsToXml } from "./xml.js";
@@ -1,3 +1,6 @@
1
1
  import type { ModuleRuntimeConfig, NitroUrlResolvers, ResolvedSitemapUrl } from '../../../types.js';
2
2
  export declare function escapeValueForXml(value: boolean | string | number): string;
3
- export declare function urlsToXml(urls: ResolvedSitemapUrl[], resolvers: NitroUrlResolvers, { version, xsl, credits, minify }: Pick<ModuleRuntimeConfig, 'version' | 'xsl' | 'credits' | 'minify'>): string;
3
+ export declare function urlsToXml(urls: ResolvedSitemapUrl[], resolvers: NitroUrlResolvers, { version, xsl, credits, minify }: Pick<ModuleRuntimeConfig, 'version' | 'xsl' | 'credits' | 'minify'>, errorInfo?: {
4
+ messages: string[];
5
+ urls: string[];
6
+ }): string;
@@ -1,7 +1,9 @@
1
+ import { withQuery } from "ufo";
2
+ import { xmlEscape } from "../../utils.js";
1
3
  export function escapeValueForXml(value) {
2
4
  if (value === true || value === false)
3
5
  return value ? "yes" : "no";
4
- return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
6
+ return xmlEscape(String(value));
5
7
  }
6
8
  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
9
  function buildUrlXml(url) {
@@ -162,13 +164,20 @@ function buildUrlXml(url) {
162
164
  parts[partIndex++] = " </url>";
163
165
  return parts.slice(0, partIndex).join("\n");
164
166
  }
165
- export function urlsToXml(urls, resolvers, { version, xsl, credits, minify }) {
167
+ export function urlsToXml(urls, resolvers, { version, xsl, credits, minify }, errorInfo) {
166
168
  const estimatedSize = urls.length + 5;
167
169
  const xmlParts = Array.from({ length: estimatedSize });
168
170
  let partIndex = 0;
169
- const xslHref = xsl ? resolvers.relativeBaseUrlResolver(xsl) : false;
171
+ let xslHref = xsl ? resolvers.relativeBaseUrlResolver(xsl) : false;
172
+ if (xslHref && errorInfo && errorInfo.messages.length > 0) {
173
+ xslHref = withQuery(xslHref, {
174
+ errors: "true",
175
+ error_messages: errorInfo.messages,
176
+ error_urls: errorInfo.urls
177
+ });
178
+ }
170
179
  if (xslHref) {
171
- xmlParts[partIndex++] = `<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet type="text/xsl" href="${xslHref}"?>`;
180
+ xmlParts[partIndex++] = `<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet type="text/xsl" href="${escapeValueForXml(xslHref)}"?>`;
172
181
  } else {
173
182
  xmlParts[partIndex++] = '<?xml version="1.0" encoding="UTF-8"?>';
174
183
  }
@@ -41,7 +41,7 @@ async function buildSitemapXml(event, definition, resolvers, runtimeConfig) {
41
41
  });
42
42
  }
43
43
  }
44
- const sitemapUrls = await buildSitemapUrls(definition, resolvers, runtimeConfig, nitro);
44
+ const { urls: sitemapUrls, failedSources } = await buildSitemapUrls(definition, resolvers, runtimeConfig, nitro);
45
45
  const routeRuleMatcher = createNitroRouteRuleMatcher();
46
46
  const { autoI18n } = runtimeConfig;
47
47
  let validCount = 0;
@@ -94,7 +94,11 @@ async function buildSitemapXml(event, definition, resolvers, runtimeConfig) {
94
94
  }
95
95
  }
96
96
  }
97
- const sitemap = urlsToXml(urls, resolvers, runtimeConfig);
97
+ const errorInfo = failedSources.length > 0 ? {
98
+ messages: failedSources.map((f) => f.error),
99
+ urls: failedSources.map((f) => f.url)
100
+ } : void 0;
101
+ const sitemap = urlsToXml(urls, resolvers, runtimeConfig, errorInfo);
98
102
  const ctx = { sitemap, sitemapName, event };
99
103
  await nitro.hooks.callHook("sitemap:output", ctx);
100
104
  return ctx.sitemap;
@@ -120,7 +124,7 @@ const buildSitemapXmlCached = defineCachedFunction(
120
124
  );
121
125
  export async function createSitemap(event, definition, runtimeConfig) {
122
126
  const resolvers = useNitroUrlResolvers(event);
123
- const shouldCache = !import.meta.dev && runtimeConfig.cacheMaxAgeSeconds > 0;
127
+ const shouldCache = !import.meta.dev && typeof runtimeConfig.cacheMaxAgeSeconds === "number" && runtimeConfig.cacheMaxAgeSeconds > 0;
124
128
  const xml = shouldCache ? await buildSitemapXmlCached(event, definition, resolvers, runtimeConfig) : await buildSitemapXml(event, definition, resolvers, runtimeConfig);
125
129
  setHeader(event, "Content-Type", "text/xml; charset=UTF-8");
126
130
  if (runtimeConfig.cacheMaxAgeSeconds) {
@@ -75,7 +75,7 @@ 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
- const alternatives = e.alternatives;
78
+ const alternatives = e.alternatives.map((a) => ({ ...a }));
79
79
  for (let i = 0; i < alternatives.length; i++) {
80
80
  const alt = alternatives[i];
81
81
  if (typeof alt.href === "string") {
@@ -87,19 +87,20 @@ export function normaliseEntry(_e, defaults, resolvers) {
87
87
  e.alternatives = mergeOnKey(alternatives, "hreflang");
88
88
  }
89
89
  if (e.images) {
90
- const images = e.images;
90
+ const images = e.images.map((i) => ({ ...i }));
91
91
  for (let i = 0; i < images.length; i++) {
92
92
  images[i].loc = resolve(images[i].loc, resolvers);
93
93
  }
94
94
  e.images = mergeOnKey(images, "loc");
95
95
  }
96
96
  if (e.videos) {
97
- const videos = e.videos;
97
+ const videos = e.videos.map((v) => ({ ...v }));
98
98
  for (let i = 0; i < videos.length; i++) {
99
99
  if (videos[i].content_loc) {
100
100
  videos[i].content_loc = resolve(videos[i].content_loc, resolvers);
101
101
  }
102
102
  }
103
+ e.videos = mergeOnKey(videos, "content_loc");
103
104
  }
104
105
  return e;
105
106
  }
@@ -2,35 +2,70 @@ import { getRequestHost } from "h3";
2
2
  import { defu } from "defu";
3
3
  import { parseURL } from "ufo";
4
4
  import { extractSitemapXML } from "../utils/extractSitemapXML.js";
5
+ import { logger } from "../../../utils-pure.js";
6
+ async function tryFetchWithFallback(url, options, event) {
7
+ const isExternalUrl = !url.startsWith("/");
8
+ if (isExternalUrl) {
9
+ const strategies = [
10
+ // Strategy 1: Use globalThis.$fetch (original approach)
11
+ () => globalThis.$fetch(url, options),
12
+ // Strategy 2: If event is available, try using event context even for external URLs
13
+ event ? () => event.$fetch(url, options) : null,
14
+ // Strategy 3: Use native fetch as last resort
15
+ () => $fetch(url, options)
16
+ ].filter(Boolean);
17
+ let lastError = null;
18
+ for (const strategy of strategies) {
19
+ try {
20
+ return await strategy();
21
+ } catch (error) {
22
+ lastError = error;
23
+ continue;
24
+ }
25
+ }
26
+ throw lastError;
27
+ }
28
+ const fetchContainer = url.startsWith("/") && event ? event : globalThis;
29
+ return await fetchContainer.$fetch(url, options);
30
+ }
5
31
  export async function fetchDataSource(input, event) {
6
32
  const context = typeof input.context === "string" ? { name: input.context } : input.context || { name: "fetch" };
7
- context.tips = context.tips || [];
8
33
  const url = typeof input.fetch === "string" ? input.fetch : input.fetch[0];
9
34
  const options = typeof input.fetch === "string" ? {} : input.fetch[1];
10
35
  const start = Date.now();
11
- const timeout = options.timeout || 5e3;
36
+ const isExternalUrl = !url.startsWith("/");
37
+ const timeout = isExternalUrl ? 1e4 : options.timeout || 5e3;
12
38
  const timeoutController = new AbortController();
13
39
  const abortRequestTimeout = setTimeout(() => timeoutController.abort(), timeout);
14
- let isMaybeErrorResponse = false;
15
- const isXmlRequest = parseURL(url).pathname.endsWith(".xml");
16
- const fetchContainer = url.startsWith("/") && event ? event : globalThis;
17
40
  try {
18
- const res = await fetchContainer.$fetch(url, {
41
+ let isMaybeErrorResponse = false;
42
+ const isXmlRequest = parseURL(url).pathname.endsWith(".xml");
43
+ const mergedHeaders = defu(
44
+ options?.headers,
45
+ {
46
+ Accept: isXmlRequest ? "text/xml" : "application/json"
47
+ },
48
+ event ? { host: getRequestHost(event, { xForwardedHost: true }) } : {}
49
+ );
50
+ const fetchOptions = {
19
51
  ...options,
20
52
  responseType: isXmlRequest ? "text" : "json",
21
53
  signal: timeoutController.signal,
22
- headers: defu(options?.headers, {
23
- Accept: isXmlRequest ? "text/xml" : "application/json"
24
- }, event ? { host: getRequestHost(event, { xForwardedHost: true }) } : {}),
54
+ headers: mergedHeaders,
55
+ // Use ofetch's built-in retry for external sources
56
+ ...isExternalUrl && {
57
+ retry: 2,
58
+ retryDelay: 200
59
+ },
25
60
  // @ts-expect-error untyped
26
61
  onResponse({ response }) {
27
62
  if (typeof response._data === "string" && response._data.startsWith("<!DOCTYPE html>"))
28
63
  isMaybeErrorResponse = true;
29
64
  }
30
- });
65
+ };
66
+ const res = await tryFetchWithFallback(url, fetchOptions, event);
31
67
  const timeTakenMs = Date.now() - start;
32
68
  if (isMaybeErrorResponse) {
33
- context.tips.push("This is usually because the URL isn't correct or is throwing an error. Please check the URL");
34
69
  return {
35
70
  ...input,
36
71
  context,
@@ -53,16 +88,26 @@ export async function fetchDataSource(input, event) {
53
88
  };
54
89
  } catch (_err) {
55
90
  const error = _err;
56
- if (error.message.includes("This operation was aborted"))
57
- context.tips.push("The request has taken too long. Make sure app sources respond within 5 seconds or adjust the timeout fetch option.");
58
- else
59
- context.tips.push(`Response returned a status of ${error.response?.status || "unknown"}.`);
60
- console.error("[@nuxtjs/sitemap] Failed to fetch source.", { url, error });
91
+ if (isExternalUrl) {
92
+ const errorInfo = {
93
+ url,
94
+ timeout,
95
+ error: error.message,
96
+ statusCode: error.response?.status,
97
+ statusText: error.response?.statusText,
98
+ method: options?.method || "GET"
99
+ };
100
+ logger.error("Failed to fetch external source.", errorInfo);
101
+ } else {
102
+ logger.error("Failed to fetch source.", { url, error: error.message });
103
+ }
61
104
  return {
62
105
  ...input,
63
106
  context,
64
107
  urls: [],
65
- error: error.message
108
+ error: error.message,
109
+ _isFailure: true
110
+ // Mark as failure to prevent caching
66
111
  };
67
112
  } finally {
68
113
  if (abortRequestTimeout) {
@@ -1,4 +1,5 @@
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): string;
4
5
  export declare function useSitemapRuntimeConfig(e?: H3Event): ModuleRuntimeConfig;
@@ -1,6 +1,9 @@
1
1
  import { useRuntimeConfig } from "nitropack/runtime";
2
2
  import { normalizeRuntimeFilters } from "../utils-pure.js";
3
3
  export * from "../utils-pure.js";
4
+ export function xmlEscape(str) {
5
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
6
+ }
4
7
  export function useSitemapRuntimeConfig(e) {
5
8
  const clone = JSON.parse(JSON.stringify(useRuntimeConfig(e).sitemap));
6
9
  for (const k in clone.sitemaps) {
@@ -178,6 +178,7 @@ export interface SitemapSourceResolved extends Omit<SitemapSourceBase, 'urls'> {
178
178
  urls: SitemapUrlInput[];
179
179
  error?: any;
180
180
  timeTakenMs?: number;
181
+ _isFailure?: boolean;
181
182
  }
182
183
  export type AppSourceContext = 'nuxt:pages' | 'nuxt:prerender' | 'nuxt:route-rules' | '@nuxtjs/i18n:pages' | '@nuxt/content:document-driven';
183
184
  export type SitemapSourceInput = string | [string, FetchOptions] | SitemapSourceBase | SitemapSourceResolved;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nuxtjs/sitemap",
3
3
  "type": "module",
4
- "version": "7.3.0",
4
+ "version": "7.4.0",
5
5
  "description": "Powerfully flexible XML Sitemaps that integrate seamlessly, for Nuxt.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -44,8 +44,8 @@
44
44
  }
45
45
  },
46
46
  "dependencies": {
47
- "@nuxt/devtools-kit": "^2.4.1",
48
- "@nuxt/kit": "^3.17.3",
47
+ "@nuxt/devtools-kit": "^2.5.0",
48
+ "@nuxt/kit": "^3.17.5",
49
49
  "chalk": "^5.4.1",
50
50
  "defu": "^6.1.4",
51
51
  "h3-compression": "^0.3.2",
@@ -59,23 +59,24 @@
59
59
  "ufo": "^1.6.1"
60
60
  },
61
61
  "devDependencies": {
62
- "@arethetypeswrong/cli": "^0.18.1",
63
- "@nuxt/content": "^3.5.1",
64
- "@nuxt/eslint-config": "^1.4.0",
62
+ "@arethetypeswrong/cli": "^0.18.2",
63
+ "@nuxt/content": "^3.6.0",
64
+ "@nuxt/eslint-config": "^1.4.1",
65
65
  "@nuxt/module-builder": "^1.0.1",
66
- "@nuxt/test-utils": "^3.19.0",
67
- "@nuxt/ui": "^3.1.2",
68
- "@nuxtjs/i18n": "^9.5.4",
66
+ "@nuxt/test-utils": "^3.19.1",
67
+ "@nuxt/ui": "^3.1.3",
68
+ "@nuxtjs/i18n": "^9.5.5",
69
69
  "@nuxtjs/robots": "^5.2.10",
70
+ "better-sqlite3": "^11.10.0",
70
71
  "bumpp": "^10.1.1",
71
- "eslint": "^9.27.0",
72
- "eslint-plugin-n": "^17.18.0",
73
- "execa": "^9.5.3",
74
- "happy-dom": "^17.4.7",
75
- "nuxt": "^3.17.3",
72
+ "eslint": "^9.29.0",
73
+ "eslint-plugin-n": "^17.20.0",
74
+ "execa": "^9.6.0",
75
+ "happy-dom": "^18.0.1",
76
+ "nuxt": "^3.17.5",
76
77
  "nuxt-i18n-micro": "^1.87.0",
77
78
  "typescript": "^5.8.3",
78
- "vitest": "^3.1.3",
79
+ "vitest": "^3.2.3",
79
80
  "vue-tsc": "^2.2.10"
80
81
  },
81
82
  "scripts": {