next-advanced-sitemap 1.1.5 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,6 +13,8 @@ While Next.js provides a built-in `MetadataRoute.Sitemap` utility, it currently
13
13
 
14
14
  - **Google Images Support**: Complete indexation of visual assets with support for titles, captions, local SEO positioning, and copyright protections.
15
15
  - **Image Accessibility Protection (v1.1.2)**: Advanced preventive protection against empty text strings or spaces (`.trim()`) in `title` and `caption` fields to completely eliminate malformed empty XML tokens.
16
+ - **Video Semantic Classification & Long-Tail SEO (v1.1.7)**: Support for `<video:category>` and multiple `<video:tag>` elements to deeply contextualize video content and map assets to highly targeted niche queries.
17
+ - **Video Monetization Models & Prices (v1.1.6)**: Support for `<video:price>` parameters allowing VOD systems, streaming apps, and online academies to append clear monetary tags (`currency`, `value`, `type: rent/own`) directly into Google video indexing carousels.
16
18
  - **Google Video Support**: Boost video search layouts and video-carousel presence on Google Search with complete structured data encapsulation.
17
19
  - **Video Subscription & Paywall Guardrails (v1.1.5)**: Native integration of the `<video:requires_subscription>` tag to signal premium paywall barriers or free-tier states, preventing user-frustration search algorithmic penalties.
18
20
  - **Video Country & Device Restrictions (v1.1.4)**: Advanced access control policy injection via `<video:restriction>` and `<video:platform>` properties to strictly control video delivery layouts across global boundaries and distinct screen classes (`web`, `mobile`, `tv`).
