@nuxtjs/sitemap 8.2.3 → 8.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 (46) hide show
  1. package/dist/content.mjs +11 -8
  2. package/dist/devtools/lib/sitemap/state.ts +4 -1
  3. package/dist/devtools/pages/sitemap/index.vue +2 -2
  4. package/dist/module.json +2 -2
  5. package/dist/module.mjs +29 -45
  6. package/dist/runtime/server/plugins/compression.js +57 -13
  7. package/dist/runtime/server/plugins/stream-transport.d.ts +2 -0
  8. package/dist/runtime/server/plugins/stream-transport.js +16 -0
  9. package/dist/runtime/server/routes/__sitemap__/debug-production.d.ts +3 -3
  10. package/dist/runtime/server/routes/__sitemap__/debug-production.js +43 -27
  11. package/dist/runtime/server/routes/__sitemap__/debug.d.ts +1 -0
  12. package/dist/runtime/server/routes/__zero-runtime/sitemap/[sitemap].xml.d.ts +1 -1
  13. package/dist/runtime/server/routes/__zero-runtime/sitemap.xml.d.ts +1 -1
  14. package/dist/runtime/server/routes/__zero-runtime/sitemap_index.xml.d.ts +1 -1
  15. package/dist/runtime/server/routes/sitemap/[sitemap].xml.d.ts +1 -1
  16. package/dist/runtime/server/routes/sitemap.xml.d.ts +1 -1
  17. package/dist/runtime/server/routes/sitemap_index.xml.d.ts +1 -1
  18. package/dist/runtime/server/sitemap/builder/index-xml.d.ts +13 -0
  19. package/dist/runtime/server/sitemap/builder/index-xml.js +67 -0
  20. package/dist/runtime/server/sitemap/builder/sitemap-index.d.ts +1 -4
  21. package/dist/runtime/server/sitemap/builder/sitemap-index.js +15 -60
  22. package/dist/runtime/server/sitemap/builder/sitemap.d.ts +1 -1
  23. package/dist/runtime/server/sitemap/builder/sitemap.js +36 -25
  24. package/dist/runtime/server/sitemap/builder/xml.d.ts +9 -1
  25. package/dist/runtime/server/sitemap/builder/xml.js +37 -11
  26. package/dist/runtime/server/sitemap/event-handlers.d.ts +3 -3
  27. package/dist/runtime/server/sitemap/event-handlers.js +18 -23
  28. package/dist/runtime/server/sitemap/nitro.d.ts +4 -1
  29. package/dist/runtime/server/sitemap/nitro.js +116 -26
  30. package/dist/runtime/server/sitemap/stream.d.ts +30 -0
  31. package/dist/runtime/server/sitemap/stream.js +144 -0
  32. package/dist/runtime/server/sitemap/urlset/normalise.d.ts +6 -1
  33. package/dist/runtime/server/sitemap/urlset/normalise.js +26 -6
  34. package/dist/runtime/server/sitemap/urlset/sort.js +12 -3
  35. package/dist/runtime/server/sitemap/urlset/sources.js +50 -9
  36. package/dist/runtime/server/utils.d.ts +0 -1
  37. package/dist/runtime/server/utils.js +12 -12
  38. package/dist/runtime/types.d.ts +14 -3
  39. package/dist/runtime/utils-pure.d.ts +5 -4
  40. package/dist/runtime/utils-pure.js +43 -26
  41. package/dist/utils.d.mts +5 -29
  42. package/dist/utils.d.ts +5 -29
  43. package/dist/utils.mjs +3 -418
  44. package/package.json +28 -30
  45. package/dist/runtime/server/kit.d.ts +0 -2
  46. package/dist/runtime/server/kit.js +0 -23
