next-advanced-sitemap 1.0.1 → 1.0.2
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 +8 -3
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ While Next.js provides a built-in `MetadataRoute.Sitemap` utility, it currently
|
|
|
13
13
|
- **Google News Support**: Comply with Google News requirements including publication names and dates.
|
|
14
14
|
- **Internationalization**: Seamless integration of `xhtml:link` tags for Hreflang and multi-regional SEO.
|
|
15
15
|
- **Strict Validation (v1.0.1)**: Built-in safety checks to ensure all URLs follow absolute protocols (http/https), preventing search engine rejection.
|
|
16
|
+
- **Advanced XML Escaping (v1.0.2)**: Enhanced processor to handle complex special characters (`&`, `"`, `'`, `<`, `>`) in SEO metadata, ensuring XML integrity.
|
|
16
17
|
- **Developer Experience**: Fully typed with TypeScript, zero external dependencies, and optimized for Next.js Route Handlers.
|
|
17
18
|
|
|
18
19
|
## Installation
|
|
@@ -41,7 +42,7 @@ export async function GET() {
|
|
|
41
42
|
]
|
|
42
43
|
},
|
|
43
44
|
{
|
|
44
|
-
url: 'https://fomadev.com/dashboard
|
|
45
|
+
url: 'https://fomadev.com/dashboard,
|
|
45
46
|
images: [
|
|
46
47
|
{
|
|
47
48
|
loc: 'https://fomadev.com/charts/analytics.png',
|
|
@@ -54,7 +55,7 @@ export async function GET() {
|
|
|
54
55
|
url: 'https://fomadev.com/video-tutorial',
|
|
55
56
|
videos: [
|
|
56
57
|
{
|
|
57
|
-
thumbnail_loc: 'https://fomadev.com/thumbs/tutorial.jpg
|
|
58
|
+
thumbnail_loc: 'https://fomadev.com/thumbs/tutorial.jpg',
|
|
58
59
|
title: 'Next.js Advanced SEO Tutorial',
|
|
59
60
|
description: 'Learn how to implement advanced sitemaps in Next.js.',
|
|
60
61
|
publication_date: new Date('2026-04-22')
|
|
@@ -133,9 +134,13 @@ Generates a standard Next.js `Response` object with the correct `application/xml
|
|
|
133
134
|
|
|
134
135
|
Starting from version 1.0.1, the library performs strict validation on all link-related fields. If a URL does not include a valid protocol (http/https), the generator will throw a descriptive error to prevent deploying malformed sitemaps.
|
|
135
136
|
|
|
137
|
+
### Advanced XML Security
|
|
138
|
+
|
|
139
|
+
Since version 1.0.2, the library includes an enhanced encoding processor. It automatically detects and escapes special characters within titles, descriptions, and captions to prevent XML corruption. For example, a title like "Tuto & News" is automatically converted to "Tuto & News", ensuring the sitemap remains readable by search engine crawlers.
|
|
140
|
+
|
|
136
141
|
### Performance
|
|
137
142
|
|
|
138
|
-
This library uses an efficient string-building approach to ensure a minimal memory footprint
|
|
143
|
+
This library uses an efficient string-building approach to ensure a minimal memory footprint during XML generation, even with thousands of entries.
|
|
139
144
|
|
|
140
145
|
## License
|
|
141
146
|
|
package/dist/index.cjs
CHANGED
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/utils/xml-escape.ts","../src/core/generator.ts"],"sourcesContent":["/* * Copyright (c) 2026 Fordi / FomaDev. \r\n * Licensed under FomaDev Public License.\r\n * See LICENSE file in the project root for full license information.\r\n */\r\n\r\nimport { SitemapEntry } from './types/sitemap.js';\r\nimport { generateXml } from './core/generator.js';\r\n\r\nexport * from './types/sitemap.js';\r\n\r\n/**\r\n * Génère une réponse HTTP compatible Next.js (App Router)\r\n * * @param entries - Liste des entrées du sitemap\r\n * @returns Une instance de Response contenant le flux XML\r\n */\r\nexport function getServerSitemapResponse(entries: SitemapEntry[]): Response {\r\n const xml = generateXml(entries);\r\n\r\n return new Response(xml, {\r\n headers: {\r\n 'Content-Type': 'application/xml',\r\n 'Cache-Control': 'public, s-maxage=86400, stale-while-revalidate',\r\n },\r\n });\r\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \r\n * Licensed under FomaDev Public License.\r\n * See LICENSE file in the project root for full license information.\r\n */\r\n\r\nexport function escapeXml(unsafe: string): string {\r\n return unsafe.replace(/[<>&\"']/g, (c) => {\r\n switch (c) {\r\n case '<': return '<';\r\n case '>': return '>';\r\n case '&': return '&';\r\n case '\"': return '"';\r\n case \"'\": return ''';\r\n default: return c;\r\n }\r\n });\r\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \r\n * Licensed under FomaDev Public License.\r\n * See LICENSE file in the project root for full license information.\r\n */\r\n\r\nimport { SitemapEntry } from '../types/sitemap.js';\r\nimport { escapeXml } from '../utils/xml-escape.js';\r\n\r\n/**\r\n * Valide que l'URL commence par un protocole autorisé.\r\n */\r\nfunction validateUrl(url: string, context: string): void {\r\n if (!url.startsWith('http://') && !url.startsWith('https://')) {\r\n throw new Error(\r\n `[next-advanced-sitemap] Invalid URL in ${context}: \"${url}\". URLs must start with http:// or https://`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Génère le flux XML complet du sitemap incluant les extensions Images, Vidéos, News et Hreflang.\r\n */\r\nexport function generateXml(entries: SitemapEntry[]): string {\r\n let xml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n`;\r\n xml += `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\\n`;\r\n xml += ` xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\"\\n`;\r\n xml += ` xmlns:video=\"http://www.google.com/schemas/sitemap-video/1.1\"\\n`;\r\n xml += ` xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\"\\n`;\r\n xml += ` xmlns:xhtml=\"http://www.w3.org/1999/xhtml\">\\n`;\r\n\r\n for (const entry of entries) {\r\n // Validation URL principale\r\n validateUrl(entry.url, 'main entry');\r\n\r\n xml += ` <url>\\n`;\r\n xml += ` <loc>${escapeXml(entry.url)}</loc>\\n`;\r\n\r\n // Support Hreflang (Internationalisation)\r\n if (entry.alternates?.length) {\r\n for (const alt of entry.alternates) {\r\n validateUrl(alt.href, 'alternate link');\r\n xml += ` <xhtml:link rel=\"alternate\" hreflang=\"${escapeXml(alt.hreflang)}\" href=\"${escapeXml(alt.href)}\" />\\n`;\r\n }\r\n }\r\n\r\n // Métadonnées standard\r\n if (entry.lastmod) {\r\n const date = entry.lastmod instanceof Date ? entry.lastmod.toISOString() : entry.lastmod;\r\n xml += ` <lastmod>${date}</lastmod>\\n`;\r\n }\r\n\r\n if (entry.changefreq) {\r\n xml += ` <changefreq>${entry.changefreq}</changefreq>\\n`;\r\n }\r\n\r\n if (entry.priority !== undefined) {\r\n xml += ` <priority>${entry.priority.toFixed(1)}</priority>\\n`;\r\n }\r\n\r\n // Extension Images\r\n if (entry.images?.length) {\r\n for (const img of entry.images) {\r\n validateUrl(img.loc, 'image location');\r\n xml += ` <image:image>\\n`;\r\n xml += ` <image:loc>${escapeXml(img.loc)}</image:loc>\\n`;\r\n if (img.title) xml += ` <image:title>${escapeXml(img.title)}</image:title>\\n`;\r\n if (img.caption) xml += ` <image:caption>${escapeXml(img.caption)}</image:caption>\\n`;\r\n xml += ` </image:image>\\n`;\r\n }\r\n }\r\n\r\n // Extension Vidéos\r\n if (entry.videos?.length) {\r\n for (const vid of entry.videos) {\r\n validateUrl(vid.thumbnail_loc, 'video thumbnail');\r\n if (vid.content_loc) validateUrl(vid.content_loc, 'video content location');\r\n if (vid.player_loc) validateUrl(vid.player_loc, 'video player location');\r\n\r\n xml += ` <video:video>\\n`;\r\n xml += ` <video:thumbnail_loc>${escapeXml(vid.thumbnail_loc)}</video:thumbnail_loc>\\n`;\r\n xml += ` <video:title>${escapeXml(vid.title)}</video:title>\\n`;\r\n xml += ` <video:description>${escapeXml(vid.description)}</video:description>\\n`;\r\n \r\n if (vid.content_loc) xml += ` <video:content_loc>${escapeXml(vid.content_loc)}</video:content_loc>\\n`;\r\n if (vid.player_loc) xml += ` <video:player_loc>${escapeXml(vid.player_loc)}</video:player_loc>\\n`;\r\n \r\n if (vid.publication_date) {\r\n const vDate = vid.publication_date instanceof Date ? vid.publication_date.toISOString() : vid.publication_date;\r\n xml += ` <video:publication_date>${vDate}</video:publication_date>\\n`;\r\n }\r\n xml += ` </video:video>\\n`;\r\n }\r\n }\r\n\r\n // Extension News\r\n if (entry.news) {\r\n const nDate = entry.news.publication_date instanceof Date ? entry.news.publication_date.toISOString() : entry.news.publication_date;\r\n xml += ` <news:news>\\n`;\r\n xml += ` <news:publication>\\n`;\r\n xml += ` <news:name>${escapeXml(entry.news.name)}</news:name>\\n`;\r\n xml += ` <news:language>${escapeXml(entry.news.language)}</news:language>\\n`;\r\n xml += ` </news:publication>\\n`;\r\n xml += ` <news:publication_date>${nDate}</news:publication_date>\\n`;\r\n xml += ` <news:title>${escapeXml(entry.news.title)}</news:title>\\n`;\r\n xml += ` </news:news>\\n`;\r\n }\r\n\r\n xml += ` </url>\\n`;\r\n }\r\n\r\n xml += `</urlset>`;\r\n return xml;\r\n}"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/utils/xml-escape.ts","../src/core/generator.ts"],"sourcesContent":["/* * Copyright (c) 2026 Fordi / FomaDev. \r\n * Licensed under FomaDev Public License.\r\n * See LICENSE file in the project root for full license information.\r\n */\r\n\r\nimport { SitemapEntry } from './types/sitemap.js';\r\nimport { generateXml } from './core/generator.js';\r\n\r\nexport * from './types/sitemap.js';\r\n\r\n/**\r\n * Génère une réponse HTTP compatible Next.js (App Router)\r\n * * @param entries - Liste des entrées du sitemap\r\n * @returns Une instance de Response contenant le flux XML\r\n */\r\nexport function getServerSitemapResponse(entries: SitemapEntry[]): Response {\r\n const xml = generateXml(entries);\r\n\r\n return new Response(xml, {\r\n headers: {\r\n 'Content-Type': 'application/xml',\r\n 'Cache-Control': 'public, s-maxage=86400, stale-while-revalidate',\r\n },\r\n });\r\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \r\n * Licensed under FomaDev Public License.\r\n * See LICENSE file in the project root for full license information.\r\n */\r\n\r\n/**\r\n * Convertit les caractères spéciaux en entités XML pour éviter la corruption du fichier.\r\n * Gère : <, >, &, \", '\r\n */\r\nexport function escapeXml(unsafe: string | undefined | null): string {\r\n if (!unsafe) return '';\r\n \r\n return unsafe.replace(/[<>&\"']/g, (c) => {\r\n switch (c) {\r\n case '<': return '<';\r\n case '>': return '>';\r\n case '&': return '&';\r\n case '\"': return '"';\r\n case \"'\": return ''';\r\n default: return c;\r\n }\r\n });\r\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \r\n * Licensed under FomaDev Public License.\r\n * See LICENSE file in the project root for full license information.\r\n */\r\n\r\nimport { SitemapEntry } from '../types/sitemap.js';\r\nimport { escapeXml } from '../utils/xml-escape.js';\r\n\r\n/**\r\n * Valide que l'URL commence par un protocole autorisé.\r\n */\r\nfunction validateUrl(url: string, context: string): void {\r\n if (!url.startsWith('http://') && !url.startsWith('https://')) {\r\n throw new Error(\r\n `[next-advanced-sitemap] Invalid URL in ${context}: \"${url}\". URLs must start with http:// or https://`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Génère le flux XML complet du sitemap incluant les extensions Images, Vidéos, News et Hreflang.\r\n */\r\nexport function generateXml(entries: SitemapEntry[]): string {\r\n let xml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n`;\r\n xml += `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\\n`;\r\n xml += ` xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\"\\n`;\r\n xml += ` xmlns:video=\"http://www.google.com/schemas/sitemap-video/1.1\"\\n`;\r\n xml += ` xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\"\\n`;\r\n xml += ` xmlns:xhtml=\"http://www.w3.org/1999/xhtml\">\\n`;\r\n\r\n for (const entry of entries) {\r\n // Validation URL principale\r\n validateUrl(entry.url, 'main entry');\r\n\r\n xml += ` <url>\\n`;\r\n xml += ` <loc>${escapeXml(entry.url)}</loc>\\n`;\r\n\r\n // Support Hreflang (Internationalisation)\r\n if (entry.alternates?.length) {\r\n for (const alt of entry.alternates) {\r\n validateUrl(alt.href, 'alternate link');\r\n xml += ` <xhtml:link rel=\"alternate\" hreflang=\"${escapeXml(alt.hreflang)}\" href=\"${escapeXml(alt.href)}\" />\\n`;\r\n }\r\n }\r\n\r\n // Métadonnées standard\r\n if (entry.lastmod) {\r\n const date = entry.lastmod instanceof Date ? entry.lastmod.toISOString() : entry.lastmod;\r\n xml += ` <lastmod>${date}</lastmod>\\n`;\r\n }\r\n\r\n if (entry.changefreq) {\r\n xml += ` <changefreq>${entry.changefreq}</changefreq>\\n`;\r\n }\r\n\r\n if (entry.priority !== undefined) {\r\n xml += ` <priority>${entry.priority.toFixed(1)}</priority>\\n`;\r\n }\r\n\r\n // Extension Images\r\n if (entry.images?.length) {\r\n for (const img of entry.images) {\r\n validateUrl(img.loc, 'image location');\r\n xml += ` <image:image>\\n`;\r\n xml += ` <image:loc>${escapeXml(img.loc)}</image:loc>\\n`;\r\n if (img.title) xml += ` <image:title>${escapeXml(img.title)}</image:title>\\n`;\r\n if (img.caption) xml += ` <image:caption>${escapeXml(img.caption)}</image:caption>\\n`;\r\n xml += ` </image:image>\\n`;\r\n }\r\n }\r\n\r\n // Extension Vidéos\r\n if (entry.videos?.length) {\r\n for (const vid of entry.videos) {\r\n validateUrl(vid.thumbnail_loc, 'video thumbnail');\r\n if (vid.content_loc) validateUrl(vid.content_loc, 'video content location');\r\n if (vid.player_loc) validateUrl(vid.player_loc, 'video player location');\r\n\r\n xml += ` <video:video>\\n`;\r\n xml += ` <video:thumbnail_loc>${escapeXml(vid.thumbnail_loc)}</video:thumbnail_loc>\\n`;\r\n xml += ` <video:title>${escapeXml(vid.title)}</video:title>\\n`;\r\n xml += ` <video:description>${escapeXml(vid.description)}</video:description>\\n`;\r\n \r\n if (vid.content_loc) xml += ` <video:content_loc>${escapeXml(vid.content_loc)}</video:content_loc>\\n`;\r\n if (vid.player_loc) xml += ` <video:player_loc>${escapeXml(vid.player_loc)}</video:player_loc>\\n`;\r\n \r\n if (vid.publication_date) {\r\n const vDate = vid.publication_date instanceof Date ? vid.publication_date.toISOString() : vid.publication_date;\r\n xml += ` <video:publication_date>${vDate}</video:publication_date>\\n`;\r\n }\r\n xml += ` </video:video>\\n`;\r\n }\r\n }\r\n\r\n // Extension News\r\n if (entry.news) {\r\n const nDate = entry.news.publication_date instanceof Date ? entry.news.publication_date.toISOString() : entry.news.publication_date;\r\n xml += ` <news:news>\\n`;\r\n xml += ` <news:publication>\\n`;\r\n xml += ` <news:name>${escapeXml(entry.news.name)}</news:name>\\n`;\r\n xml += ` <news:language>${escapeXml(entry.news.language)}</news:language>\\n`;\r\n xml += ` </news:publication>\\n`;\r\n xml += ` <news:publication_date>${nDate}</news:publication_date>\\n`;\r\n xml += ` <news:title>${escapeXml(entry.news.title)}</news:title>\\n`;\r\n xml += ` </news:news>\\n`;\r\n }\r\n\r\n xml += ` </url>\\n`;\r\n }\r\n\r\n xml += `</urlset>`;\r\n return xml;\r\n}"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/xml-escape.ts","../src/core/generator.ts","../src/index.ts"],"sourcesContent":["/* * Copyright (c) 2026 Fordi / FomaDev. \r\n * Licensed under FomaDev Public License.\r\n * See LICENSE file in the project root for full license information.\r\n */\r\n\r\nexport function escapeXml(unsafe: string): string {\r\n return unsafe.replace(/[<>&\"']/g, (c) => {\r\n switch (c) {\r\n case '<': return '<';\r\n case '>': return '>';\r\n case '&': return '&';\r\n case '\"': return '"';\r\n case \"'\": return ''';\r\n default: return c;\r\n }\r\n });\r\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \r\n * Licensed under FomaDev Public License.\r\n * See LICENSE file in the project root for full license information.\r\n */\r\n\r\nimport { SitemapEntry } from '../types/sitemap.js';\r\nimport { escapeXml } from '../utils/xml-escape.js';\r\n\r\n/**\r\n * Valide que l'URL commence par un protocole autorisé.\r\n */\r\nfunction validateUrl(url: string, context: string): void {\r\n if (!url.startsWith('http://') && !url.startsWith('https://')) {\r\n throw new Error(\r\n `[next-advanced-sitemap] Invalid URL in ${context}: \"${url}\". URLs must start with http:// or https://`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Génère le flux XML complet du sitemap incluant les extensions Images, Vidéos, News et Hreflang.\r\n */\r\nexport function generateXml(entries: SitemapEntry[]): string {\r\n let xml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n`;\r\n xml += `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\\n`;\r\n xml += ` xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\"\\n`;\r\n xml += ` xmlns:video=\"http://www.google.com/schemas/sitemap-video/1.1\"\\n`;\r\n xml += ` xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\"\\n`;\r\n xml += ` xmlns:xhtml=\"http://www.w3.org/1999/xhtml\">\\n`;\r\n\r\n for (const entry of entries) {\r\n // Validation URL principale\r\n validateUrl(entry.url, 'main entry');\r\n\r\n xml += ` <url>\\n`;\r\n xml += ` <loc>${escapeXml(entry.url)}</loc>\\n`;\r\n\r\n // Support Hreflang (Internationalisation)\r\n if (entry.alternates?.length) {\r\n for (const alt of entry.alternates) {\r\n validateUrl(alt.href, 'alternate link');\r\n xml += ` <xhtml:link rel=\"alternate\" hreflang=\"${escapeXml(alt.hreflang)}\" href=\"${escapeXml(alt.href)}\" />\\n`;\r\n }\r\n }\r\n\r\n // Métadonnées standard\r\n if (entry.lastmod) {\r\n const date = entry.lastmod instanceof Date ? entry.lastmod.toISOString() : entry.lastmod;\r\n xml += ` <lastmod>${date}</lastmod>\\n`;\r\n }\r\n\r\n if (entry.changefreq) {\r\n xml += ` <changefreq>${entry.changefreq}</changefreq>\\n`;\r\n }\r\n\r\n if (entry.priority !== undefined) {\r\n xml += ` <priority>${entry.priority.toFixed(1)}</priority>\\n`;\r\n }\r\n\r\n // Extension Images\r\n if (entry.images?.length) {\r\n for (const img of entry.images) {\r\n validateUrl(img.loc, 'image location');\r\n xml += ` <image:image>\\n`;\r\n xml += ` <image:loc>${escapeXml(img.loc)}</image:loc>\\n`;\r\n if (img.title) xml += ` <image:title>${escapeXml(img.title)}</image:title>\\n`;\r\n if (img.caption) xml += ` <image:caption>${escapeXml(img.caption)}</image:caption>\\n`;\r\n xml += ` </image:image>\\n`;\r\n }\r\n }\r\n\r\n // Extension Vidéos\r\n if (entry.videos?.length) {\r\n for (const vid of entry.videos) {\r\n validateUrl(vid.thumbnail_loc, 'video thumbnail');\r\n if (vid.content_loc) validateUrl(vid.content_loc, 'video content location');\r\n if (vid.player_loc) validateUrl(vid.player_loc, 'video player location');\r\n\r\n xml += ` <video:video>\\n`;\r\n xml += ` <video:thumbnail_loc>${escapeXml(vid.thumbnail_loc)}</video:thumbnail_loc>\\n`;\r\n xml += ` <video:title>${escapeXml(vid.title)}</video:title>\\n`;\r\n xml += ` <video:description>${escapeXml(vid.description)}</video:description>\\n`;\r\n \r\n if (vid.content_loc) xml += ` <video:content_loc>${escapeXml(vid.content_loc)}</video:content_loc>\\n`;\r\n if (vid.player_loc) xml += ` <video:player_loc>${escapeXml(vid.player_loc)}</video:player_loc>\\n`;\r\n \r\n if (vid.publication_date) {\r\n const vDate = vid.publication_date instanceof Date ? vid.publication_date.toISOString() : vid.publication_date;\r\n xml += ` <video:publication_date>${vDate}</video:publication_date>\\n`;\r\n }\r\n xml += ` </video:video>\\n`;\r\n }\r\n }\r\n\r\n // Extension News\r\n if (entry.news) {\r\n const nDate = entry.news.publication_date instanceof Date ? entry.news.publication_date.toISOString() : entry.news.publication_date;\r\n xml += ` <news:news>\\n`;\r\n xml += ` <news:publication>\\n`;\r\n xml += ` <news:name>${escapeXml(entry.news.name)}</news:name>\\n`;\r\n xml += ` <news:language>${escapeXml(entry.news.language)}</news:language>\\n`;\r\n xml += ` </news:publication>\\n`;\r\n xml += ` <news:publication_date>${nDate}</news:publication_date>\\n`;\r\n xml += ` <news:title>${escapeXml(entry.news.title)}</news:title>\\n`;\r\n xml += ` </news:news>\\n`;\r\n }\r\n\r\n xml += ` </url>\\n`;\r\n }\r\n\r\n xml += `</urlset>`;\r\n return xml;\r\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \r\n * Licensed under FomaDev Public License.\r\n * See LICENSE file in the project root for full license information.\r\n */\r\n\r\nimport { SitemapEntry } from './types/sitemap.js';\r\nimport { generateXml } from './core/generator.js';\r\n\r\nexport * from './types/sitemap.js';\r\n\r\n/**\r\n * Génère une réponse HTTP compatible Next.js (App Router)\r\n * * @param entries - Liste des entrées du sitemap\r\n * @returns Une instance de Response contenant le flux XML\r\n */\r\nexport function getServerSitemapResponse(entries: SitemapEntry[]): Response {\r\n const xml = generateXml(entries);\r\n\r\n return new Response(xml, {\r\n headers: {\r\n 'Content-Type': 'application/xml',\r\n 'Cache-Control': 'public, s-maxage=86400, stale-while-revalidate',\r\n },\r\n });\r\n}"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../src/utils/xml-escape.ts","../src/core/generator.ts","../src/index.ts"],"sourcesContent":["/* * Copyright (c) 2026 Fordi / FomaDev. \r\n * Licensed under FomaDev Public License.\r\n * See LICENSE file in the project root for full license information.\r\n */\r\n\r\n/**\r\n * Convertit les caractères spéciaux en entités XML pour éviter la corruption du fichier.\r\n * Gère : <, >, &, \", '\r\n */\r\nexport function escapeXml(unsafe: string | undefined | null): string {\r\n if (!unsafe) return '';\r\n \r\n return unsafe.replace(/[<>&\"']/g, (c) => {\r\n switch (c) {\r\n case '<': return '<';\r\n case '>': return '>';\r\n case '&': return '&';\r\n case '\"': return '"';\r\n case \"'\": return ''';\r\n default: return c;\r\n }\r\n });\r\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \r\n * Licensed under FomaDev Public License.\r\n * See LICENSE file in the project root for full license information.\r\n */\r\n\r\nimport { SitemapEntry } from '../types/sitemap.js';\r\nimport { escapeXml } from '../utils/xml-escape.js';\r\n\r\n/**\r\n * Valide que l'URL commence par un protocole autorisé.\r\n */\r\nfunction validateUrl(url: string, context: string): void {\r\n if (!url.startsWith('http://') && !url.startsWith('https://')) {\r\n throw new Error(\r\n `[next-advanced-sitemap] Invalid URL in ${context}: \"${url}\". URLs must start with http:// or https://`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Génère le flux XML complet du sitemap incluant les extensions Images, Vidéos, News et Hreflang.\r\n */\r\nexport function generateXml(entries: SitemapEntry[]): string {\r\n let xml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n`;\r\n xml += `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\\n`;\r\n xml += ` xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\"\\n`;\r\n xml += ` xmlns:video=\"http://www.google.com/schemas/sitemap-video/1.1\"\\n`;\r\n xml += ` xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\"\\n`;\r\n xml += ` xmlns:xhtml=\"http://www.w3.org/1999/xhtml\">\\n`;\r\n\r\n for (const entry of entries) {\r\n // Validation URL principale\r\n validateUrl(entry.url, 'main entry');\r\n\r\n xml += ` <url>\\n`;\r\n xml += ` <loc>${escapeXml(entry.url)}</loc>\\n`;\r\n\r\n // Support Hreflang (Internationalisation)\r\n if (entry.alternates?.length) {\r\n for (const alt of entry.alternates) {\r\n validateUrl(alt.href, 'alternate link');\r\n xml += ` <xhtml:link rel=\"alternate\" hreflang=\"${escapeXml(alt.hreflang)}\" href=\"${escapeXml(alt.href)}\" />\\n`;\r\n }\r\n }\r\n\r\n // Métadonnées standard\r\n if (entry.lastmod) {\r\n const date = entry.lastmod instanceof Date ? entry.lastmod.toISOString() : entry.lastmod;\r\n xml += ` <lastmod>${date}</lastmod>\\n`;\r\n }\r\n\r\n if (entry.changefreq) {\r\n xml += ` <changefreq>${entry.changefreq}</changefreq>\\n`;\r\n }\r\n\r\n if (entry.priority !== undefined) {\r\n xml += ` <priority>${entry.priority.toFixed(1)}</priority>\\n`;\r\n }\r\n\r\n // Extension Images\r\n if (entry.images?.length) {\r\n for (const img of entry.images) {\r\n validateUrl(img.loc, 'image location');\r\n xml += ` <image:image>\\n`;\r\n xml += ` <image:loc>${escapeXml(img.loc)}</image:loc>\\n`;\r\n if (img.title) xml += ` <image:title>${escapeXml(img.title)}</image:title>\\n`;\r\n if (img.caption) xml += ` <image:caption>${escapeXml(img.caption)}</image:caption>\\n`;\r\n xml += ` </image:image>\\n`;\r\n }\r\n }\r\n\r\n // Extension Vidéos\r\n if (entry.videos?.length) {\r\n for (const vid of entry.videos) {\r\n validateUrl(vid.thumbnail_loc, 'video thumbnail');\r\n if (vid.content_loc) validateUrl(vid.content_loc, 'video content location');\r\n if (vid.player_loc) validateUrl(vid.player_loc, 'video player location');\r\n\r\n xml += ` <video:video>\\n`;\r\n xml += ` <video:thumbnail_loc>${escapeXml(vid.thumbnail_loc)}</video:thumbnail_loc>\\n`;\r\n xml += ` <video:title>${escapeXml(vid.title)}</video:title>\\n`;\r\n xml += ` <video:description>${escapeXml(vid.description)}</video:description>\\n`;\r\n \r\n if (vid.content_loc) xml += ` <video:content_loc>${escapeXml(vid.content_loc)}</video:content_loc>\\n`;\r\n if (vid.player_loc) xml += ` <video:player_loc>${escapeXml(vid.player_loc)}</video:player_loc>\\n`;\r\n \r\n if (vid.publication_date) {\r\n const vDate = vid.publication_date instanceof Date ? vid.publication_date.toISOString() : vid.publication_date;\r\n xml += ` <video:publication_date>${vDate}</video:publication_date>\\n`;\r\n }\r\n xml += ` </video:video>\\n`;\r\n }\r\n }\r\n\r\n // Extension News\r\n if (entry.news) {\r\n const nDate = entry.news.publication_date instanceof Date ? entry.news.publication_date.toISOString() : entry.news.publication_date;\r\n xml += ` <news:news>\\n`;\r\n xml += ` <news:publication>\\n`;\r\n xml += ` <news:name>${escapeXml(entry.news.name)}</news:name>\\n`;\r\n xml += ` <news:language>${escapeXml(entry.news.language)}</news:language>\\n`;\r\n xml += ` </news:publication>\\n`;\r\n xml += ` <news:publication_date>${nDate}</news:publication_date>\\n`;\r\n xml += ` <news:title>${escapeXml(entry.news.title)}</news:title>\\n`;\r\n xml += ` </news:news>\\n`;\r\n }\r\n\r\n xml += ` </url>\\n`;\r\n }\r\n\r\n xml += `</urlset>`;\r\n return xml;\r\n}","/* * Copyright (c) 2026 Fordi / FomaDev. \r\n * Licensed under FomaDev Public License.\r\n * See LICENSE file in the project root for full license information.\r\n */\r\n\r\nimport { SitemapEntry } from './types/sitemap.js';\r\nimport { generateXml } from './core/generator.js';\r\n\r\nexport * from './types/sitemap.js';\r\n\r\n/**\r\n * Génère une réponse HTTP compatible Next.js (App Router)\r\n * * @param entries - Liste des entrées du sitemap\r\n * @returns Une instance de Response contenant le flux XML\r\n */\r\nexport function getServerSitemapResponse(entries: SitemapEntry[]): Response {\r\n const xml = generateXml(entries);\r\n\r\n return new Response(xml, {\r\n headers: {\r\n 'Content-Type': 'application/xml',\r\n 'Cache-Control': 'public, s-maxage=86400, stale-while-revalidate',\r\n },\r\n });\r\n}"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-advanced-sitemap",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"app-router"
|
|
36
36
|
],
|
|
37
37
|
"author": "fomadev",
|
|
38
|
-
"license": "
|
|
38
|
+
"license": "FomaDev Public License",
|
|
39
39
|
"repository": {
|
|
40
40
|
"type": "git",
|
|
41
41
|
"url": "git+https://github.com/fomadev/next-advanced-sitemap.git"
|