@@ -66,6 +68,11 @@ export async function GET() {
66
68
  publication_date: new Date(),
67
69
  duration: 7200,
68
70
  view_count: 25000,
71
+ // v1.1.7: Semantic Topical Classification & Long-Tail Tags
72
+ category: ' Education & Technology ', // Auto-trimmed and XML-escaped
73
+ tags: ['nextjs', 'typescript', ' advanced seo '], // Limited to 32 tags max
74
+ // v1.1.6: Commercial VOD Pricing Structure (Auto ISO 4217 & decimal formatting)
75
+ price: { value: 19.99, currency: 'usd', type: 'own' },
69
76
  // v1.1.5: Flexible Paywall Registration (Accepts boolean or strict 'yes' | 'no')
70
77
  requires_subscription: true,
71
78
  // v1.1.4: Strict Geographic Filtering & Capitalization Sanitization
@@ -224,64 +231,103 @@ Generates a standard Next.js `Response` object with the correct `application/xml
224
231
  ### SitemapVideo
225
232
 
226
233
  <table>
227
- <thead>
228
- <tr>
229
- <th>Property</th>
230
- <th>Type</th>
231
- <th>Description</th>
232
- </tr>
233
- </thead>
234
- <tbody>
235
- <tr>
236
- <td><code>thumbnail_loc</code></td>
237
- <td>string</td>
238
- <td><strong>Required.</strong> The absolute URL targeting the source image asset.</td>
239
- </tr>
240
- <tr>
241
- <td><code>title</code></td>
242
- <td>string</td>
243
- <td><strong>Required.</strong> The descriptive headline of the video asset. Escaped.</td>
244
- </tr>
245
- <tr>
246
- <td><code>description</code></td>
247
- <td>string</td>
248
- <td><strong>Required.</strong> Summary text representing the video topic. Max 2048 chars.</td>
249
- </tr>
250
- <tr>
251
- <td><code>publication_date</code></td>
252
- <td>Date | string</td>
253
- <td><strong>Required.</strong> Publication date object or raw formatted ISO string.</td>
254
- </tr>
255
- <tr>
256
- <td><code>content_loc</code></td>
257
- <td>string</td>
258
- <td>Optional absolute URL targeting the raw video media stream container.</td>
259
- </tr>
260
- <tr>
261
- <td><code>player_loc</code></td>
262
- <td>string</td>
263
- <td>Optional absolute URL linking out to an embeddable video player frame.</td>
264
- </tr>
265
- <tr>
266
- <td><code>duration</code></td>
267
- <td>number</td>
268
- <td>Optional length in seconds. Must be an integer bounded between 0 and 28800.</td>
269
- </tr>
270
- <tr>
271
- <td><code>view_count</code></td>
272
- <td>number</td>
273
- <td>Optional overall hit counter. Negative values strictly prohibited.</td>
274
- </tr>
275
- <tr>
276
- <td><code>live</code></td>
277
- <td>'yes' | 'no'</td>
278
- <td>Optional switch triggering immediate Google SERP LIVE badges.</td>
279
- </tr>
280
- </tbody>
234
+ <thead>
235
+ <tr>
236
+ <th>Property</th>
237
+ <th>Type</th>
238
+ <th>Description</th>
239
+ </tr>
240
+ </thead>
241
+ <tbody>
242
+ <tr>
243
+ <td><code>thumbnail_loc</code></td>
244
+ <td>string</td>
245
+ <td><strong>Required.</strong> The absolute URL targeting the source image asset.</td>
246
+ </tr>
247
+ <tr>
248
+ <td><code>title</code></td>
249
+ <td>string</td>
250
+ <td><strong>Required.</strong> The descriptive headline of the video asset. Escaped.</td>
251
+ </tr>
252
+ <tr>
253
+ <td><code>description</code></td>
254
+ <td>string</td>
255
+ <td><strong>Required.</strong> Summary text representing the video topic. Max 2048 chars.</td>
256
+ </tr>
257
+ <tr>
258
+ <td><code>publication_date</code></td>
259
+ <td>Date | string</td>
260
+ <td><strong>Required.</strong> Publication date object or raw formatted ISO string.</td>
261
+ </tr>
262
+ <tr>
263
+ <td><code>content_loc</code></td>
264
+ <td>string</td>
265
+ <td>Optional absolute URL targeting the raw video media stream container.</td>
266
+ </tr>
267
+ <tr>
268
+ <td><code>player_loc</code></td>
269
+ <td>string</td>
270
+ <td>Optional absolute URL linking out to an embeddable video player frame.</td>
271
+ </tr>
272
+ <tr>
273
+ <td><code>duration</code></td>
274
+ <td>number</td>
275
+ <td>Optional length in seconds. Must be an integer bounded between 0 and 28800.</td>
276
+ </tr>
277
+ <tr>
278
+ <td><code>view_count</code></td>
279
+ <td>number</td>
280
+ <td>Optional overall hit counter. Negative values strictly prohibited.</td>
281
+ </tr>
282
+ <tr>
283
+ <td><code>live</code></td>
284
+ <td>'yes' | 'no'</td>
285
+ <td>Optional switch triggering immediate Google SERP LIVE badges.</td>
286
+ </tr>
287
+ <tr>
288
+ <td><code>price</code></td>
289
+ <td>VideoPrice</td>
290
+ <td>Optional metadata structure attaching commercial purchase parameters to standard Google video rich cards.</td>
291
+ </tr>
292
+ <tr>
293
+ <td><code>category</code></td>
294
+ <td>string</td>
295
+ <td>Optional general topical category (e.g., 'Education', 'Technology'). Max 256 characters. Automatically trimmed and XML-escaped.</td>
296
+ </tr>
297
+ <tr>
298
+ <td><code>tags</code></td>
299
+ <td>string[]</td>
300
+ <td>Optional array of keywords describing the video. Bound to a strict maximum of 32 tags per video entry. Individual tags are automatically trimmed and XML-escaped.</td>
301
+ </tr>
302
+ <tr>
303
+ <td><code>category</code></td>
304
+ <td>string</td>
305
+ <td>Optional general topical category (e.g., 'Education', 'Technology'). Max 256 characters. Automatically trimmed and XML-escaped.</td>
306
+ </tr>
307
+ <tr>
308
+ <td><code>tags</code></td>
309
+ <td>string[]</td>
310
+ <td>Optional array of keywords describing the video. Bound to a strict maximum of 32 tags per video entry. Individual tags are automatically trimmed and XML-escaped.</td>
311
+ </tr>
312
+ </tbody>
281
313
  </table>
282
314
 
283
315
  ## Technical Implementation
284
316
 
317
+ ### Video Semantic Classification & Strict Structural Boundaries (v1.1.7)
318
+ To establish high topical authority without triggering algorithmic index drops or schema structure rejections in Google Search Console, **v1.1.7** implements multi-layered structural validation guardrails for categorization metadata:
319
+
320
+ - **Strict Array Length Checks**: The engine intercepts tag matrices at runtime. If a video block exceeds Google's hard threshold of 32 tags, the pipeline aborts via a descriptive fail-fast exception to prevent writing non-compliant XML structures.
321
+ - **Character Constraint & Trimming Safeties**: Category strings are automatically run through a whitespace-reduction pipeline. The internal engine verifies that the sanitized result complies with the 256-character limitation while throwing an error if the category reduces to an empty token.
322
+ - **Deep Entity Escaping on Metadata**: To fully protect the XML stream tree from layout crashes caused by characters like `&` or `<` inside user-generated or database-stored categories and tags (e.g., `"Next.js & Architecture"`), every text node is safely passed through the core high-performance regex escaping matrix.
323
+
324
+ ### Video Pay-Per-View & VOD Pricing Architecture (v1.1.6)
325
+ For on-demand streaming infrastructures, private bootcamps, and e-learning engines, exposing precise transactional pricing properties to crawlers structures Google's rich metadata carousels. **v1.1.6** implements strict formatting pipelines to meet internal Google Search Console parameters:
326
+
327
+ - **ISO 4217 Auto-Normalization**: Currency codes are uniformly trimmed and transformed to mandatory uppercase formats (e.g. `currency: 'eur'` standardizes to `currency="EUR"`). Strings mismatching the exact 3-character international standard are rejected instantly.
328
+ - **Float Price Rounding**: Transaction values automatically pass through an integrated decimal standardizer mapping raw database values into clean, 2-digit floating formats (`.toFixed(2)`) to ensure layout parser compliance.
329
+ - **Transactional Intent Filtering**: The structure locks the optional transactional property down to strict literal string unions (`'rent'` | `'own'`) to avoid invalid data entry.
330
+
285
331
  ### Paywall Registration & Subscription Guardrails (v1.1.5)
286
332
  For media syndicates, educational organizations, and video streaming architectures utilizing monetization paywalls, misconfiguring premium access markers can lead to harsh ranking reductions due to misleading click funnels (user frustration loops). **v1.1.5** abstracts this integration completely:
287
333
 
package/dist/index.cjs CHANGED
@@ -241,6 +241,64 @@ function buildVideoXml(videos) {
241
241
  xml += ` <video:requires_subscription>${subValue}</video:requires_subscription>
242
242
  `;
243
243
  }
244
+ if (vid.price) {
245
+ const { value, currency, type } = vid.price;
246
+ if (value === void 0 || value < 0) {
247
+ throw new Error(
248
+ `[next-advanced-sitemap] Invalid video price value: "${value}". Value must be a positive number.`
249
+ );
250
+ }
251
+ const cleanCurrency = currency ? currency.trim().toUpperCase() : "";
252
+ if (cleanCurrency.length !== 3) {
253
+ throw new Error(
254
+ `[next-advanced-sitemap] Invalid ISO 4217 currency code: "${currency}". Currency must be exactly a 3-letter code.`
255
+ );
256
+ }
257
+ let priceXml = ` <video:price currency="${cleanCurrency}"`;
258
+ if (type) {
259
+ if (type !== "rent" && type !== "own") {
260
+ throw new Error(
261
+ `[next-advanced-sitemap] Invalid price type: "${type}". Allowed values are 'rent' or 'own'.`
262
+ );
263
+ }
264
+ priceXml += ` type="${type}"`;
265
+ }
266
+ priceXml += `>${value.toFixed(2)}</video:price>
267
+ `;
268
+ xml += priceXml;
269
+ }
270
+ if (vid.category !== void 0) {
271
+ const cleanCategory = vid.category.trim();
272
+ if (!cleanCategory) {
273
+ throw new Error(
274
+ `[next-advanced-sitemap] Invalid video category: category cannot be empty or just whitespaces.`
275
+ );
276
+ }
277
+ if (cleanCategory.length > 256) {
278
+ throw new Error(
279
+ `[next-advanced-sitemap] Invalid video category length: ${cleanCategory.length}. Maximum allowed is 256 characters.`
280
+ );
281
+ }
282
+ xml += ` <video:category>${escapeXml(cleanCategory)}</video:category>
283
+ `;
284
+ }
285
+ if (vid.tags) {
286
+ if (vid.tags.length > 32) {
287
+ throw new Error(
288
+ `[next-advanced-sitemap] Invalid video tags count: ${vid.tags.length}. A video can have a maximum of 32 tags.`
289
+ );
290
+ }
291
+ for (const tag of vid.tags) {
292
+ const cleanTag = tag.trim();
293
+ if (!cleanTag) {
294
+ throw new Error(
295
+ `[next-advanced-sitemap] Invalid video tag detected: tag cannot be empty or just whitespaces.`
296
+ );
297
+ }
298
+ xml += ` <video:tag>${escapeXml(cleanTag)}</video:tag>
299
+ `;
300
+ }
301
+ }
244
302
  xml += ` </video:video>
245
303
  `;
246
304
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/utils/xml-escape.ts","../src/core/builders/url-builder.ts","../src/core/builders/image-builder.ts","../src/core/builders/video-builder.ts","../src/core/builders/news-builder.ts","../src/core/generator.ts"],"sourcesContent":["/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry, SitemapOptions } from './types/sitemap.js';\nimport { generateXml } from './core/generator.js';\n\nexport * from './types/sitemap.js';\n\n/**\n * Génère une réponse HTTP compatible Next.js (App Router) avec options de configuration.\n * v1.0.9 : Injection dynamique et personnalisable de l'en-tête Cache-Control via l'option maxAge\n * * @param entries - Liste des entrées du sitemap\n * @param options - Options de génération et de mise en cache (ex: autoLastmod, maxAge)\n * @returns Une instance de Response contenant le flux XML configuré\n */\nexport function getServerSitemapResponse(\n entries: SitemapEntry[], \n options: SitemapOptions = {}\n): Response {\n const xml = generateXml(entries, options);\n\n // Détermination de la stratégie de mise en cache (v1.0.9)\n const cacheControlHeader = options.maxAge !== undefined\n ? `public, max-age=${options.maxAge}, must-revalidate`\n : 'public, s-maxage=86400, stale-while-revalidate';\n\n return new Response(xml, {\n headers: {\n 'Content-Type': 'application/xml',\n 'Cache-Control': cacheControlHeader,\n },\n });\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\n/**\n * Convertit les caractères spéciaux en entités XML pour éviter la corruption du fichier.\n * Gère : <, >, &, \", '\n */\nexport function escapeXml(unsafe: string | undefined | null): string {\n if (!unsafe) return '';\n \n return unsafe.replace(/[<>&\"']/g, (c) => {\n switch (c) {\n case '<': return '&lt;';\n case '>': return '&gt;';\n case '&': return '&amp;';\n case '\"': return '&quot;';\n case \"'\": return '&apos;';\n default: return c;\n }\n });\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry, SitemapOptions } from '../../types/sitemap.js';\nimport { escapeXml } from '../../utils/xml-escape.js';\n\n/**\n * Nettoie et valide de manière stricte le format et la structure d'une URL.\n * v1.0.7 : Intégration de l'Auto-Trimming (nettoyage des espaces de début et de fin)\n */\nexport function sanitizeAndValidateUrl(rawUrl: string, context: string): string {\n const url = rawUrl ? rawUrl.trim() : '';\n\n if (!url.startsWith('http://') && !url.startsWith('https://')) {\n throw new Error(\n `[next-advanced-sitemap] Invalid URL in ${context}: \"${url}\". URLs must start with http:// or https://`\n );\n }\n\n if (url.includes(' ')) {\n throw new Error(\n `[next-advanced-sitemap] Malformed URL structure detected in ${context}: \"${url}\". Please verify spaces or special characters.`\n );\n }\n\n let isValid = false;\n if (typeof URL.canParse === 'function') {\n isValid = URL.canParse(url);\n } else {\n try {\n new URL(url);\n isValid = true;\n } catch {\n isValid = false;\n }\n }\n\n if (!isValid) {\n throw new Error(\n `[next-advanced-sitemap] Malformed URL structure detected in ${context}: \"${url}\". Please verify spaces or special characters.`\n );\n }\n\n return url;\n}\n\n/**\n * Génère le bloc XML de base pour un nœud URL (loc, alternates, lastmod, changefreq, priority).\n */\nexport function buildUrlBaseXml(entry: SitemapEntry, options: SitemapOptions, nowIso: string): string {\n let xml = '';\n \n const cleanMainUrl = sanitizeAndValidateUrl(entry.url, 'main entry');\n xml += ` <loc>${escapeXml(cleanMainUrl)}</loc>\\n`;\n\n if (entry.alternates?.length) {\n for (const alt of entry.alternates) {\n const cleanAltUrl = sanitizeAndValidateUrl(alt.href, 'alternate link');\n xml += ` <xhtml:link rel=\"alternate\" hreflang=\"${escapeXml(alt.hreflang)}\" href=\"${escapeXml(cleanAltUrl)}\" />\\n`;\n }\n }\n\n let lastmodValue = entry.lastmod;\n if (options.autoLastmod && !lastmodValue) {\n lastmodValue = nowIso;\n }\n\n if (lastmodValue) {\n const date = lastmodValue instanceof Date ? lastmodValue.toISOString() : lastmodValue;\n xml += ` <lastmod>${date}</lastmod>\\n`;\n }\n\n if (entry.changefreq) {\n xml += ` <changefreq>${entry.changefreq}</changefreq>\\n`;\n }\n\n if (entry.priority !== undefined) {\n xml += ` <priority>${(entry.priority as number).toFixed(1)}</priority>\\n`;\n }\n\n return xml;\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry } from '../../types/sitemap.js';\nimport { escapeXml } from '../../utils/xml-escape.js';\nimport { sanitizeAndValidateUrl } from './url-builder.js';\n\nexport function buildImageXml(images: SitemapEntry['images']): string {\n if (!images?.length) return '';\n\n let xml = '';\n for (const img of images) {\n const cleanImgUrl = sanitizeAndValidateUrl(img.loc, 'image location');\n \n xml += ` <image:image>\\n`;\n xml += ` <image:loc>${escapeXml(cleanImgUrl)}</image:loc>\\n`;\n \n if (img.title && img.title.trim() !== '') {\n xml += ` <image:title>${escapeXml(img.title.trim())}</image:title>\\n`;\n }\n if (img.caption && img.caption.trim() !== '') {\n xml += ` <image:caption>${escapeXml(img.caption.trim())}</image:caption>\\n`;\n }\n \n if (img.geo_location && img.geo_location.trim() !== '') {\n xml += ` <image:geo_location>${escapeXml(img.geo_location.trim())}</image:geo_location>\\n`;\n }\n \n if (img.license) {\n const cleanLicenseUrl = sanitizeAndValidateUrl(img.license, 'image license URL');\n xml += ` <image:license>${escapeXml(cleanLicenseUrl)}</image:license>\\n`;\n }\n \n xml += ` </image:image>\\n`;\n }\n \n return xml;\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry } from '../../types/sitemap.js';\nimport { escapeXml } from '../../utils/xml-escape.js';\nimport { sanitizeAndValidateUrl } from './url-builder.js';\n\nexport function buildVideoXml(videos: SitemapEntry['videos']): string {\n if (!videos?.length) return '';\n\n let xml = '';\n for (const vid of videos) {\n const cleanThumbLoc = sanitizeAndValidateUrl(vid.thumbnail_loc, 'video thumbnail');\n const cleanContentLoc = vid.content_loc ? sanitizeAndValidateUrl(vid.content_loc, 'video content location') : undefined;\n const cleanPlayerLoc = vid.player_loc ? sanitizeAndValidateUrl(vid.player_loc, 'video player location') : undefined;\n\n xml += ` <video:video>\\n`;\n xml += ` <video:thumbnail_loc>${escapeXml(cleanThumbLoc)}</video:thumbnail_loc>\\n`;\n xml += ` <video:title>${escapeXml(vid.title)}</video:title>\\n`;\n xml += ` <video:description>${escapeXml(vid.description)}</video:description>\\n`;\n \n if (cleanContentLoc) xml += ` <video:content_loc>${escapeXml(cleanContentLoc)}</video:content_loc>\\n`;\n if (cleanPlayerLoc) xml += ` <video:player_loc>${escapeXml(cleanPlayerLoc)}</video:player_loc>\\n`;\n \n if (vid.publication_date) {\n const vDate = vid.publication_date instanceof Date ? vid.publication_date.toISOString() : vid.publication_date;\n xml += ` <video:publication_date>${vDate}</video:publication_date>\\n`;\n }\n\n // ✨ Validation et Sérialisation de la durée (0 - 28800s)\n if (vid.duration !== undefined) {\n const finalDuration = Math.floor(vid.duration);\n if (finalDuration < 0 || finalDuration > 28800) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video duration: ${finalDuration}. Duration must be an integer between 0 and 28800 seconds (8 hours).`\n );\n }\n xml += ` <video:duration>${finalDuration}</video:duration>\\n`;\n }\n\n // ✨ Validation et Sérialisation du nombre de vues (>= 0)\n if (vid.view_count !== undefined) {\n const finalViewCount = Math.floor(vid.view_count);\n if (finalViewCount < 0) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video view_count: ${finalViewCount}. View count cannot be negative.`\n );\n }\n xml += ` <video:view_count>${finalViewCount}</video:view_count>\\n`;\n }\n\n if (vid.live) {\n xml += ` <video:live>${vid.live}</video:live>\\n`;\n }\n\n // ✨ Validation et Sérialisation des Restrictions Pays (v1.1.4)\n if (vid.restriction) {\n if (!vid.restriction.countries || vid.restriction.countries.length === 0) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video restriction: countries array cannot be empty.`\n );\n }\n\n const cleanCountries = vid.restriction.countries.map(country => {\n const code = country.trim().toUpperCase();\n if (code.length < 2 || code.length > 3) {\n throw new Error(\n `[next-advanced-sitemap] Invalid ISO country code detected: \"${country}\". Must be a valid ISO 3166 code.`\n );\n }\n return code;\n });\n\n const countriesStr = cleanCountries.join(' ');\n xml += ` <video:restriction relationship=\"${vid.restriction.relationship}\">${countriesStr}</video:restriction>\\n`;\n }\n\n // ✨ Validation et Sérialisation des Plateformes (v1.1.4)\n if (vid.platform) {\n if (!vid.platform.platforms || vid.platform.platforms.length === 0) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video platform: platforms array cannot be empty.`\n );\n }\n\n const validPlatforms = ['web', 'mobile', 'tv'];\n for (const p of vid.platform.platforms) {\n if (!validPlatforms.includes(p)) {\n throw new Error(\n `[next-advanced-sitemap] Invalid platform type: \"${p}\". Allowed values are 'web', 'mobile', or 'tv'.`\n );\n }\n }\n\n const platformsStr = vid.platform.platforms.join(' ');\n xml += ` <video:platform relationship=\"${vid.platform.relationship}\">${platformsStr}</video:platform>\\n`;\n }\n \n if (vid.requires_subscription !== undefined) {\n let subValue: 'yes' | 'no';\n\n if (typeof vid.requires_subscription === 'boolean') {\n subValue = vid.requires_subscription ? 'yes' : 'no';\n } else if (vid.requires_subscription === 'yes' || vid.requires_subscription === 'no') {\n subValue = vid.requires_subscription;\n } else {\n throw new Error(\n `[next-advanced-sitemap] Invalid value for requires_subscription: \"${vid.requires_subscription}\". Expected boolean or strict string 'yes' | 'no'.`\n );\n }\n\n xml += ` <video:requires_subscription>${subValue}</video:requires_subscription>\\n`;\n }\n\n xml += ` </video:video>\\n`;\n }\n \n return xml;\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry } from '../../types/sitemap.js';\nimport { escapeXml } from '../../utils/xml-escape.js';\n\nexport function buildNewsXml(news: SitemapEntry['news']): string {\n if (!news) return '';\n\n const nDate = news.publication_date instanceof Date ? news.publication_date.toISOString() : news.publication_date;\n \n let xml = '';\n xml += ` <news:news>\\n`;\n xml += ` <news:publication>\\n`;\n xml += ` <news:name>${escapeXml(news.name)}</news:name>\\n`;\n xml += ` <news:language>${escapeXml(news.language)}</news:language>\\n`;\n xml += ` </news:publication>\\n`;\n xml += ` <news:publication_date>${nDate}</news:publication_date>\\n`;\n xml += ` <news:title>${escapeXml(news.title)}</news:title>\\n`;\n xml += ` </news:news>\\n`;\n \n return xml;\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry, SitemapOptions } from '../types/sitemap.js';\nimport { buildUrlBaseXml } from './builders/url-builder.js';\nimport { buildImageXml } from './builders/image-builder.js';\nimport { buildVideoXml } from './builders/video-builder.js';\nimport { buildNewsXml } from './builders/news-builder.js';\n\n/**\n * Génère le flux XML complet du sitemap incluant les extensions Images, Vidéos, News et Hreflang.\n * v1.1.4 : Version découplée et hautement modulaire.\n */\nexport function generateXml(entries: SitemapEntry[], options: SitemapOptions = {}): string {\n const now = new Date().toISOString();\n let finalEntries = [...entries];\n\n if (options.sortByPriority) {\n finalEntries.sort((a, b) => {\n const priorityA = a.priority !== undefined ? (a.priority as number) : 0.5;\n const priorityB = b.priority !== undefined ? (b.priority as number) : 0.5;\n return priorityB - priorityA;\n });\n }\n \n let xml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n`;\n xml += `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\\n`;\n xml += ` xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\"\\n`;\n xml += ` xmlns:video=\"http://www.google.com/schemas/sitemap-video/1.1\"\\n`;\n xml += ` xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\"\\n`;\n xml += ` xmlns:xhtml=\"http://www.w3.org/1999/xhtml\">\\n`;\n\n for (const entry of finalEntries) {\n xml += ` <url>\\n`;\n \n // 1. Éléments de base et hreflang alternatifs\n xml += buildUrlBaseXml(entry, options, now);\n\n // 2. Extension Images Google\n xml += buildImageXml(entry.images);\n\n // 3. Extension Vidéos Google (Validations v1.1.3 & v1.1.4 intégrées)\n xml += buildVideoXml(entry.videos);\n\n // 4. Extension News Google\n xml += buildNewsXml(entry.news);\n\n xml += ` </url>\\n`;\n }\n\n xml += `</urlset>`;\n return xml;\n}"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/utils/xml-escape.ts","../src/core/builders/url-builder.ts","../src/core/builders/image-builder.ts","../src/core/builders/video-builder.ts","../src/core/builders/news-builder.ts","../src/core/generator.ts"],"sourcesContent":["/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry, SitemapOptions } from './types/sitemap.js';\nimport { generateXml } from './core/generator.js';\n\nexport * from './types/sitemap.js';\n\n/**\n * Génère une réponse HTTP compatible Next.js (App Router) avec options de configuration.\n * v1.0.9 : Injection dynamique et personnalisable de l'en-tête Cache-Control via l'option maxAge\n * * @param entries - Liste des entrées du sitemap\n * @param options - Options de génération et de mise en cache (ex: autoLastmod, maxAge)\n * @returns Une instance de Response contenant le flux XML configuré\n */\nexport function getServerSitemapResponse(\n entries: SitemapEntry[], \n options: SitemapOptions = {}\n): Response {\n const xml = generateXml(entries, options);\n\n // Détermination de la stratégie de mise en cache (v1.0.9)\n const cacheControlHeader = options.maxAge !== undefined\n ? `public, max-age=${options.maxAge}, must-revalidate`\n : 'public, s-maxage=86400, stale-while-revalidate';\n\n return new Response(xml, {\n headers: {\n 'Content-Type': 'application/xml',\n 'Cache-Control': cacheControlHeader,\n },\n });\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\n/**\n * Convertit les caractères spéciaux en entités XML pour éviter la corruption du fichier.\n * Gère : <, >, &, \", '\n */\nexport function escapeXml(unsafe: string | undefined | null): string {\n if (!unsafe) return '';\n \n return unsafe.replace(/[<>&\"']/g, (c) => {\n switch (c) {\n case '<': return '&lt;';\n case '>': return '&gt;';\n case '&': return '&amp;';\n case '\"': return '&quot;';\n case \"'\": return '&apos;';\n default: return c;\n }\n });\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry, SitemapOptions } from '../../types/sitemap.js';\nimport { escapeXml } from '../../utils/xml-escape.js';\n\n/**\n * Nettoie et valide de manière stricte le format et la structure d'une URL.\n * v1.0.7 : Intégration de l'Auto-Trimming (nettoyage des espaces de début et de fin)\n */\nexport function sanitizeAndValidateUrl(rawUrl: string, context: string): string {\n const url = rawUrl ? rawUrl.trim() : '';\n\n if (!url.startsWith('http://') && !url.startsWith('https://')) {\n throw new Error(\n `[next-advanced-sitemap] Invalid URL in ${context}: \"${url}\". URLs must start with http:// or https://`\n );\n }\n\n if (url.includes(' ')) {\n throw new Error(\n `[next-advanced-sitemap] Malformed URL structure detected in ${context}: \"${url}\". Please verify spaces or special characters.`\n );\n }\n\n let isValid = false;\n if (typeof URL.canParse === 'function') {\n isValid = URL.canParse(url);\n } else {\n try {\n new URL(url);\n isValid = true;\n } catch {\n isValid = false;\n }\n }\n\n if (!isValid) {\n throw new Error(\n `[next-advanced-sitemap] Malformed URL structure detected in ${context}: \"${url}\". Please verify spaces or special characters.`\n );\n }\n\n return url;\n}\n\n/**\n * Génère le bloc XML de base pour un nœud URL (loc, alternates, lastmod, changefreq, priority).\n */\nexport function buildUrlBaseXml(entry: SitemapEntry, options: SitemapOptions, nowIso: string): string {\n let xml = '';\n \n const cleanMainUrl = sanitizeAndValidateUrl(entry.url, 'main entry');\n xml += ` <loc>${escapeXml(cleanMainUrl)}</loc>\\n`;\n\n if (entry.alternates?.length) {\n for (const alt of entry.alternates) {\n const cleanAltUrl = sanitizeAndValidateUrl(alt.href, 'alternate link');\n xml += ` <xhtml:link rel=\"alternate\" hreflang=\"${escapeXml(alt.hreflang)}\" href=\"${escapeXml(cleanAltUrl)}\" />\\n`;\n }\n }\n\n let lastmodValue = entry.lastmod;\n if (options.autoLastmod && !lastmodValue) {\n lastmodValue = nowIso;\n }\n\n if (lastmodValue) {\n const date = lastmodValue instanceof Date ? lastmodValue.toISOString() : lastmodValue;\n xml += ` <lastmod>${date}</lastmod>\\n`;\n }\n\n if (entry.changefreq) {\n xml += ` <changefreq>${entry.changefreq}</changefreq>\\n`;\n }\n\n if (entry.priority !== undefined) {\n xml += ` <priority>${(entry.priority as number).toFixed(1)}</priority>\\n`;\n }\n\n return xml;\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry } from '../../types/sitemap.js';\nimport { escapeXml } from '../../utils/xml-escape.js';\nimport { sanitizeAndValidateUrl } from './url-builder.js';\n\nexport function buildImageXml(images: SitemapEntry['images']): string {\n if (!images?.length) return '';\n\n let xml = '';\n for (const img of images) {\n const cleanImgUrl = sanitizeAndValidateUrl(img.loc, 'image location');\n \n xml += ` <image:image>\\n`;\n xml += ` <image:loc>${escapeXml(cleanImgUrl)}</image:loc>\\n`;\n \n if (img.title && img.title.trim() !== '') {\n xml += ` <image:title>${escapeXml(img.title.trim())}</image:title>\\n`;\n }\n if (img.caption && img.caption.trim() !== '') {\n xml += ` <image:caption>${escapeXml(img.caption.trim())}</image:caption>\\n`;\n }\n \n if (img.geo_location && img.geo_location.trim() !== '') {\n xml += ` <image:geo_location>${escapeXml(img.geo_location.trim())}</image:geo_location>\\n`;\n }\n \n if (img.license) {\n const cleanLicenseUrl = sanitizeAndValidateUrl(img.license, 'image license URL');\n xml += ` <image:license>${escapeXml(cleanLicenseUrl)}</image:license>\\n`;\n }\n \n xml += ` </image:image>\\n`;\n }\n \n return xml;\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry } from '../../types/sitemap.js';\nimport { escapeXml } from '../../utils/xml-escape.js';\nimport { sanitizeAndValidateUrl } from './url-builder.js';\n\nexport function buildVideoXml(videos: SitemapEntry['videos']): string {\n if (!videos?.length) return '';\n\n let xml = '';\n for (const vid of videos) {\n const cleanThumbLoc = sanitizeAndValidateUrl(vid.thumbnail_loc, 'video thumbnail');\n const cleanContentLoc = vid.content_loc ? sanitizeAndValidateUrl(vid.content_loc, 'video content location') : undefined;\n const cleanPlayerLoc = vid.player_loc ? sanitizeAndValidateUrl(vid.player_loc, 'video player location') : undefined;\n\n xml += ` <video:video>\\n`;\n xml += ` <video:thumbnail_loc>${escapeXml(cleanThumbLoc)}</video:thumbnail_loc>\\n`;\n xml += ` <video:title>${escapeXml(vid.title)}</video:title>\\n`;\n xml += ` <video:description>${escapeXml(vid.description)}</video:description>\\n`;\n \n if (cleanContentLoc) xml += ` <video:content_loc>${escapeXml(cleanContentLoc)}</video:content_loc>\\n`;\n if (cleanPlayerLoc) xml += ` <video:player_loc>${escapeXml(cleanPlayerLoc)}</video:player_loc>\\n`;\n \n if (vid.publication_date) {\n const vDate = vid.publication_date instanceof Date ? vid.publication_date.toISOString() : vid.publication_date;\n xml += ` <video:publication_date>${vDate}</video:publication_date>\\n`;\n }\n\n // ✨ Validation et Sérialisation de la durée (0 - 28800s)\n if (vid.duration !== undefined) {\n const finalDuration = Math.floor(vid.duration);\n if (finalDuration < 0 || finalDuration > 28800) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video duration: ${finalDuration}. Duration must be an integer between 0 and 28800 seconds (8 hours).`\n );\n }\n xml += ` <video:duration>${finalDuration}</video:duration>\\n`;\n }\n\n // ✨ Validation et Sérialisation du nombre de vues (>= 0)\n if (vid.view_count !== undefined) {\n const finalViewCount = Math.floor(vid.view_count);\n if (finalViewCount < 0) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video view_count: ${finalViewCount}. View count cannot be negative.`\n );\n }\n xml += ` <video:view_count>${finalViewCount}</video:view_count>\\n`;\n }\n\n if (vid.live) {\n xml += ` <video:live>${vid.live}</video:live>\\n`;\n }\n\n // ✨ Validation et Sérialisation des Restrictions Pays (v1.1.4)\n if (vid.restriction) {\n if (!vid.restriction.countries || vid.restriction.countries.length === 0) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video restriction: countries array cannot be empty.`\n );\n }\n\n const cleanCountries = vid.restriction.countries.map(country => {\n const code = country.trim().toUpperCase();\n if (code.length < 2 || code.length > 3) {\n throw new Error(\n `[next-advanced-sitemap] Invalid ISO country code detected: \"${country}\". Must be a valid ISO 3166 code.`\n );\n }\n return code;\n });\n\n const countriesStr = cleanCountries.join(' ');\n xml += ` <video:restriction relationship=\"${vid.restriction.relationship}\">${countriesStr}</video:restriction>\\n`;\n }\n\n // ✨ Validation et Sérialisation des Plateformes (v1.1.4)\n if (vid.platform) {\n if (!vid.platform.platforms || vid.platform.platforms.length === 0) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video platform: platforms array cannot be empty.`\n );\n }\n\n const validPlatforms = ['web', 'mobile', 'tv'];\n for (const p of vid.platform.platforms) {\n if (!validPlatforms.includes(p)) {\n throw new Error(\n `[next-advanced-sitemap] Invalid platform type: \"${p}\". Allowed values are 'web', 'mobile', or 'tv'.`\n );\n }\n }\n\n const platformsStr = vid.platform.platforms.join(' ');\n xml += ` <video:platform relationship=\"${vid.platform.relationship}\">${platformsStr}</video:platform>\\n`;\n }\n \n if (vid.requires_subscription !== undefined) {\n let subValue: 'yes' | 'no';\n\n if (typeof vid.requires_subscription === 'boolean') {\n subValue = vid.requires_subscription ? 'yes' : 'no';\n } else if (vid.requires_subscription === 'yes' || vid.requires_subscription === 'no') {\n subValue = vid.requires_subscription;\n } else {\n throw new Error(\n `[next-advanced-sitemap] Invalid value for requires_subscription: \"${vid.requires_subscription}\". Expected boolean or strict string 'yes' | 'no'.`\n );\n }\n\n xml += ` <video:requires_subscription>${subValue}</video:requires_subscription>\\n`;\n }\n\n // ✨ Validation et Sérialisation des Prix et Achats (v1.1.6)\n if (vid.price) {\n const { value, currency, type } = vid.price;\n\n if (value === undefined || value < 0) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video price value: \"${value}\". Value must be a positive number.`\n );\n }\n\n const cleanCurrency = currency ? currency.trim().toUpperCase() : '';\n if (cleanCurrency.length !== 3) {\n throw new Error(\n `[next-advanced-sitemap] Invalid ISO 4217 currency code: \"${currency}\". Currency must be exactly a 3-letter code.`\n );\n }\n\n let priceXml = ` <video:price currency=\"${cleanCurrency}\"`;\n if (type) {\n if (type !== 'rent' && type !== 'own') {\n throw new Error(\n `[next-advanced-sitemap] Invalid price type: \"${type}\". Allowed values are 'rent' or 'own'.`\n );\n }\n priceXml += ` type=\"${type}\"`;\n }\n priceXml += `>${value.toFixed(2)}</video:price>\\n`;\n \n xml += priceXml;\n }\n\n // ✨ Validation et Sérialisation de la Catégorie (v1.1.7)\n if (vid.category !== undefined) {\n const cleanCategory = vid.category.trim();\n if (!cleanCategory) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video category: category cannot be empty or just whitespaces.`\n );\n }\n if (cleanCategory.length > 256) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video category length: ${cleanCategory.length}. Maximum allowed is 256 characters.`\n );\n }\n xml += ` <video:category>${escapeXml(cleanCategory)}</video:category>\\n`;\n }\n\n // ✨ Validation et Sérialisation des Tags (v1.1.7)\n if (vid.tags) {\n if (vid.tags.length > 32) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video tags count: ${vid.tags.length}. A video can have a maximum of 32 tags.`\n );\n }\n\n for (const tag of vid.tags) {\n const cleanTag = tag.trim();\n if (!cleanTag) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video tag detected: tag cannot be empty or just whitespaces.`\n );\n }\n xml += ` <video:tag>${escapeXml(cleanTag)}</video:tag>\\n`;\n }\n }\n\n xml += ` </video:video>\\n`;\n }\n \n return xml;\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry } from '../../types/sitemap.js';\nimport { escapeXml } from '../../utils/xml-escape.js';\n\nexport function buildNewsXml(news: SitemapEntry['news']): string {\n if (!news) return '';\n\n const nDate = news.publication_date instanceof Date ? news.publication_date.toISOString() : news.publication_date;\n \n let xml = '';\n xml += ` <news:news>\\n`;\n xml += ` <news:publication>\\n`;\n xml += ` <news:name>${escapeXml(news.name)}</news:name>\\n`;\n xml += ` <news:language>${escapeXml(news.language)}</news:language>\\n`;\n xml += ` </news:publication>\\n`;\n xml += ` <news:publication_date>${nDate}</news:publication_date>\\n`;\n xml += ` <news:title>${escapeXml(news.title)}</news:title>\\n`;\n xml += ` </news:news>\\n`;\n \n return xml;\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry, SitemapOptions } from '../types/sitemap.js';\nimport { buildUrlBaseXml } from './builders/url-builder.js';\nimport { buildImageXml } from './builders/image-builder.js';\nimport { buildVideoXml } from './builders/video-builder.js';\nimport { buildNewsXml } from './builders/news-builder.js';\n\n/**\n * Génère le flux XML complet du sitemap incluant les extensions Images, Vidéos, News et Hreflang.\n * v1.1.4 : Version découplée et hautement modulaire.\n */\nexport function generateXml(entries: SitemapEntry[], options: SitemapOptions = {}): string {\n const now = new Date().toISOString();\n let finalEntries = [...entries];\n\n if (options.sortByPriority) {\n finalEntries.sort((a, b) => {\n const priorityA = a.priority !== undefined ? (a.priority as number) : 0.5;\n const priorityB = b.priority !== undefined ? (b.priority as number) : 0.5;\n return priorityB - priorityA;\n });\n }\n \n let xml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n`;\n xml += `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\\n`;\n xml += ` xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\"\\n`;\n xml += ` xmlns:video=\"http://www.google.com/schemas/sitemap-video/1.1\"\\n`;\n xml += ` xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\"\\n`;\n xml += ` xmlns:xhtml=\"http://www.w3.org/1999/xhtml\">\\n`;\n\n for (const entry of finalEntries) {\n xml += ` <url>\\n`;\n \n // 1. Éléments de base et hreflang alternatifs\n xml += buildUrlBaseXml(entry, options, now);\n\n // 2. Extension Images Google\n xml += buildImageXml(entry.images);\n\n // 3. Extension Vidéos Google (Validations v1.1.3 & v1.1.4 intégrées)\n xml += buildVideoXml(entry.videos);\n\n // 4. Extension News Google\n xml += buildNewsXml(entry.news);\n\n xml += ` </url>\\n`;\n }\n\n xml += `</urlset>`;\n return xml;\n}"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -45,6 +45,14 @@ interface VideoPlatform {
45
45
  /** Tableau de plateformes autorisées ou interdites */
46
46
  platforms: ('web' | 'mobile' | 'tv')[];
47
47
  }