@@ -148,11 +148,22 @@ export interface ModuleOptions extends SitemapDefinition {
148
148
  */
149
149
  experimentalWarmUp?: boolean;
150
150
  /**
151
- * Send the Sitemap as a compressed stream supporting gzip, brolti, etc.
151
+ * Compress sitemap responses with gzip or deflate when supported by the client.
152
152
  *
153
- * @experimental Will be enabled by default in v5 (if stable)
153
+ * @experimental This option may change in a future release.
154
154
  */
155
155
  experimentalCompression?: boolean;
156
+ /**
157
+ * Serialize dynamic sitemap responses as a pull-driven stream instead of building
158
+ * the complete XML string in memory first. Prerendered sitemaps remain buffered.
159
+ *
160
+ * Accessing `ctx.sitemap` in the `sitemap:output` hook buffers that response for
161
+ * backwards compatibility.
162
+ *
163
+ * @default false
164
+ * @experimental This option may change in a future release.
165
+ */
166
+ experimentalStreaming?: boolean;
156
167
  /**
157
168
  * When enabled, sitemap generation only runs during prerendering.
158
169
  * The sitemap building code is tree-shaken from the runtime bundle.
@@ -227,7 +238,7 @@ export interface AutoI18nConfig {
227
238
  strategy: 'prefix' | 'prefix_except_default' | 'prefix_and_default' | 'no_prefix';
228
239
  pages?: Record<string, Record<string, string | false>>;
229
240
  }
230
- export interface ModuleRuntimeConfig extends Pick<ModuleOptions, 'sitemapsPathPrefix' | 'cacheMaxAgeSeconds' | 'sitemapName' | 'excludeAppSources' | 'sortEntries' | 'defaultSitemapsChunkSize' | 'xslColumns' | 'xslTips' | 'debug' | 'discoverImages' | 'discoverVideos' | 'autoLastmod' | 'xsl' | 'credits' | 'minify'> {
241
+ export interface ModuleRuntimeConfig extends Pick<ModuleOptions, 'sitemapsPathPrefix' | 'cacheMaxAgeSeconds' | 'sitemapName' | 'excludeAppSources' | 'sortEntries' | 'defaultSitemapsChunkSize' | 'xslColumns' | 'xslTips' | 'debug' | 'discoverImages' | 'discoverVideos' | 'autoLastmod' | 'xsl' | 'credits' | 'minify' | 'experimentalStreaming'> {
231
242
  version: string;
232
243
  isNuxtContentDocumentDriven: boolean;
233
244
  sitemaps: {
@@ -1,8 +1,9 @@
1
1
  import type { FilterInput } from './types.js';
2
2
  export { createFilter, type CreateFilterOptions } from 'nuxtseo-shared/utils';
3
3
  export declare const logger: import("consola").ConsolaInstance;
4
- export declare function mergeOnKey<T, K extends keyof T>(arr: T[], key: K): T[];
5
- export declare function splitForLocales(path: string, locales: string[]): [string | null, string];
4
+ export declare function xmlEscape(value: string | number | boolean | Date): string;
5
+ export declare function mergeOnKey<T, K extends keyof T>(arr: T[], key: K, onMerge?: (key: T[K]) => void): T[];
6
+ export declare function splitForLocales(path: string, locales: readonly string[] | Set<string>): [string | null, string];
6
7
  /**
7
8
  * Resolve which locale a multi-sitemap name belongs to.
8
9
  *
@@ -20,10 +21,10 @@ export declare function normalizeRuntimeFilters(input?: FilterInput[]): (RegExp
20
21
  export declare function createPathFilter(options?: {
21
22
  include?: (FilterInput | string | RegExp)[];
22
23
  exclude?: (FilterInput | string | RegExp)[];
23
- }, baseURL?: string): (loc: string) => boolean;
24
+ }, baseURL?: string): (loc: string, pathname?: string) => boolean;
24
25
  export interface PageMatch {
25
26
  mappings: Record<string, string | false>;
26
27
  paramSegments: string[];
27
28
  }
28
- export declare function findPageMapping(pathWithoutPrefix: string, pages: Record<string, Record<string, string | false>>): PageMatch | null;
29
+ export declare function findPageMapping(pathWithoutPrefix: string, pages: Record<string, Record<string, string | false>>, sortedKeys?: string[]): PageMatch | null;
29
30
  export declare function applyDynamicParams(customPath: string, paramSegments: string[]): string;
@@ -1,39 +1,54 @@
1
- import { createConsola } from "consola";
2
1
  import { createDefu } from "defu";
3
- import { createFilter } from "nuxtseo-shared/utils";
4
- import { parseURL, withLeadingSlash, withoutBase } from "ufo";
2
+ import { createFilter, createModuleLogger } from "nuxtseo-shared/utils";
3
+ import { parseURL, withoutBase } from "ufo";
5
4
  export { createFilter } from "nuxtseo-shared/utils";
6
- export const logger = createConsola({
7
- defaults: {
8
- tag: "@nuxt/sitemap"
9
- }
10
- });
5
+ export const logger = createModuleLogger("@nuxt/sitemap");
6
+ const XML_ENTITIES = {
7
+ "&": "&amp;",
8
+ "<": "&lt;",
9
+ ">": "&gt;",
10
+ '"': "&quot;",
11
+ "'": "&apos;"
12
+ };
13
+ const XML_SPECIAL_CHARS_RE = /[&<>"']/g;
14
+ const HAS_XML_SPECIAL_CHARS_RE = /[&<>"']/;
15
+ export function xmlEscape(value) {
16
+ const input = String(value);
17
+ return HAS_XML_SPECIAL_CHARS_RE.test(input) ? input.replace(XML_SPECIAL_CHARS_RE, (char) => XML_ENTITIES[char]) : input;
18
+ }
11
19
  const merger = createDefu((obj, key, value) => {
12
20
  if (Array.isArray(obj[key]) && Array.isArray(value))
13
21
  obj[key] = Array.from(/* @__PURE__ */ new Set([...obj[key], ...value]));