48
+ interface VideoPrice {
49
+ /** Valeur numérique du prix (ex: 9.99) */
50
+ value: number;
51
+ /** Code de devise ISO 4217 à 3 lettres (ex: 'USD', 'EUR', 'CDF') */
52
+ currency: string;
53
+ /** Optionnel : Type de transaction, soit 'rent' (location) ou 'own' (achat) */
54
+ type?: 'rent' | 'own';
55
+ }
48
56
  /**
49
57
  * Interface pour les vidéos dans le sitemap
50
58
  * @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/video-sitemaps
@@ -73,6 +81,21 @@ interface SitemapVideo {
73
81
  * @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/video-sitemaps
74
82
  */
75
83
  requires_subscription?: boolean | 'yes' | 'no';
84
+ /**
85
+ * v1.1.6 : Tarification de la vidéo pour l'achat ou la location (VOD).
86
+ * @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/video-sitemaps
87
+ */
88
+ price?: VideoPrice;
89
+ /**
90
+ * ✨ v1.1.7 : Catégorie thématique générale de la vidéo (ex: 'Éducation', 'Technologie').
91
+ * Chaîne de caractères de 256 caractères maximum.
92
+ */
93
+ category?: string;
94
+ /**
95
+ * ✨ v1.1.7 : Mots-clés décrivant la vidéo.
96
+ * Tableau de chaînes de caractères, limité à 32 tags maximum par vidéo.
97
+ */
98
+ tags?: string[];
76
99
  }