14
22
  return obj[key];
15
23
  });
16
- export function mergeOnKey(arr, key) {
24
+ export function mergeOnKey(arr, key, onMerge) {
25
+ if (arr.length < 2)
26
+ return arr;
17
27
  const seen = /* @__PURE__ */ new Map();
18
28
  let resultLength = 0;
19
- const result = Array.from({ length: arr.length });
20
29
  for (const item of arr) {
21
30
  const k = item[key];
22
31
  if (seen.has(k)) {
23
32
  const existingIndex = seen.get(k);
24
- result[existingIndex] = merger(item, result[existingIndex]);
33
+ onMerge?.(item[key]);
34
+ arr[existingIndex] = merger(item, arr[existingIndex]);
25
35
  } else {
26
36
  seen.set(k, resultLength);
27
- result[resultLength++] = item;
37
+ arr[resultLength++] = item;
28
38
  }
29
39
  }
30
- result.length = resultLength;
31
- return result;
40
+ arr.length = resultLength;
41
+ return arr;
32
42
  }
33
43
  export function splitForLocales(path, locales) {
34
- const prefix = withLeadingSlash(path).split("/")[1];
35
- if (prefix && locales.includes(prefix))
36
- return [prefix, path.replace(`/${prefix}`, "")];
44
+ const start = path.charCodeAt(0) === 47 ? 1 : 0;
45
+ const end = path.indexOf("/", start);
46
+ const prefix = path.slice(start, end === -1 ? path.length : end);
47
+ const hasLocale = locales instanceof Set ? locales.has(prefix) : locales.includes(prefix);
48
+ if (prefix && hasLocale) {
49
+ const prefixEnd = start + prefix.length;
50
+ return [prefix, start === 1 ? path.slice(prefixEnd) : path];
51
+ }
37
52
  return [null, path];
38
53
  }
39
54
  export function resolveI18nSitemapLocaleKey(sitemapName, localeSitemapKeys) {
@@ -63,25 +78,27 @@ export function createPathFilter(options = {}, baseURL) {
63
78
  exclude: normalizeRuntimeFilters(options.exclude)
64
79
  });
65
80
  const hasBase = baseURL && baseURL !== "/";
66
- return (loc) => {
67
- let path = loc;
68
- try {
69
- path = parseURL(loc).pathname;
70
- } catch {
71
- return false;
81
+ return (loc, pathname) => {
82
+ let path = pathname;
83
+ if (typeof path !== "string") {
84
+ try {
85
+ path = parseURL(loc).pathname;
86
+ } catch {
87
+ return false;
88
+ }
72
89
  }
73
90
  if (hasBase)
74
91
  path = withoutBase(path, baseURL);
75
92
  return urlFilter(path);
76
93
  };
77
94
  }
78
- export function findPageMapping(pathWithoutPrefix, pages) {
95
+ export function findPageMapping(pathWithoutPrefix, pages, sortedKeys) {
79
96
  const stripped = pathWithoutPrefix[0] === "/" ? pathWithoutPrefix.slice(1) : pathWithoutPrefix;
80
97
  const pageKey = stripped.endsWith("/index") ? stripped.slice(0, -6) || "index" : stripped || "index";
81
98
  if (pages[pageKey])
82
99
  return { mappings: pages[pageKey], paramSegments: [] };
83
- const sortedKeys = Object.keys(pages).sort((a, b) => b.length - a.length);
84
- for (const key of sortedKeys) {
100
+ const keys = sortedKeys || Object.keys(pages).sort((a, b) => b.length - a.length);
101
+ for (const key of keys) {
85
102
  if (pageKey.startsWith(`${key}/`)) {
86
103
  const paramPath = pageKey.slice(key.length + 1);
87
104
  return { mappings: pages[key], paramSegments: paramPath.split("/") };
package/dist/utils.d.mts CHANGED
@@ -1,5 +1,8 @@
1
- import { SitemapUrl, SitemapUrlInput } from '../dist/runtime/types.js';
1
+ import { SitemapUrl } from '../dist/runtime/types.js';
2
2
  export * from '../dist/runtime/types.js';
3
+ export { SitemapDocumentLoadResult, SitemapDocumentLoader, SitemapLoadFailureCode, SitemapLoadRequest, SitemapLoadSource, SitemapReadOptions, SitemapReadResult, SitemapReader, SitemapReaderOptions, SitemapTargetAuthorization, SitemapTargetAuthorizer, SitemapWalkDocument, SitemapWalkDocumentVisitor, SitemapWalkFailure, SitemapWalkNonRetainedResult, SitemapWalkOptions, SitemapWalkPartialReason, SitemapWalkResult, SitemapWalkRetainedResult, createSitemapReader } from 'sitemapd';
4
+ export { FetchDocumentLoaderOptions, SitemapFetch, createFetchDocumentLoader } from 'sitemapd/fetch';
5
+ export { CollectSitemapResult, ParseSitemapOptions, SitemapCompleteness, SitemapDocument, SitemapDocumentKind, SitemapExtensions, SitemapFormat, SitemapInput, SitemapIssue, SitemapIssueCode, SitemapParseEvent, SitemapReference, SitemapUrlRecord, collectSitemap, parseSitemap } from 'sitemapd/parse';
3
6
 
4
7
  declare function parseHtmlExtractSitemapMeta(html: string, options?: {
5
8
  images?: boolean;
@@ -9,31 +12,4 @@ declare function parseHtmlExtractSitemapMeta(html: string, options?: {
9
12
  resolveUrl?: (s: string) => string;
10
13
  }): Partial<SitemapUrl> | null;
11
14
 
12
- interface SitemapWarning {
13
- type: 'validation';
14
- message: string;
15
- context?: {
16
- url?: string;
17
- field?: string;
18
- value?: unknown;
19
- };
20
- }
21
- interface SitemapParseResult {
22
- urls: SitemapUrlInput[];
23
- warnings: SitemapWarning[];
24
- }
25
- declare function parseSitemapXml(xml: string): Promise<SitemapParseResult>;
26
-
27
- interface SitemapIndexEntry {
28
- loc: string;
29
- lastmod?: string;
30
- }
31
- interface SitemapIndexParseResult {
32
- entries: SitemapIndexEntry[];
33
- warnings: SitemapWarning[];
34
- }
35
- declare function parseSitemapIndex(xml: string): Promise<SitemapIndexParseResult>;
36
- declare function isSitemapIndex(xml: string): boolean;
37
-
38
- export { isSitemapIndex, parseHtmlExtractSitemapMeta, parseSitemapIndex, parseSitemapXml };
39
- export type { SitemapIndexEntry, SitemapIndexParseResult, SitemapParseResult, SitemapWarning };
15
+ export { parseHtmlExtractSitemapMeta };
package/dist/utils.d.ts CHANGED
@@ -1,5 +1,8 @@
1
- import { SitemapUrl, SitemapUrlInput } from '../dist/runtime/types.js';
1
+ import { SitemapUrl } from '../dist/runtime/types.js';
2
2
  export * from '../dist/runtime/types.js';
3
+ export { SitemapDocumentLoadResult, SitemapDocumentLoader, SitemapLoadFailureCode, SitemapLoadRequest, SitemapLoadSource, SitemapReadOptions, SitemapReadResult, SitemapReader, SitemapReaderOptions, SitemapTargetAuthorization, SitemapTargetAuthorizer, SitemapWalkDocument, SitemapWalkDocumentVisitor, SitemapWalkFailure, SitemapWalkNonRetainedResult, SitemapWalkOptions, SitemapWalkPartialReason, SitemapWalkResult, SitemapWalkRetainedResult, createSitemapReader } from 'sitemapd';
4
+ export { FetchDocumentLoaderOptions, SitemapFetch, createFetchDocumentLoader } from 'sitemapd/fetch';
5
+ export { CollectSitemapResult, ParseSitemapOptions, SitemapCompleteness, SitemapDocument, SitemapDocumentKind, SitemapExtensions, SitemapFormat, SitemapInput, SitemapIssue, SitemapIssueCode, SitemapParseEvent, SitemapReference, SitemapUrlRecord, collectSitemap, parseSitemap } from 'sitemapd/parse';
3
6
 
4
7
  declare function parseHtmlExtractSitemapMeta(html: string, options?: {
5
8
  images?: boolean;
@@ -9,31 +12,4 @@ declare function parseHtmlExtractSitemapMeta(html: string, options?: {
9
12
  resolveUrl?: (s: string) => string;
10
13
  }): Partial<SitemapUrl> | null;
11
14
 
12
- interface SitemapWarning {
13
- type: 'validation';
14
- message: string;
15
- context?: {
16
- url?: string;
17
- field?: string;
18
- value?: unknown;
19
- };
20
- }
21
- interface SitemapParseResult {
22
- urls: SitemapUrlInput[];
23
- warnings: SitemapWarning[];
24
- }
25
- declare function parseSitemapXml(xml: string): Promise<SitemapParseResult>;
26
-
27
- interface SitemapIndexEntry {
28
- loc: string;
29
- lastmod?: string;
30
- }
31
- interface SitemapIndexParseResult {
32
- entries: SitemapIndexEntry[];
33
- warnings: SitemapWarning[];
34
- }
35
- declare function parseSitemapIndex(xml: string): Promise<SitemapIndexParseResult>;
36
- declare function isSitemapIndex(xml: string): boolean;
37
-
38
- export { isSitemapIndex, parseHtmlExtractSitemapMeta, parseSitemapIndex, parseSitemapXml };
39
- export type { SitemapIndexEntry, SitemapIndexParseResult, SitemapParseResult, SitemapWarning };
15
+ export { parseHtmlExtractSitemapMeta };
package/dist/utils.mjs CHANGED
@@ -1,421 +1,6 @@
1
1
  export { p as parseHtmlExtractSitemapMeta } from './shared/sitemap.BoMnWHOt.mjs';
2
+ export { createSitemapReader } from 'sitemapd';
3
+ export { createFetchDocumentLoader } from 'sitemapd/fetch';
4
+ export { collectSitemap, parseSitemap } from 'sitemapd/parse';
2
5
  import 'ufo';
3
6
  import 'ultrahtml';
4
-
5
- function isValidUrl(value) {
6
- return URL.canParse(value);
7
- }
8
- async function parseSitemapIndex(xml) {
9
- if (!xml)
10
- throw new Error("Empty XML input provided");
11
- const { XMLParser } = await import('fast-xml-parser');
12
- const parser = new XMLParser({
13
- isArray: (tagName) => tagName === "sitemap",
14
- removeNSPrefix: true,
15
- trimValues: true
16
- });
17
- const parsed = parser.parse(xml);
18
- if (parsed?.sitemapindex === void 0)
19
- throw new Error("XML does not contain a valid sitemapindex element");
20
- if (!parsed.sitemapindex || !parsed.sitemapindex.sitemap)
21
- return { entries: [], warnings: [] };
22
- const sitemaps = Array.isArray(parsed.sitemapindex.sitemap) ? parsed.sitemapindex.sitemap : [parsed.sitemapindex.sitemap];
23
- const warnings = [];
24
- const entries = [];
25
- for (const s of sitemaps) {
26
- if (typeof s.loc !== "string" || !s.loc.trim().length) {
27
- warnings.push({
28
- type: "validation",
29
- message: "Sitemap entry missing required loc element"
30
- });
31
- continue;
32
- }
33
- const loc = s.loc.trim();
34
- if (!isValidUrl(loc)) {
35
- warnings.push({
36
- type: "validation",
37
- message: "Sitemap entry has invalid URL",
38
- context: { url: loc }
39
- });
40
- continue;
41
- }
42
- entries.push({
43
- loc,
44
- ...s.lastmod && { lastmod: s.lastmod.trim() }
45
- });
46
- }
47
- return { entries, warnings };
48
- }
49
- function isSitemapIndex(xml) {
50
- return xml.includes("<sitemapindex") || xml.includes("sitemapindex>");
51
- }
52
-
53
- function isValidString(value) {
54
- return typeof value === "string" && value.trim().length > 0;
55
- }
56
- function parseNumber(value) {
57
- if (typeof value === "number")
58
- return value;
59
- if (typeof value === "string" && value.trim()) {
60
- const num = Number.parseFloat(value.trim());
61
- return Number.isNaN(num) ? void 0 : num;
62
- }
63
- return void 0;
64
- }
65
- function parseInteger(value) {
66
- if (typeof value === "number")
67
- return Math.floor(value);
68
- if (typeof value === "string" && value.trim()) {
69
- const num = Number.parseInt(value.trim(), 10);
70
- return Number.isNaN(num) ? void 0 : num;
71
- }
72
- return void 0;
73
- }
74
- function extractUrlFromParsedElement(urlElement, warnings) {
75
- if (!isValidString(urlElement.loc)) {
76
- warnings.push({
77
- type: "validation",
78
- message: "URL entry missing required loc element",
79
- context: { url: String(urlElement.loc || "undefined") }
80
- });
81
- return null;
82
- }
83
- const urlObj = { loc: urlElement.loc };
84
- if (isValidString(urlElement.lastmod)) {
85
- urlObj.lastmod = urlElement.lastmod;
86
- }
87
- if (isValidString(urlElement.changefreq)) {
88
- const validFreqs = ["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"];
89
- if (validFreqs.includes(urlElement.changefreq)) {
90
- urlObj.changefreq = urlElement.changefreq;
91
- } else {
92
- warnings.push({
93
- type: "validation",
94
- message: "Invalid changefreq value",
95
- context: { url: urlElement.loc, field: "changefreq", value: urlElement.changefreq }
96
- });
97
- }
98
- }
99
- const priority = parseNumber(urlElement.priority);
100
- if (priority !== void 0 && !Number.isNaN(priority)) {
101
- if (priority < 0 || priority > 1) {
102
- warnings.push({
103
- type: "validation",
104
- message: "Priority value should be between 0.0 and 1.0, clamping to valid range",
105
- context: { url: urlElement.loc, field: "priority", value: priority }
106
- });
107
- }
108
- urlObj.priority = Math.max(0, Math.min(1, priority));
109
- } else if (urlElement.priority !== void 0) {
110
- warnings.push({
111
- type: "validation",
112
- message: "Invalid priority value",
113
- context: { url: urlElement.loc, field: "priority", value: urlElement.priority }
114
- });
115
- }
116
- if (urlElement.image) {
117
- const images = Array.isArray(urlElement.image) ? urlElement.image : [urlElement.image];
118
- const validImages = images.map((img) => {
119
- if (isValidString(img.loc)) {
120
- return { loc: img.loc };
121
- } else {
122
- warnings.push({
123
- type: "validation",
124
- message: "Image missing required loc element",
125
- context: { url: urlElement.loc, field: "image.loc" }
126
- });
127
- return null;
128
- }
129
- }).filter((img) => img !== null);
130
- if (validImages.length > 0) {
131
- urlObj.images = validImages;
132
- }
133
- }
134
- if (urlElement.video) {
135
- const videos = Array.isArray(urlElement.video) ? urlElement.video : [urlElement.video];
136
- const validVideos = videos.map((video) => {
137
- const missingFields = [];
138
- if (!isValidString(video.title))
139
- missingFields.push("title");
140
- if (!isValidString(video.thumbnail_loc))
141
- missingFields.push("thumbnail_loc");
142
- if (!isValidString(video.description))
143
- missingFields.push("description");
144
- if (!isValidString(video.content_loc))
145
- missingFields.push("content_loc");
146
- if (missingFields.length > 0) {
147
- warnings.push({
148
- type: "validation",
149
- message: `Video missing required fields: ${missingFields.join(", ")}`,
150
- context: { url: urlElement.loc, field: "video" }
151
- });
152
- return null;
153
- }
154
- const videoObj = {
155
- title: video.title,
156
- thumbnail_loc: video.thumbnail_loc,
157
- description: video.description,
158
- content_loc: video.content_loc
159
- };
160
- if (isValidString(video.player_loc)) {
161
- videoObj.player_loc = video.player_loc;
162
- }
163
- const duration = parseInteger(video.duration);
164
- if (duration !== void 0) {
165
- videoObj.duration = duration;
166
- } else if (video.duration !== void 0) {
167
- warnings.push({
168
- type: "validation",
169
- message: "Invalid video duration value",
170
- context: { url: urlElement.loc, field: "video.duration", value: video.duration }
171
- });
172
- }
173
- if (isValidString(video.expiration_date)) {
174
- videoObj.expiration_date = video.expiration_date;
175
- }
176
- const rating = parseNumber(video.rating);
177
- if (rating !== void 0) {
178
- if (rating < 0 || rating > 5) {
179
- warnings.push({
180
- type: "validation",
181
- message: "Video rating should be between 0.0 and 5.0",
182
- context: { url: urlElement.loc, field: "video.rating", value: rating }
183
- });
184
- }
185
- videoObj.rating = rating;
186
- } else if (video.rating !== void 0) {
187
- warnings.push({
188
- type: "validation",
189
- message: "Invalid video rating value",
190
- context: { url: urlElement.loc, field: "video.rating", value: video.rating }
191
- });
192
- }
193
- const viewCount = parseInteger(video.view_count);
194
- if (viewCount !== void 0) {
195
- videoObj.view_count = viewCount;
196
- } else if (video.view_count !== void 0) {
197
- warnings.push({
198
- type: "validation",
199
- message: "Invalid video view_count value",
200
- context: { url: urlElement.loc, field: "video.view_count", value: video.view_count }
201
- });
202
- }
203
- if (isValidString(video.publication_date)) {
204
- videoObj.publication_date = video.publication_date;
205
- }
206
- if (isValidString(video.family_friendly)) {
207
- const validValues = ["yes", "no"];
208
- if (validValues.includes(video.family_friendly)) {
209
- videoObj.family_friendly = video.family_friendly;
210
- } else {
211
- warnings.push({
212
- type: "validation",
213
- message: 'Invalid video family_friendly value, should be "yes" or "no"',
214
- context: { url: urlElement.loc, field: "video.family_friendly", value: video.family_friendly }
215
- });
216
- }
217
- }
218
- if (isValidString(video.requires_subscription)) {
219
- const validValues = ["yes", "no"];
220
- if (validValues.includes(video.requires_subscription)) {
221
- videoObj.requires_subscription = video.requires_subscription;
222
- } else {
223
- warnings.push({
224
- type: "validation",
225
- message: 'Invalid video requires_subscription value, should be "yes" or "no"',
226
- context: { url: urlElement.loc, field: "video.requires_subscription", value: video.requires_subscription }
227
- });
228
- }
229
- }
230
- if (isValidString(video.live)) {
231
- const validValues = ["yes", "no"];
232
- if (validValues.includes(video.live)) {
233
- videoObj.live = video.live;
234
- } else {
235
- warnings.push({
236
- type: "validation",
237
- message: 'Invalid video live value, should be "yes" or "no"',
238
- context: { url: urlElement.loc, field: "video.live", value: video.live }
239
- });
240
- }
241
- }
242
- if (video.restriction && typeof video.restriction === "object") {
243
- const restriction = video.restriction;
244
- if (isValidString(restriction.relationship) && isValidString(restriction["#text"])) {
245
- const validRelationships = ["allow", "deny"];
246
- if (validRelationships.includes(restriction.relationship)) {
247
- videoObj.restriction = {
248
- relationship: restriction.relationship,
249
- restriction: restriction["#text"]
250
- };
251
- } else {
252
- warnings.push({
253
- type: "validation",
254
- message: 'Invalid video restriction relationship, should be "allow" or "deny"',
255
- context: { url: urlElement.loc, field: "video.restriction.relationship", value: restriction.relationship }
256
- });
257
- }
258
- }
259
- }
260
- if (video.platform && typeof video.platform === "object") {
261
- const platform = video.platform;
262
- if (isValidString(platform.relationship) && isValidString(platform["#text"])) {
263
- const validRelationships = ["allow", "deny"];
264
- if (validRelationships.includes(platform.relationship)) {
265
- videoObj.platform = {
266
- relationship: platform.relationship,
267
- platform: platform["#text"]
268
- };
269
- } else {
270
- warnings.push({
271
- type: "validation",
272
- message: 'Invalid video platform relationship, should be "allow" or "deny"',
273
- context: { url: urlElement.loc, field: "video.platform.relationship", value: platform.relationship }
274
- });
275
- }
276
- }
277
- }
278
- if (video.price) {
279
- const prices = Array.isArray(video.price) ? video.price : [video.price];
280
- const validPrices = prices.map((price) => {
281
- const priceValue = price["#text"];
282
- if (priceValue == null || typeof priceValue !== "string" && typeof priceValue !== "number") {
283
- warnings.push({
284
- type: "validation",
285
- message: "Video price missing value",
286
- context: { url: urlElement.loc, field: "video.price" }
287
- });
288
- return null;
289
- }
290
- const validTypes = ["rent", "purchase", "package", "subscription"];
291
- if (price.type && !validTypes.includes(price.type)) {
292
- warnings.push({
293
- type: "validation",
294
- message: `Invalid video price type "${price.type}", should be one of: ${validTypes.join(", ")}`,
295
- context: { url: urlElement.loc, field: "video.price.type", value: price.type }
296
- });
297
- }
298
- return {
299
- price: String(priceValue),
300
- currency: price.currency,
301
- type: price.type
302
- };
303
- }).filter((p) => p !== null);
304
- if (validPrices.length > 0) {
305
- videoObj.price = validPrices;
306
- }
307
- }
308
- if (video.uploader && typeof video.uploader === "object") {
309
- const uploader = video.uploader;
310
- if (isValidString(uploader.info) && isValidString(uploader["#text"])) {
311
- videoObj.uploader = {
312
- uploader: uploader["#text"],
313
- info: uploader.info
314
- };
315
- } else {
316
- warnings.push({
317
- type: "validation",
318
- message: "Video uploader missing required info or name",
319
- context: { url: urlElement.loc, field: "video.uploader" }
320
- });
321
- }
322
- }
323
- if (video.tag) {
324
- const tags = Array.isArray(video.tag) ? video.tag : [video.tag];
325
- const validTags = tags.filter(isValidString);
326
- if (validTags.length > 0) {
327
- videoObj.tag = validTags;
328
- }
329
- }
330
- return videoObj;
331
- }).filter((video) => video !== null);
332
- if (validVideos.length > 0) {
333
- urlObj.videos = validVideos;
334
- }
335
- }
336
- if (urlElement.link) {
337
- const links = Array.isArray(urlElement.link) ? urlElement.link : [urlElement.link];
338
- const alternatives = links.map((link) => {
339
- if (link.rel === "alternate" && isValidString(link.hreflang) && isValidString(link.href)) {
340
- return {
341
- hreflang: link.hreflang,
342
- href: link.href
343
- };
344
- } else {
345
- warnings.push({
346
- type: "validation",
347
- message: 'Alternative link missing required rel="alternate", hreflang, or href',
348
- context: { url: urlElement.loc, field: "link" }
349
- });
350
- return null;
351
- }
352
- }).filter((alt) => alt !== null);
353
- if (alternatives.length > 0) {
354
- urlObj.alternatives = alternatives;
355
- }
356
- }
357
- if (urlElement.news && typeof urlElement.news === "object") {
358
- const news = urlElement.news;
359
- if (isValidString(news.title) && isValidString(news.publication_date) && news.publication && isValidString(news.publication.name) && isValidString(news.publication.language)) {
360
- urlObj.news = {
361
- title: news.title,
362
- publication_date: news.publication_date,
363
- publication: {
364
- name: news.publication.name,
365
- language: news.publication.language
366
- }
367
- };
368
- } else {
369
- warnings.push({
370
- type: "validation",
371
- message: "News entry missing required fields (title, publication_date, publication.name, publication.language)",
372
- context: { url: urlElement.loc, field: "news" }
373
- });
374
- }
375
- }
376
- return Object.fromEntries(
377
- Object.entries(urlObj).filter(
378
- ([_, value]) => value != null && (!Array.isArray(value) || value.length > 0)
379
- )
380
- );
381
- }
382
- async function parseSitemapXml(xml) {
383
- const warnings = [];
384
- if (!xml) {
385
- throw new Error("Empty XML input provided");
386
- }
387
- const { XMLParser } = await import('fast-xml-parser');
388
- const parser = new XMLParser({
389
- isArray: (tagName) => ["url", "image", "video", "link", "tag", "price"].includes(tagName),
390
- removeNSPrefix: true,
391
- parseAttributeValue: false,
392
- ignoreAttributes: false,
393
- attributeNamePrefix: "",
394
- trimValues: true
395
- });
396
- try {
397
- const parsed = parser.parse(xml);
398
- if (!parsed?.urlset) {
399
- throw new Error("XML does not contain a valid urlset element");
400
- }
401
- if (!parsed.urlset.url) {
402
- throw new Error("Sitemap contains no URL entries");
403
- }
404
- const urls = Array.isArray(parsed.urlset.url) ? parsed.urlset.url : [parsed.urlset.url];
405
- const validUrls = urls.map((url) => extractUrlFromParsedElement(url, warnings)).filter((url) => url !== null);
406
- if (validUrls.length === 0 && urls.length > 0) {
407
- warnings.push({
408
- type: "validation",
409
- message: "No valid URLs found in sitemap after validation"
410
- });
411
- }
412
- return { urls: validUrls, warnings };
413
- } catch (error) {
414
- if (error instanceof Error && (error.message === "Empty XML input provided" || error.message === "XML does not contain a valid urlset element" || error.message === "Sitemap contains no URL entries")) {
415
- throw error;
416
- }
417
- throw new Error(`Failed to parse XML: ${error instanceof Error ? error.message : String(error)}`);
418
- }
419
- }
420
-
421
- export { isSitemapIndex, parseSitemapIndex, parseSitemapXml };