77
100
  /**
78
101
  * Interface pour Google News
@@ -129,4 +152,4 @@ interface SitemapOptions {
129
152
  */
130
153
  declare function getServerSitemapResponse(entries: SitemapEntry[], options?: SitemapOptions): Response;
131
154
 
132
- export { type SitemapAlternate, type SitemapChangeFreq, type SitemapEntry, type SitemapImage, type SitemapNews, type SitemapOptions, type SitemapPriority, type SitemapVideo, type VideoPlatform, type VideoRestriction, getServerSitemapResponse };
155
+ export { type SitemapAlternate, type SitemapChangeFreq, type SitemapEntry, type SitemapImage, type SitemapNews, type SitemapOptions, type SitemapPriority, type SitemapVideo, type VideoPlatform, type VideoPrice, type VideoRestriction, getServerSitemapResponse };
package/dist/index.d.ts CHANGED
@@ -45,6 +45,14 @@ interface VideoPlatform {
45
45
  /** Tableau de plateformes autorisées ou interdites */
46
46
  platforms: ('web' | 'mobile' | 'tv')[];
47
47
  }
48
+ interface VideoPrice {
49
+ /** Valeur numérique du prix (ex: 9.99) */
50
+ value: number;
51
+ /** Code de devise ISO 4217 à 3 lettres (ex: 'USD', 'EUR', 'CDF') */
52
+ currency: string;
53
+ /** Optionnel : Type de transaction, soit 'rent' (location) ou 'own' (achat) */
54
+ type?: 'rent' | 'own';
55
+ }
48
56
  /**
49
57
  * Interface pour les vidéos dans le sitemap
50
58
  * @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/video-sitemaps
@@ -73,6 +81,21 @@ interface SitemapVideo {
73
81
  * @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/video-sitemaps
74
82
  */
75
83
  requires_subscription?: boolean | 'yes' | 'no';
84
+ /**
85
+ * v1.1.6 : Tarification de la vidéo pour l'achat ou la location (VOD).
86
+ * @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/video-sitemaps
87
+ */
88
+ price?: VideoPrice;
89
+ /**
90
+ * ✨ v1.1.7 : Catégorie thématique générale de la vidéo (ex: 'Éducation', 'Technologie').
91
+ * Chaîne de caractères de 256 caractères maximum.
92
+ */
93
+ category?: string;
94
+ /**
95
+ * ✨ v1.1.7 : Mots-clés décrivant la vidéo.
96
+ * Tableau de chaînes de caractères, limité à 32 tags maximum par vidéo.
97
+ */
98
+ tags?: string[];
76
99
  }
77
100
  /**
78
101
  * Interface pour Google News
@@ -129,4 +152,4 @@ interface SitemapOptions {
129
152
  */
130
153
  declare function getServerSitemapResponse(entries: SitemapEntry[], options?: SitemapOptions): Response;
131
154
 
132
- export { type SitemapAlternate, type SitemapChangeFreq, type SitemapEntry, type SitemapImage, type SitemapNews, type SitemapOptions, type SitemapPriority, type SitemapVideo, type VideoPlatform, type VideoRestriction, getServerSitemapResponse };
155
+ export { type SitemapAlternate, type SitemapChangeFreq, type SitemapEntry, type SitemapImage, type SitemapNews, type SitemapOptions, type SitemapPriority, type SitemapVideo, type VideoPlatform, type VideoPrice, type VideoRestriction, getServerSitemapResponse };
package/dist/index.js CHANGED
@@ -215,6 +215,64 @@ function buildVideoXml(videos) {
215
215
  xml += ` <video:requires_subscription>${subValue}</video:requires_subscription>
216
216
  `;
217
217
  }
218
+ if (vid.price) {
219
+ const { value, currency, type } = vid.price;
220
+ if (value === void 0 || value < 0) {
221
+ throw new Error(
222
+ `[next-advanced-sitemap] Invalid video price value: "${value}". Value must be a positive number.`
223
+ );
224
+ }
225
+ const cleanCurrency = currency ? currency.trim().toUpperCase() : "";
226
+ if (cleanCurrency.length !== 3) {
227
+ throw new Error(
228
+ `[next-advanced-sitemap] Invalid ISO 4217 currency code: "${currency}". Currency must be exactly a 3-letter code.`
229
+ );
230
+ }
231
+ let priceXml = ` <video:price currency="${cleanCurrency}"`;
232
+ if (type) {
233
+ if (type !== "rent" && type !== "own") {
234
+ throw new Error(
235
+ `[next-advanced-sitemap] Invalid price type: "${type}". Allowed values are 'rent' or 'own'.`
236
+ );
237
+ }
238
+ priceXml += ` type="${type}"`;
239
+ }
240
+ priceXml += `>${value.toFixed(2)}</video:price>
241
+ `;
242
+ xml += priceXml;
243
+ }
244
+ if (vid.category !== void 0) {
245
+ const cleanCategory = vid.category.trim();
246
+ if (!cleanCategory) {
247
+ throw new Error(
248
+ `[next-advanced-sitemap] Invalid video category: category cannot be empty or just whitespaces.`
249
+ );
250
+ }
251
+ if (cleanCategory.length > 256) {
252
+ throw new Error(
253
+ `[next-advanced-sitemap] Invalid video category length: ${cleanCategory.length}. Maximum allowed is 256 characters.`
254
+ );
255
+ }
256
+ xml += ` <video:category>${escapeXml(cleanCategory)}</video:category>
257
+ `;
258
+ }
259
+ if (vid.tags) {
260
+ if (vid.tags.length > 32) {
261
+ throw new Error(
262
+ `[next-advanced-sitemap] Invalid video tags count: ${vid.tags.length}. A video can have a maximum of 32 tags.`
263
+ );
264
+ }
265
+ for (const tag of vid.tags) {
266
+ const cleanTag = tag.trim();
267
+ if (!cleanTag) {
268
+ throw new Error(
269
+ `[next-advanced-sitemap] Invalid video tag detected: tag cannot be empty or just whitespaces.`
270
+ );
271
+ }
272
+ xml += ` <video:tag>${escapeXml(cleanTag)}</video:tag>
273
+ `;
274
+ }
275
+ }
218
276
  xml += ` </video:video>
219
277
  `;
220
278
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/utils/xml-escape.ts","../src/core/builders/url-builder.ts","../src/core/builders/image-builder.ts","../src/core/builders/video-builder.ts","../src/core/builders/news-builder.ts","../src/core/generator.ts","../src/index.ts"],"sourcesContent":["/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\n/**\n * Convertit les caractères spéciaux en entités XML pour éviter la corruption du fichier.\n * Gère : <, >, &, \", '\n */\nexport function escapeXml(unsafe: string | undefined | null): string {\n if (!unsafe) return '';\n \n return unsafe.replace(/[<>&\"']/g, (c) => {\n switch (c) {\n case '<': return '&lt;';\n case '>': return '&gt;';\n case '&': return '&amp;';\n case '\"': return '&quot;';\n case \"'\": return '&apos;';\n default: return c;\n }\n });\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry, SitemapOptions } from '../../types/sitemap.js';\nimport { escapeXml } from '../../utils/xml-escape.js';\n\n/**\n * Nettoie et valide de manière stricte le format et la structure d'une URL.\n * v1.0.7 : Intégration de l'Auto-Trimming (nettoyage des espaces de début et de fin)\n */\nexport function sanitizeAndValidateUrl(rawUrl: string, context: string): string {\n const url = rawUrl ? rawUrl.trim() : '';\n\n if (!url.startsWith('http://') && !url.startsWith('https://')) {\n throw new Error(\n `[next-advanced-sitemap] Invalid URL in ${context}: \"${url}\". URLs must start with http:// or https://`\n );\n }\n\n if (url.includes(' ')) {\n throw new Error(\n `[next-advanced-sitemap] Malformed URL structure detected in ${context}: \"${url}\". Please verify spaces or special characters.`\n );\n }\n\n let isValid = false;\n if (typeof URL.canParse === 'function') {\n isValid = URL.canParse(url);\n } else {\n try {\n new URL(url);\n isValid = true;\n } catch {\n isValid = false;\n }\n }\n\n if (!isValid) {\n throw new Error(\n `[next-advanced-sitemap] Malformed URL structure detected in ${context}: \"${url}\". Please verify spaces or special characters.`\n );\n }\n\n return url;\n}\n\n/**\n * Génère le bloc XML de base pour un nœud URL (loc, alternates, lastmod, changefreq, priority).\n */\nexport function buildUrlBaseXml(entry: SitemapEntry, options: SitemapOptions, nowIso: string): string {\n let xml = '';\n \n const cleanMainUrl = sanitizeAndValidateUrl(entry.url, 'main entry');\n xml += ` <loc>${escapeXml(cleanMainUrl)}</loc>\\n`;\n\n if (entry.alternates?.length) {\n for (const alt of entry.alternates) {\n const cleanAltUrl = sanitizeAndValidateUrl(alt.href, 'alternate link');\n xml += ` <xhtml:link rel=\"alternate\" hreflang=\"${escapeXml(alt.hreflang)}\" href=\"${escapeXml(cleanAltUrl)}\" />\\n`;\n }\n }\n\n let lastmodValue = entry.lastmod;\n if (options.autoLastmod && !lastmodValue) {\n lastmodValue = nowIso;\n }\n\n if (lastmodValue) {\n const date = lastmodValue instanceof Date ? lastmodValue.toISOString() : lastmodValue;\n xml += ` <lastmod>${date}</lastmod>\\n`;\n }\n\n if (entry.changefreq) {\n xml += ` <changefreq>${entry.changefreq}</changefreq>\\n`;\n }\n\n if (entry.priority !== undefined) {\n xml += ` <priority>${(entry.priority as number).toFixed(1)}</priority>\\n`;\n }\n\n return xml;\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry } from '../../types/sitemap.js';\nimport { escapeXml } from '../../utils/xml-escape.js';\nimport { sanitizeAndValidateUrl } from './url-builder.js';\n\nexport function buildImageXml(images: SitemapEntry['images']): string {\n if (!images?.length) return '';\n\n let xml = '';\n for (const img of images) {\n const cleanImgUrl = sanitizeAndValidateUrl(img.loc, 'image location');\n \n xml += ` <image:image>\\n`;\n xml += ` <image:loc>${escapeXml(cleanImgUrl)}</image:loc>\\n`;\n \n if (img.title && img.title.trim() !== '') {\n xml += ` <image:title>${escapeXml(img.title.trim())}</image:title>\\n`;\n }\n if (img.caption && img.caption.trim() !== '') {\n xml += ` <image:caption>${escapeXml(img.caption.trim())}</image:caption>\\n`;\n }\n \n if (img.geo_location && img.geo_location.trim() !== '') {\n xml += ` <image:geo_location>${escapeXml(img.geo_location.trim())}</image:geo_location>\\n`;\n }\n \n if (img.license) {\n const cleanLicenseUrl = sanitizeAndValidateUrl(img.license, 'image license URL');\n xml += ` <image:license>${escapeXml(cleanLicenseUrl)}</image:license>\\n`;\n }\n \n xml += ` </image:image>\\n`;\n }\n \n return xml;\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry } from '../../types/sitemap.js';\nimport { escapeXml } from '../../utils/xml-escape.js';\nimport { sanitizeAndValidateUrl } from './url-builder.js';\n\nexport function buildVideoXml(videos: SitemapEntry['videos']): string {\n if (!videos?.length) return '';\n\n let xml = '';\n for (const vid of videos) {\n const cleanThumbLoc = sanitizeAndValidateUrl(vid.thumbnail_loc, 'video thumbnail');\n const cleanContentLoc = vid.content_loc ? sanitizeAndValidateUrl(vid.content_loc, 'video content location') : undefined;\n const cleanPlayerLoc = vid.player_loc ? sanitizeAndValidateUrl(vid.player_loc, 'video player location') : undefined;\n\n xml += ` <video:video>\\n`;\n xml += ` <video:thumbnail_loc>${escapeXml(cleanThumbLoc)}</video:thumbnail_loc>\\n`;\n xml += ` <video:title>${escapeXml(vid.title)}</video:title>\\n`;\n xml += ` <video:description>${escapeXml(vid.description)}</video:description>\\n`;\n \n if (cleanContentLoc) xml += ` <video:content_loc>${escapeXml(cleanContentLoc)}</video:content_loc>\\n`;\n if (cleanPlayerLoc) xml += ` <video:player_loc>${escapeXml(cleanPlayerLoc)}</video:player_loc>\\n`;\n \n if (vid.publication_date) {\n const vDate = vid.publication_date instanceof Date ? vid.publication_date.toISOString() : vid.publication_date;\n xml += ` <video:publication_date>${vDate}</video:publication_date>\\n`;\n }\n\n // ✨ Validation et Sérialisation de la durée (0 - 28800s)\n if (vid.duration !== undefined) {\n const finalDuration = Math.floor(vid.duration);\n if (finalDuration < 0 || finalDuration > 28800) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video duration: ${finalDuration}. Duration must be an integer between 0 and 28800 seconds (8 hours).`\n );\n }\n xml += ` <video:duration>${finalDuration}</video:duration>\\n`;\n }\n\n // ✨ Validation et Sérialisation du nombre de vues (>= 0)\n if (vid.view_count !== undefined) {\n const finalViewCount = Math.floor(vid.view_count);\n if (finalViewCount < 0) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video view_count: ${finalViewCount}. View count cannot be negative.`\n );\n }\n xml += ` <video:view_count>${finalViewCount}</video:view_count>\\n`;\n }\n\n if (vid.live) {\n xml += ` <video:live>${vid.live}</video:live>\\n`;\n }\n\n // ✨ Validation et Sérialisation des Restrictions Pays (v1.1.4)\n if (vid.restriction) {\n if (!vid.restriction.countries || vid.restriction.countries.length === 0) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video restriction: countries array cannot be empty.`\n );\n }\n\n const cleanCountries = vid.restriction.countries.map(country => {\n const code = country.trim().toUpperCase();\n if (code.length < 2 || code.length > 3) {\n throw new Error(\n `[next-advanced-sitemap] Invalid ISO country code detected: \"${country}\". Must be a valid ISO 3166 code.`\n );\n }\n return code;\n });\n\n const countriesStr = cleanCountries.join(' ');\n xml += ` <video:restriction relationship=\"${vid.restriction.relationship}\">${countriesStr}</video:restriction>\\n`;\n }\n\n // ✨ Validation et Sérialisation des Plateformes (v1.1.4)\n if (vid.platform) {\n if (!vid.platform.platforms || vid.platform.platforms.length === 0) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video platform: platforms array cannot be empty.`\n );\n }\n\n const validPlatforms = ['web', 'mobile', 'tv'];\n for (const p of vid.platform.platforms) {\n if (!validPlatforms.includes(p)) {\n throw new Error(\n `[next-advanced-sitemap] Invalid platform type: \"${p}\". Allowed values are 'web', 'mobile', or 'tv'.`\n );\n }\n }\n\n const platformsStr = vid.platform.platforms.join(' ');\n xml += ` <video:platform relationship=\"${vid.platform.relationship}\">${platformsStr}</video:platform>\\n`;\n }\n \n if (vid.requires_subscription !== undefined) {\n let subValue: 'yes' | 'no';\n\n if (typeof vid.requires_subscription === 'boolean') {\n subValue = vid.requires_subscription ? 'yes' : 'no';\n } else if (vid.requires_subscription === 'yes' || vid.requires_subscription === 'no') {\n subValue = vid.requires_subscription;\n } else {\n throw new Error(\n `[next-advanced-sitemap] Invalid value for requires_subscription: \"${vid.requires_subscription}\". Expected boolean or strict string 'yes' | 'no'.`\n );\n }\n\n xml += ` <video:requires_subscription>${subValue}</video:requires_subscription>\\n`;\n }\n\n xml += ` </video:video>\\n`;\n }\n \n return xml;\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry } from '../../types/sitemap.js';\nimport { escapeXml } from '../../utils/xml-escape.js';\n\nexport function buildNewsXml(news: SitemapEntry['news']): string {\n if (!news) return '';\n\n const nDate = news.publication_date instanceof Date ? news.publication_date.toISOString() : news.publication_date;\n \n let xml = '';\n xml += ` <news:news>\\n`;\n xml += ` <news:publication>\\n`;\n xml += ` <news:name>${escapeXml(news.name)}</news:name>\\n`;\n xml += ` <news:language>${escapeXml(news.language)}</news:language>\\n`;\n xml += ` </news:publication>\\n`;\n xml += ` <news:publication_date>${nDate}</news:publication_date>\\n`;\n xml += ` <news:title>${escapeXml(news.title)}</news:title>\\n`;\n xml += ` </news:news>\\n`;\n \n return xml;\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry, SitemapOptions } from '../types/sitemap.js';\nimport { buildUrlBaseXml } from './builders/url-builder.js';\nimport { buildImageXml } from './builders/image-builder.js';\nimport { buildVideoXml } from './builders/video-builder.js';\nimport { buildNewsXml } from './builders/news-builder.js';\n\n/**\n * Génère le flux XML complet du sitemap incluant les extensions Images, Vidéos, News et Hreflang.\n * v1.1.4 : Version découplée et hautement modulaire.\n */\nexport function generateXml(entries: SitemapEntry[], options: SitemapOptions = {}): string {\n const now = new Date().toISOString();\n let finalEntries = [...entries];\n\n if (options.sortByPriority) {\n finalEntries.sort((a, b) => {\n const priorityA = a.priority !== undefined ? (a.priority as number) : 0.5;\n const priorityB = b.priority !== undefined ? (b.priority as number) : 0.5;\n return priorityB - priorityA;\n });\n }\n \n let xml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n`;\n xml += `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\\n`;\n xml += ` xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\"\\n`;\n xml += ` xmlns:video=\"http://www.google.com/schemas/sitemap-video/1.1\"\\n`;\n xml += ` xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\"\\n`;\n xml += ` xmlns:xhtml=\"http://www.w3.org/1999/xhtml\">\\n`;\n\n for (const entry of finalEntries) {\n xml += ` <url>\\n`;\n \n // 1. Éléments de base et hreflang alternatifs\n xml += buildUrlBaseXml(entry, options, now);\n\n // 2. Extension Images Google\n xml += buildImageXml(entry.images);\n\n // 3. Extension Vidéos Google (Validations v1.1.3 & v1.1.4 intégrées)\n xml += buildVideoXml(entry.videos);\n\n // 4. Extension News Google\n xml += buildNewsXml(entry.news);\n\n xml += ` </url>\\n`;\n }\n\n xml += `</urlset>`;\n return xml;\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry, SitemapOptions } from './types/sitemap.js';\nimport { generateXml } from './core/generator.js';\n\nexport * from './types/sitemap.js';\n\n/**\n * Génère une réponse HTTP compatible Next.js (App Router) avec options de configuration.\n * v1.0.9 : Injection dynamique et personnalisable de l'en-tête Cache-Control via l'option maxAge\n * * @param entries - Liste des entrées du sitemap\n * @param options - Options de génération et de mise en cache (ex: autoLastmod, maxAge)\n * @returns Une instance de Response contenant le flux XML configuré\n */\nexport function getServerSitemapResponse(\n entries: SitemapEntry[], \n options: SitemapOptions = {}\n): Response {\n const xml = generateXml(entries, options);\n\n // Détermination de la stratégie de mise en cache (v1.0.9)\n const cacheControlHeader = options.maxAge !== undefined\n ? `public, max-age=${options.maxAge}, must-revalidate`\n : 'public, s-maxage=86400, stale-while-revalidate';\n\n return new Response(xml, {\n headers: {\n 'Content-Type': 'application/xml',\n 'Cache-Control': cacheControlHeader,\n },\n });\n}"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/utils/xml-escape.ts","../src/core/builders/url-builder.ts","../src/core/builders/image-builder.ts","../src/core/builders/video-builder.ts","../src/core/builders/news-builder.ts","../src/core/generator.ts","../src/index.ts"],"sourcesContent":["/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\n/**\n * Convertit les caractères spéciaux en entités XML pour éviter la corruption du fichier.\n * Gère : <, >, &, \", '\n */\nexport function escapeXml(unsafe: string | undefined | null): string {\n if (!unsafe) return '';\n \n return unsafe.replace(/[<>&\"']/g, (c) => {\n switch (c) {\n case '<': return '&lt;';\n case '>': return '&gt;';\n case '&': return '&amp;';\n case '\"': return '&quot;';\n case \"'\": return '&apos;';\n default: return c;\n }\n });\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry, SitemapOptions } from '../../types/sitemap.js';\nimport { escapeXml } from '../../utils/xml-escape.js';\n\n/**\n * Nettoie et valide de manière stricte le format et la structure d'une URL.\n * v1.0.7 : Intégration de l'Auto-Trimming (nettoyage des espaces de début et de fin)\n */\nexport function sanitizeAndValidateUrl(rawUrl: string, context: string): string {\n const url = rawUrl ? rawUrl.trim() : '';\n\n if (!url.startsWith('http://') && !url.startsWith('https://')) {\n throw new Error(\n `[next-advanced-sitemap] Invalid URL in ${context}: \"${url}\". URLs must start with http:// or https://`\n );\n }\n\n if (url.includes(' ')) {\n throw new Error(\n `[next-advanced-sitemap] Malformed URL structure detected in ${context}: \"${url}\". Please verify spaces or special characters.`\n );\n }\n\n let isValid = false;\n if (typeof URL.canParse === 'function') {\n isValid = URL.canParse(url);\n } else {\n try {\n new URL(url);\n isValid = true;\n } catch {\n isValid = false;\n }\n }\n\n if (!isValid) {\n throw new Error(\n `[next-advanced-sitemap] Malformed URL structure detected in ${context}: \"${url}\". Please verify spaces or special characters.`\n );\n }\n\n return url;\n}\n\n/**\n * Génère le bloc XML de base pour un nœud URL (loc, alternates, lastmod, changefreq, priority).\n */\nexport function buildUrlBaseXml(entry: SitemapEntry, options: SitemapOptions, nowIso: string): string {\n let xml = '';\n \n const cleanMainUrl = sanitizeAndValidateUrl(entry.url, 'main entry');\n xml += ` <loc>${escapeXml(cleanMainUrl)}</loc>\\n`;\n\n if (entry.alternates?.length) {\n for (const alt of entry.alternates) {\n const cleanAltUrl = sanitizeAndValidateUrl(alt.href, 'alternate link');\n xml += ` <xhtml:link rel=\"alternate\" hreflang=\"${escapeXml(alt.hreflang)}\" href=\"${escapeXml(cleanAltUrl)}\" />\\n`;\n }\n }\n\n let lastmodValue = entry.lastmod;\n if (options.autoLastmod && !lastmodValue) {\n lastmodValue = nowIso;\n }\n\n if (lastmodValue) {\n const date = lastmodValue instanceof Date ? lastmodValue.toISOString() : lastmodValue;\n xml += ` <lastmod>${date}</lastmod>\\n`;\n }\n\n if (entry.changefreq) {\n xml += ` <changefreq>${entry.changefreq}</changefreq>\\n`;\n }\n\n if (entry.priority !== undefined) {\n xml += ` <priority>${(entry.priority as number).toFixed(1)}</priority>\\n`;\n }\n\n return xml;\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry } from '../../types/sitemap.js';\nimport { escapeXml } from '../../utils/xml-escape.js';\nimport { sanitizeAndValidateUrl } from './url-builder.js';\n\nexport function buildImageXml(images: SitemapEntry['images']): string {\n if (!images?.length) return '';\n\n let xml = '';\n for (const img of images) {\n const cleanImgUrl = sanitizeAndValidateUrl(img.loc, 'image location');\n \n xml += ` <image:image>\\n`;\n xml += ` <image:loc>${escapeXml(cleanImgUrl)}</image:loc>\\n`;\n \n if (img.title && img.title.trim() !== '') {\n xml += ` <image:title>${escapeXml(img.title.trim())}</image:title>\\n`;\n }\n if (img.caption && img.caption.trim() !== '') {\n xml += ` <image:caption>${escapeXml(img.caption.trim())}</image:caption>\\n`;\n }\n \n if (img.geo_location && img.geo_location.trim() !== '') {\n xml += ` <image:geo_location>${escapeXml(img.geo_location.trim())}</image:geo_location>\\n`;\n }\n \n if (img.license) {\n const cleanLicenseUrl = sanitizeAndValidateUrl(img.license, 'image license URL');\n xml += ` <image:license>${escapeXml(cleanLicenseUrl)}</image:license>\\n`;\n }\n \n xml += ` </image:image>\\n`;\n }\n \n return xml;\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry } from '../../types/sitemap.js';\nimport { escapeXml } from '../../utils/xml-escape.js';\nimport { sanitizeAndValidateUrl } from './url-builder.js';\n\nexport function buildVideoXml(videos: SitemapEntry['videos']): string {\n if (!videos?.length) return '';\n\n let xml = '';\n for (const vid of videos) {\n const cleanThumbLoc = sanitizeAndValidateUrl(vid.thumbnail_loc, 'video thumbnail');\n const cleanContentLoc = vid.content_loc ? sanitizeAndValidateUrl(vid.content_loc, 'video content location') : undefined;\n const cleanPlayerLoc = vid.player_loc ? sanitizeAndValidateUrl(vid.player_loc, 'video player location') : undefined;\n\n xml += ` <video:video>\\n`;\n xml += ` <video:thumbnail_loc>${escapeXml(cleanThumbLoc)}</video:thumbnail_loc>\\n`;\n xml += ` <video:title>${escapeXml(vid.title)}</video:title>\\n`;\n xml += ` <video:description>${escapeXml(vid.description)}</video:description>\\n`;\n \n if (cleanContentLoc) xml += ` <video:content_loc>${escapeXml(cleanContentLoc)}</video:content_loc>\\n`;\n if (cleanPlayerLoc) xml += ` <video:player_loc>${escapeXml(cleanPlayerLoc)}</video:player_loc>\\n`;\n \n if (vid.publication_date) {\n const vDate = vid.publication_date instanceof Date ? vid.publication_date.toISOString() : vid.publication_date;\n xml += ` <video:publication_date>${vDate}</video:publication_date>\\n`;\n }\n\n // ✨ Validation et Sérialisation de la durée (0 - 28800s)\n if (vid.duration !== undefined) {\n const finalDuration = Math.floor(vid.duration);\n if (finalDuration < 0 || finalDuration > 28800) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video duration: ${finalDuration}. Duration must be an integer between 0 and 28800 seconds (8 hours).`\n );\n }\n xml += ` <video:duration>${finalDuration}</video:duration>\\n`;\n }\n\n // ✨ Validation et Sérialisation du nombre de vues (>= 0)\n if (vid.view_count !== undefined) {\n const finalViewCount = Math.floor(vid.view_count);\n if (finalViewCount < 0) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video view_count: ${finalViewCount}. View count cannot be negative.`\n );\n }\n xml += ` <video:view_count>${finalViewCount}</video:view_count>\\n`;\n }\n\n if (vid.live) {\n xml += ` <video:live>${vid.live}</video:live>\\n`;\n }\n\n // ✨ Validation et Sérialisation des Restrictions Pays (v1.1.4)\n if (vid.restriction) {\n if (!vid.restriction.countries || vid.restriction.countries.length === 0) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video restriction: countries array cannot be empty.`\n );\n }\n\n const cleanCountries = vid.restriction.countries.map(country => {\n const code = country.trim().toUpperCase();\n if (code.length < 2 || code.length > 3) {\n throw new Error(\n `[next-advanced-sitemap] Invalid ISO country code detected: \"${country}\". Must be a valid ISO 3166 code.`\n );\n }\n return code;\n });\n\n const countriesStr = cleanCountries.join(' ');\n xml += ` <video:restriction relationship=\"${vid.restriction.relationship}\">${countriesStr}</video:restriction>\\n`;\n }\n\n // ✨ Validation et Sérialisation des Plateformes (v1.1.4)\n if (vid.platform) {\n if (!vid.platform.platforms || vid.platform.platforms.length === 0) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video platform: platforms array cannot be empty.`\n );\n }\n\n const validPlatforms = ['web', 'mobile', 'tv'];\n for (const p of vid.platform.platforms) {\n if (!validPlatforms.includes(p)) {\n throw new Error(\n `[next-advanced-sitemap] Invalid platform type: \"${p}\". Allowed values are 'web', 'mobile', or 'tv'.`\n );\n }\n }\n\n const platformsStr = vid.platform.platforms.join(' ');\n xml += ` <video:platform relationship=\"${vid.platform.relationship}\">${platformsStr}</video:platform>\\n`;\n }\n \n if (vid.requires_subscription !== undefined) {\n let subValue: 'yes' | 'no';\n\n if (typeof vid.requires_subscription === 'boolean') {\n subValue = vid.requires_subscription ? 'yes' : 'no';\n } else if (vid.requires_subscription === 'yes' || vid.requires_subscription === 'no') {\n subValue = vid.requires_subscription;\n } else {\n throw new Error(\n `[next-advanced-sitemap] Invalid value for requires_subscription: \"${vid.requires_subscription}\". Expected boolean or strict string 'yes' | 'no'.`\n );\n }\n\n xml += ` <video:requires_subscription>${subValue}</video:requires_subscription>\\n`;\n }\n\n // ✨ Validation et Sérialisation des Prix et Achats (v1.1.6)\n if (vid.price) {\n const { value, currency, type } = vid.price;\n\n if (value === undefined || value < 0) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video price value: \"${value}\". Value must be a positive number.`\n );\n }\n\n const cleanCurrency = currency ? currency.trim().toUpperCase() : '';\n if (cleanCurrency.length !== 3) {\n throw new Error(\n `[next-advanced-sitemap] Invalid ISO 4217 currency code: \"${currency}\". Currency must be exactly a 3-letter code.`\n );\n }\n\n let priceXml = ` <video:price currency=\"${cleanCurrency}\"`;\n if (type) {\n if (type !== 'rent' && type !== 'own') {\n throw new Error(\n `[next-advanced-sitemap] Invalid price type: \"${type}\". Allowed values are 'rent' or 'own'.`\n );\n }\n priceXml += ` type=\"${type}\"`;\n }\n priceXml += `>${value.toFixed(2)}</video:price>\\n`;\n \n xml += priceXml;\n }\n\n // ✨ Validation et Sérialisation de la Catégorie (v1.1.7)\n if (vid.category !== undefined) {\n const cleanCategory = vid.category.trim();\n if (!cleanCategory) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video category: category cannot be empty or just whitespaces.`\n );\n }\n if (cleanCategory.length > 256) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video category length: ${cleanCategory.length}. Maximum allowed is 256 characters.`\n );\n }\n xml += ` <video:category>${escapeXml(cleanCategory)}</video:category>\\n`;\n }\n\n // ✨ Validation et Sérialisation des Tags (v1.1.7)\n if (vid.tags) {\n if (vid.tags.length > 32) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video tags count: ${vid.tags.length}. A video can have a maximum of 32 tags.`\n );\n }\n\n for (const tag of vid.tags) {\n const cleanTag = tag.trim();\n if (!cleanTag) {\n throw new Error(\n `[next-advanced-sitemap] Invalid video tag detected: tag cannot be empty or just whitespaces.`\n );\n }\n xml += ` <video:tag>${escapeXml(cleanTag)}</video:tag>\\n`;\n }\n }\n\n xml += ` </video:video>\\n`;\n }\n \n return xml;\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry } from '../../types/sitemap.js';\nimport { escapeXml } from '../../utils/xml-escape.js';\n\nexport function buildNewsXml(news: SitemapEntry['news']): string {\n if (!news) return '';\n\n const nDate = news.publication_date instanceof Date ? news.publication_date.toISOString() : news.publication_date;\n \n let xml = '';\n xml += ` <news:news>\\n`;\n xml += ` <news:publication>\\n`;\n xml += ` <news:name>${escapeXml(news.name)}</news:name>\\n`;\n xml += ` <news:language>${escapeXml(news.language)}</news:language>\\n`;\n xml += ` </news:publication>\\n`;\n xml += ` <news:publication_date>${nDate}</news:publication_date>\\n`;\n xml += ` <news:title>${escapeXml(news.title)}</news:title>\\n`;\n xml += ` </news:news>\\n`;\n \n return xml;\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry, SitemapOptions } from '../types/sitemap.js';\nimport { buildUrlBaseXml } from './builders/url-builder.js';\nimport { buildImageXml } from './builders/image-builder.js';\nimport { buildVideoXml } from './builders/video-builder.js';\nimport { buildNewsXml } from './builders/news-builder.js';\n\n/**\n * Génère le flux XML complet du sitemap incluant les extensions Images, Vidéos, News et Hreflang.\n * v1.1.4 : Version découplée et hautement modulaire.\n */\nexport function generateXml(entries: SitemapEntry[], options: SitemapOptions = {}): string {\n const now = new Date().toISOString();\n let finalEntries = [...entries];\n\n if (options.sortByPriority) {\n finalEntries.sort((a, b) => {\n const priorityA = a.priority !== undefined ? (a.priority as number) : 0.5;\n const priorityB = b.priority !== undefined ? (b.priority as number) : 0.5;\n return priorityB - priorityA;\n });\n }\n \n let xml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n`;\n xml += `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\\n`;\n xml += ` xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\"\\n`;\n xml += ` xmlns:video=\"http://www.google.com/schemas/sitemap-video/1.1\"\\n`;\n xml += ` xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\"\\n`;\n xml += ` xmlns:xhtml=\"http://www.w3.org/1999/xhtml\">\\n`;\n\n for (const entry of finalEntries) {\n xml += ` <url>\\n`;\n \n // 1. Éléments de base et hreflang alternatifs\n xml += buildUrlBaseXml(entry, options, now);\n\n // 2. Extension Images Google\n xml += buildImageXml(entry.images);\n\n // 3. Extension Vidéos Google (Validations v1.1.3 & v1.1.4 intégrées)\n xml += buildVideoXml(entry.videos);\n\n // 4. Extension News Google\n xml += buildNewsXml(entry.news);\n\n xml += ` </url>\\n`;\n }\n\n xml += `</urlset>`;\n return xml;\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \n * Licensed under FomaDev Public License.\n * See LICENSE file in the project root for full license information.\n */\n\nimport { SitemapEntry, SitemapOptions } from './types/sitemap.js';\nimport { generateXml } from './core/generator.js';\n\nexport * from './types/sitemap.js';\n\n/**\n * Génère une réponse HTTP compatible Next.js (App Router) avec options de configuration.\n * v1.0.9 : Injection dynamique et personnalisable de l'en-tête Cache-Control via l'option maxAge\n * * @param entries - Liste des entrées du sitemap\n * @param options - Options de génération et de mise en cache (ex: autoLastmod, maxAge)\n * @returns Une instance de Response contenant le flux XML configuré\n */\nexport function getServerSitemapResponse(\n entries: SitemapEntry[], \n options: SitemapOptions = {}\n): Response {\n const xml = generateXml(entries, options);\n\n // Détermination de la stratégie de mise en cache (v1.0.9)\n const cacheControlHeader = options.maxAge !== undefined\n ? `public, max-age=${options.maxAge}, must-revalidate`\n : 'public, s-maxage=86400, stale-while-revalidate';\n\n return new Response(xml, {\n headers: {\n 'Content-Type': 'application/xml',\n 'Cache-Control': cacheControlHeader,\n },\n });\n}"],"mappings":";AASO,SAAS,UAAU,QAA2C;AACnE,MAAI,CAAC,OAAQ,QAAO;AAEpB,SAAO,OAAO,QAAQ,YAAY,CAAC,MAAM;AACvC,YAAQ,GAAG;AAAA,MACT,KAAK;AAAK,eAAO;AAAA,MACjB,KAAK;AAAK,eAAO;AAAA,MACjB,KAAK;AAAK,eAAO;AAAA,MACjB,KAAK;AAAK,eAAO;AAAA,MACjB,KAAK;AAAK,eAAO;AAAA,MACjB;AAAS,eAAO;AAAA,IAClB;AAAA,EACF,CAAC;AACH;;;ACVO,SAAS,uBAAuB,QAAgB,SAAyB;AAC9E,QAAM,MAAM,SAAS,OAAO,KAAK,IAAI;AAErC,MAAI,CAAC,IAAI,WAAW,SAAS,KAAK,CAAC,IAAI,WAAW,UAAU,GAAG;AAC7D,UAAM,IAAI;AAAA,MACR,0CAA0C,OAAO,MAAM,GAAG;AAAA,IAC5D;AAAA,EACF;AAEA,MAAI,IAAI,SAAS,GAAG,GAAG;AACrB,UAAM,IAAI;AAAA,MACR,+DAA+D,OAAO,MAAM,GAAG;AAAA,IACjF;AAAA,EACF;AAEA,MAAI,UAAU;AACd,MAAI,OAAO,IAAI,aAAa,YAAY;AACtC,cAAU,IAAI,SAAS,GAAG;AAAA,EAC5B,OAAO;AACL,QAAI;AACF,UAAI,IAAI,GAAG;AACX,gBAAU;AAAA,IACZ,QAAQ;AACN,gBAAU;AAAA,IACZ;AAAA,EACF;AAEA,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR,+DAA+D,OAAO,MAAM,GAAG;AAAA,IACjF;AAAA,EACF;AAEA,SAAO;AACT;AAKO,SAAS,gBAAgB,OAAqB,SAAyB,QAAwB;AACpG,MAAI,MAAM;AAEV,QAAM,eAAe,uBAAuB,MAAM,KAAK,YAAY;AACnE,SAAO,YAAY,UAAU,YAAY,CAAC;AAAA;AAE1C,MAAI,MAAM,YAAY,QAAQ;AAC5B,eAAW,OAAO,MAAM,YAAY;AAClC,YAAM,cAAc,uBAAuB,IAAI,MAAM,gBAAgB;AACrE,aAAO,6CAA6C,UAAU,IAAI,QAAQ,CAAC,WAAW,UAAU,WAAW,CAAC;AAAA;AAAA,IAC9G;AAAA,EACF;AAEA,MAAI,eAAe,MAAM;AACzB,MAAI,QAAQ,eAAe,CAAC,cAAc;AACxC,mBAAe;AAAA,EACjB;AAEA,MAAI,cAAc;AAChB,UAAM,OAAO,wBAAwB,OAAO,aAAa,YAAY,IAAI;AACzE,WAAO,gBAAgB,IAAI;AAAA;AAAA,EAC7B;AAEA,MAAI,MAAM,YAAY;AACpB,WAAO,mBAAmB,MAAM,UAAU;AAAA;AAAA,EAC5C;AAEA,MAAI,MAAM,aAAa,QAAW;AAChC,WAAO,iBAAkB,MAAM,SAAoB,QAAQ,CAAC,CAAC;AAAA;AAAA,EAC/D;AAEA,SAAO;AACT;;;AC1EO,SAAS,cAAc,QAAwC;AACpE,MAAI,CAAC,QAAQ,OAAQ,QAAO;AAE5B,MAAI,MAAM;AACV,aAAW,OAAO,QAAQ;AACxB,UAAM,cAAc,uBAAuB,IAAI,KAAK,gBAAgB;AAEpE,WAAO;AAAA;AACP,WAAO,oBAAoB,UAAU,WAAW,CAAC;AAAA;AAEjD,QAAI,IAAI,SAAS,IAAI,MAAM,KAAK,MAAM,IAAI;AACxC,aAAO,sBAAsB,UAAU,IAAI,MAAM,KAAK,CAAC,CAAC;AAAA;AAAA,IAC1D;AACA,QAAI,IAAI,WAAW,IAAI,QAAQ,KAAK,MAAM,IAAI;AAC5C,aAAO,wBAAwB,UAAU,IAAI,QAAQ,KAAK,CAAC,CAAC;AAAA;AAAA,IAC9D;AAEA,QAAI,IAAI,gBAAgB,IAAI,aAAa,KAAK,MAAM,IAAI;AACtD,aAAO,6BAA6B,UAAU,IAAI,aAAa,KAAK,CAAC,CAAC;AAAA;AAAA,IACxE;AAEA,QAAI,IAAI,SAAS;AACf,YAAM,kBAAkB,uBAAuB,IAAI,SAAS,mBAAmB;AAC/E,aAAO,wBAAwB,UAAU,eAAe,CAAC;AAAA;AAAA,IAC3D;AAEA,WAAO;AAAA;AAAA,EACT;AAEA,SAAO;AACT;;;AC9BO,SAAS,cAAc,QAAwC;AACpE,MAAI,CAAC,QAAQ,OAAQ,QAAO;AAE5B,MAAI,MAAM;AACV,aAAW,OAAO,QAAQ;AACxB,UAAM,gBAAgB,uBAAuB,IAAI,eAAe,iBAAiB;AACjF,UAAM,kBAAkB,IAAI,cAAc,uBAAuB,IAAI,aAAa,wBAAwB,IAAI;AAC9G,UAAM,iBAAiB,IAAI,aAAa,uBAAuB,IAAI,YAAY,uBAAuB,IAAI;AAE1G,WAAO;AAAA;AACP,WAAO,8BAA8B,UAAU,aAAa,CAAC;AAAA;AAC7D,WAAO,sBAAsB,UAAU,IAAI,KAAK,CAAC;AAAA;AACjD,WAAO,4BAA4B,UAAU,IAAI,WAAW,CAAC;AAAA;AAE7D,QAAI,gBAAiB,QAAO,4BAA4B,UAAU,eAAe,CAAC;AAAA;AAClF,QAAI,eAAgB,QAAO,2BAA2B,UAAU,cAAc,CAAC;AAAA;AAE/E,QAAI,IAAI,kBAAkB;AACxB,YAAM,QAAQ,IAAI,4BAA4B,OAAO,IAAI,iBAAiB,YAAY,IAAI,IAAI;AAC9F,aAAO,iCAAiC,KAAK;AAAA;AAAA,IAC/C;AAGA,QAAI,IAAI,aAAa,QAAW;AAC9B,YAAM,gBAAgB,KAAK,MAAM,IAAI,QAAQ;AAC7C,UAAI,gBAAgB,KAAK,gBAAgB,OAAO;AAC9C,cAAM,IAAI;AAAA,UACR,mDAAmD,aAAa;AAAA,QAClE;AAAA,MACF;AACA,aAAO,yBAAyB,aAAa;AAAA;AAAA,IAC/C;AAGA,QAAI,IAAI,eAAe,QAAW;AAChC,YAAM,iBAAiB,KAAK,MAAM,IAAI,UAAU;AAChD,UAAI,iBAAiB,GAAG;AACtB,cAAM,IAAI;AAAA,UACR,qDAAqD,cAAc;AAAA,QACrE;AAAA,MACF;AACA,aAAO,2BAA2B,cAAc;AAAA;AAAA,IAClD;AAEA,QAAI,IAAI,MAAM;AACZ,aAAO,qBAAqB,IAAI,IAAI;AAAA;AAAA,IACtC;AAGA,QAAI,IAAI,aAAa;AACnB,UAAI,CAAC,IAAI,YAAY,aAAa,IAAI,YAAY,UAAU,WAAW,GAAG;AACxE,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AAEA,YAAM,iBAAiB,IAAI,YAAY,UAAU,IAAI,aAAW;AAC9D,cAAM,OAAO,QAAQ,KAAK,EAAE,YAAY;AACxC,YAAI,KAAK,SAAS,KAAK,KAAK,SAAS,GAAG;AACtC,gBAAM,IAAI;AAAA,YACR,+DAA+D,OAAO;AAAA,UACxE;AAAA,QACF;AACA,eAAO;AAAA,MACT,CAAC;AAED,YAAM,eAAe,eAAe,KAAK,GAAG;AAC5C,aAAO,0CAA0C,IAAI,YAAY,YAAY,KAAK,YAAY;AAAA;AAAA,IAChG;AAGA,QAAI,IAAI,UAAU;AAChB,UAAI,CAAC,IAAI,SAAS,aAAa,IAAI,SAAS,UAAU,WAAW,GAAG;AAClE,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AAEA,YAAM,iBAAiB,CAAC,OAAO,UAAU,IAAI;AAC7C,iBAAW,KAAK,IAAI,SAAS,WAAW;AACtC,YAAI,CAAC,eAAe,SAAS,CAAC,GAAG;AAC/B,gBAAM,IAAI;AAAA,YACR,mDAAmD,CAAC;AAAA,UACtD;AAAA,QACF;AAAA,MACF;AAEA,YAAM,eAAe,IAAI,SAAS,UAAU,KAAK,GAAG;AACpD,aAAO,uCAAuC,IAAI,SAAS,YAAY,KAAK,YAAY;AAAA;AAAA,IAC1F;AAEA,QAAI,IAAI,0BAA0B,QAAW;AAC3C,UAAI;AAEJ,UAAI,OAAO,IAAI,0BAA0B,WAAW;AAClD,mBAAW,IAAI,wBAAwB,QAAQ;AAAA,MACjD,WAAW,IAAI,0BAA0B,SAAS,IAAI,0BAA0B,MAAM;AACpF,mBAAW,IAAI;AAAA,MACjB,OAAO;AACL,cAAM,IAAI;AAAA,UACR,qEAAqE,IAAI,qBAAqB;AAAA,QAChG;AAAA,MACF;AAEA,aAAO,sCAAsC,QAAQ;AAAA;AAAA,IACvD;AAGA,QAAI,IAAI,OAAO;AACb,YAAM,EAAE,OAAO,UAAU,KAAK,IAAI,IAAI;AAEtC,UAAI,UAAU,UAAa,QAAQ,GAAG;AACpC,cAAM,IAAI;AAAA,UACR,uDAAuD,KAAK;AAAA,QAC9D;AAAA,MACF;AAEA,YAAM,gBAAgB,WAAW,SAAS,KAAK,EAAE,YAAY,IAAI;AACjE,UAAI,cAAc,WAAW,GAAG;AAC9B,cAAM,IAAI;AAAA,UACR,4DAA4D,QAAQ;AAAA,QACtE;AAAA,MACF;AAEA,UAAI,WAAW,gCAAgC,aAAa;AAC5D,UAAI,MAAM;AACR,YAAI,SAAS,UAAU,SAAS,OAAO;AACrC,gBAAM,IAAI;AAAA,YACR,gDAAgD,IAAI;AAAA,UACtD;AAAA,QACF;AACA,oBAAY,UAAU,IAAI;AAAA,MAC5B;AACA,kBAAY,IAAI,MAAM,QAAQ,CAAC,CAAC;AAAA;AAEhC,aAAO;AAAA,IACT;AAGA,QAAI,IAAI,aAAa,QAAW;AAC9B,YAAM,gBAAgB,IAAI,SAAS,KAAK;AACxC,UAAI,CAAC,eAAe;AAClB,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AACA,UAAI,cAAc,SAAS,KAAK;AAC9B,cAAM,IAAI;AAAA,UACR,0DAA0D,cAAc,MAAM;AAAA,QAChF;AAAA,MACF;AACA,aAAO,yBAAyB,UAAU,aAAa,CAAC;AAAA;AAAA,IAC1D;AAGA,QAAI,IAAI,MAAM;AACZ,UAAI,IAAI,KAAK,SAAS,IAAI;AACxB,cAAM,IAAI;AAAA,UACR,qDAAqD,IAAI,KAAK,MAAM;AAAA,QACtE;AAAA,MACF;AAEA,iBAAW,OAAO,IAAI,MAAM;AAC1B,cAAM,WAAW,IAAI,KAAK;AAC1B,YAAI,CAAC,UAAU;AACb,gBAAM,IAAI;AAAA,YACR;AAAA,UACF;AAAA,QACF;AACA,eAAO,oBAAoB,UAAU,QAAQ,CAAC;AAAA;AAAA,MAChD;AAAA,IACF;AAEA,WAAO;AAAA;AAAA,EACT;AAEA,SAAO;AACT;;;AClLO,SAAS,aAAa,MAAoC;AAC/D,MAAI,CAAC,KAAM,QAAO;AAElB,QAAM,QAAQ,KAAK,4BAA4B,OAAO,KAAK,iBAAiB,YAAY,IAAI,KAAK;AAEjG,MAAI,MAAM;AACV,SAAO;AAAA;AACP,SAAO;AAAA;AACP,SAAO,sBAAsB,UAAU,KAAK,IAAI,CAAC;AAAA;AACjD,SAAO,0BAA0B,UAAU,KAAK,QAAQ,CAAC;AAAA;AACzD,SAAO;AAAA;AACP,SAAO,gCAAgC,KAAK;AAAA;AAC5C,SAAO,qBAAqB,UAAU,KAAK,KAAK,CAAC;AAAA;AACjD,SAAO;AAAA;AAEP,SAAO;AACT;;;ACTO,SAAS,YAAY,SAAyB,UAA0B,CAAC,GAAW;AACzF,QAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,MAAI,eAAe,CAAC,GAAG,OAAO;AAE9B,MAAI,QAAQ,gBAAgB;AAC1B,iBAAa,KAAK,CAAC,GAAG,MAAM;AAC1B,YAAM,YAAY,EAAE,aAAa,SAAa,EAAE,WAAsB;AACtE,YAAM,YAAY,EAAE,aAAa,SAAa,EAAE,WAAsB;AACtE,aAAO,YAAY;AAAA,IACrB,CAAC;AAAA,EACH;AAEA,MAAI,MAAM;AAAA;AACV,SAAO;AAAA;AACP,SAAO;AAAA;AACP,SAAO;AAAA;AACP,SAAO;AAAA;AACP,SAAO;AAAA;AAEP,aAAW,SAAS,cAAc;AAChC,WAAO;AAAA;AAGP,WAAO,gBAAgB,OAAO,SAAS,GAAG;AAG1C,WAAO,cAAc,MAAM,MAAM;AAGjC,WAAO,cAAc,MAAM,MAAM;AAGjC,WAAO,aAAa,MAAM,IAAI;AAE9B,WAAO;AAAA;AAAA,EACT;AAEA,SAAO;AACP,SAAO;AACT;;;ACrCO,SAAS,yBACd,SACA,UAA0B,CAAC,GACjB;AACV,QAAM,MAAM,YAAY,SAAS,OAAO;AAGxC,QAAM,qBAAqB,QAAQ,WAAW,SAC1C,mBAAmB,QAAQ,MAAM,sBACjC;AAEJ,SAAO,IAAI,SAAS,KAAK;AAAA,IACvB,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,iBAAiB;AAAA,IACnB;AAAA,EACF,CAAC;AACH;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-advanced-sitemap",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "type": "module",
5
5
  "description": "Advanced sitemap generator for Next.js. Powerful support for Google Images, Video, News, and Hreflang (multilingual). Type-safe, zero-dependency, and built for App Router.",
6
6
  "main": "./dist/index.cjs",