next-advanced-sitemap 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fomadev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # next-advanced-sitemap
2
+
3
+ A robust and type-safe sitemap generator for Next.js (App Router). This library extends standard sitemap capabilities by providing native support for Google-specific metadata including Images, Videos, News, and Internationalization (Hreflang).
4
+
5
+ ## Overview
6
+
7
+ While Next.js provides a built-in `MetadataRoute.Sitemap` utility, it currently lacks support for advanced SEO attributes required by high-performance web applications. `next-advanced-sitemap` bridges this gap, allowing developers to programmatically generate complex XML sitemaps that comply with Google's extended schemas.
8
+
9
+ ## Features
10
+
11
+ - **Google Images Support**: Index visual assets such as dashboard charts, infographics, and banners.
12
+ - **Google Video Support**: Improve search visibility for video content with thumbnail and description metadata.
13
+ - **Google News Support**: Comply with Google News requirements including publication names and dates.
14
+ - **Internationalization**: Seamless integration of `xhtml:link` tags for Hreflang and multi-regional SEO.
15
+ - **Developer Experience**: Fully typed with TypeScript, zero external dependencies, and optimized for Next.js Route Handlers.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install next-advanced-sitemap
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ To implement an advanced sitemap in the Next.js App Router, create a Route Handler at `app/sitemap.xml/route.ts`.
26
+
27
+ ```ts
28
+ import { getServerSitemapResponse } from 'next-advanced-sitemap';
29
+
30
+ export async function GET() {
31
+ const entries = [
32
+ {
33
+ url: 'https://fomadev.com',
34
+ lastmod: new Date(),
35
+ changefreq: 'daily',
36
+ priority: 1.0,
37
+ alternates: [
38
+ { hreflang: 'fr', href: 'https://fomadev.com/fr' },
39
+ { hreflang: 'en', href: 'https://fomadev.com/en' }
40
+ ]
41
+ },
42
+ {
43
+ url: 'https://fomadev.com/dashboard',
44
+ images: [
45
+ {
46
+ loc: 'https://fomadev.com/charts/analytics.png',
47
+ title: 'Growth Analytics Chart',
48
+ caption: 'Visual representation of monthly user growth.'
49
+ }
50
+ ]
51
+ },
52
+ {
53
+ url: 'https://fomadev.com/video-tutorial',
54
+ videos: [
55
+ {
56
+ thumbnail_loc: 'https://fomadev.com/thumbs/tutorial.jpg',
57
+ title: 'Next.js Advanced SEO Tutorial',
58
+ description: 'Learn how to implement advanced sitemaps in Next.js.',
59
+ publication_date: new Date('2026-04-22')
60
+ }
61
+ ]
62
+ }
63
+ ];
64
+
65
+ return getServerSitemapResponse(entries);
66
+ }
67
+ ```
68
+
69
+ ## API Reference
70
+
71
+ ### getServerSitemapResponse(entries: SitemapEntry[])
72
+
73
+ Generates a standard Next.js `Response` object with the correct `application/xml` content-type and optimized cache headers.
74
+
75
+ ### SitemapEntry Object
76
+
77
+ <table>
78
+ <thead>
79
+ <tr>
80
+ <th>Property</th>
81
+ <th>Type</th>
82
+ <th>Description</th>
83
+ </tr>
84
+ </thead>
85
+ <tbody>
86
+ <tr>
87
+ <td><code>url</code></td>
88
+ <td>string</td>
89
+ <td>The absolute URL of the page.</td>
90
+ </tr>
91
+ <tr>
92
+ <td><code>lastmod</code></td>
93
+ <td>Date | string</td>
94
+ <td>(Optional) Last modification date in ISO format.</td>
95
+ </tr>
96
+ <tr>
97
+ <td><code>changefreq</code></td>
98
+ <td>string</td>
99
+ <td>(Optional) Search engine hint for crawl frequency.</td>
100
+ </tr>
101
+ <tr>
102
+ <td><code>priority</code></td>
103
+ <td>number</td>
104
+ <td>(Optional) Priority of the URL (0.0 to 1.0).</td>
105
+ </tr>
106
+ <tr>
107
+ <td><code>images</code></td>
108
+ <td>SitemapImage[]</td>
109
+ <td>(Optional) Array of image metadata for Google Images.</td>
110
+ </tr>
111
+ <tr>
112
+ <td><code>videos</code></td>
113
+ <td>SitemapVideo[]</td>
114
+ <td>(Optional) Array of video metadata for Google Videos.</td>
115
+ </tr>
116
+ <tr>
117
+ <td><code>news</code></td>
118
+ <td>SitemapNews</td>
119
+ <td>(Optional) Metadata for Google News indexing.</td>
120
+ </tr>
121
+ <tr>
122
+ <td><code>alternates</code></td>
123
+ <td>SitemapAlternate[]</td>
124
+ <td>(Optional) Language and regional alternate URLs (Hreflang).</td>
125
+ </tr>
126
+ </tbody>
127
+ </table>
128
+
129
+ ## Technical Implementation
130
+
131
+ This library uses a stream-aligned string builder approach to ensure minimal memory footprint during XML generation. It automatically handles XML entity escaping for special characters (e.g., `&`, `<`, `>`) to prevent sitemap corruption.
132
+
133
+ ## License
134
+
135
+ Distributed under the MIT License. See <a href="LICENSE">LICENSE</a> for more information.
136
+
137
+ ## Author
138
+
139
+ Created by <a href="https://github.com/fomadev">fomadev</a>.
package/dist/index.cjs ADDED
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ getServerSitemapResponse: () => getServerSitemapResponse
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/utils/xml-escape.ts
28
+ function escapeXml(unsafe) {
29
+ return unsafe.replace(/[<>&"']/g, (c) => {
30
+ switch (c) {
31
+ case "<":
32
+ return "&lt;";
33
+ case ">":
34
+ return "&gt;";
35
+ case "&":
36
+ return "&amp;";
37
+ case '"':
38
+ return "&quot;";
39
+ case "'":
40
+ return "&apos;";
41
+ default:
42
+ return c;
43
+ }
44
+ });
45
+ }
46
+
47
+ // src/core/generator.ts
48
+ function generateXml(entries) {
49
+ let xml = `<?xml version="1.0" encoding="UTF-8"?>
50
+ `;
51
+ xml += `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
52
+ `;
53
+ xml += ` xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
54
+ `;
55
+ xml += ` xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"
56
+ `;
57
+ xml += ` xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"
58
+ `;
59
+ xml += ` xmlns:xhtml="http://www.w3.org/1999/xhtml">
60
+ `;
61
+ for (const entry of entries) {
62
+ xml += ` <url>
63
+ `;
64
+ xml += ` <loc>${escapeXml(entry.url)}</loc>
65
+ `;
66
+ if (entry.alternates?.length) {
67
+ for (const alt of entry.alternates) {
68
+ xml += ` <xhtml:link rel="alternate" hreflang="${escapeXml(alt.hreflang)}" href="${escapeXml(alt.href)}" />
69
+ `;
70
+ }
71
+ }
72
+ if (entry.lastmod) {
73
+ const date = entry.lastmod instanceof Date ? entry.lastmod.toISOString() : entry.lastmod;
74
+ xml += ` <lastmod>${date}</lastmod>
75
+ `;
76
+ }
77
+ if (entry.changefreq) {
78
+ xml += ` <changefreq>${entry.changefreq}</changefreq>
79
+ `;
80
+ }
81
+ if (entry.priority !== void 0) {
82
+ xml += ` <priority>${entry.priority.toFixed(1)}</priority>
83
+ `;
84
+ }
85
+ if (entry.images?.length) {
86
+ for (const img of entry.images) {
87
+ xml += ` <image:image>
88
+ `;
89
+ xml += ` <image:loc>${escapeXml(img.loc)}</image:loc>
90
+ `;
91
+ if (img.title) xml += ` <image:title>${escapeXml(img.title)}</image:title>
92
+ `;
93
+ if (img.caption) xml += ` <image:caption>${escapeXml(img.caption)}</image:caption>
94
+ `;
95
+ xml += ` </image:image>
96
+ `;
97
+ }
98
+ }
99
+ if (entry.videos?.length) {
100
+ for (const vid of entry.videos) {
101
+ xml += ` <video:video>
102
+ `;
103
+ xml += ` <video:thumbnail_loc>${escapeXml(vid.thumbnail_loc)}</video:thumbnail_loc>
104
+ `;
105
+ xml += ` <video:title>${escapeXml(vid.title)}</video:title>
106
+ `;
107
+ xml += ` <video:description>${escapeXml(vid.description)}</video:description>
108
+ `;
109
+ if (vid.content_loc) xml += ` <video:content_loc>${escapeXml(vid.content_loc)}</video:content_loc>
110
+ `;
111
+ if (vid.player_loc) xml += ` <video:player_loc>${escapeXml(vid.player_loc)}</video:player_loc>
112
+ `;
113
+ if (vid.publication_date) {
114
+ const vDate = vid.publication_date instanceof Date ? vid.publication_date.toISOString() : vid.publication_date;
115
+ xml += ` <video:publication_date>${vDate}</video:publication_date>
116
+ `;
117
+ }
118
+ xml += ` </video:video>
119
+ `;
120
+ }
121
+ }
122
+ if (entry.news) {
123
+ const nDate = entry.news.publication_date instanceof Date ? entry.news.publication_date.toISOString() : entry.news.publication_date;
124
+ xml += ` <news:news>
125
+ `;
126
+ xml += ` <news:publication>
127
+ `;
128
+ xml += ` <news:name>${escapeXml(entry.news.name)}</news:name>
129
+ `;
130
+ xml += ` <news:language>${escapeXml(entry.news.language)}</news:language>
131
+ `;
132
+ xml += ` </news:publication>
133
+ `;
134
+ xml += ` <news:publication_date>${nDate}</news:publication_date>
135
+ `;
136
+ xml += ` <news:title>${escapeXml(entry.news.title)}</news:title>
137
+ `;
138
+ xml += ` </news:news>
139
+ `;
140
+ }
141
+ xml += ` </url>
142
+ `;
143
+ }
144
+ xml += `</urlset>`;
145
+ return xml;
146
+ }
147
+
148
+ // src/index.ts
149
+ function getServerSitemapResponse(entries) {
150
+ const xml = generateXml(entries);
151
+ return new Response(xml, {
152
+ headers: {
153
+ "Content-Type": "application/xml",
154
+ "Cache-Control": "public, s-maxage=86400, stale-while-revalidate"
155
+ }
156
+ });
157
+ }
158
+ // Annotate the CommonJS export names for ESM import in node:
159
+ 0 && (module.exports = {
160
+ getServerSitemapResponse
161
+ });
162
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/utils/xml-escape.ts","../src/core/generator.ts"],"sourcesContent":["import { 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}","export function escapeXml(unsafe: string): string {\r\n return unsafe.replace(/[<>&\"']/g, (c) => {\r\n switch (c) {\r\n case '<': return '&lt;';\r\n case '>': return '&gt;';\r\n case '&': return '&amp;';\r\n case '\"': return '&quot;';\r\n case \"'\": return '&apos;';\r\n default: return c;\r\n }\r\n });\r\n}","import { SitemapEntry } from '../types/sitemap.js';\r\nimport { escapeXml } from '../utils/xml-escape.js';\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 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 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 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 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 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 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":[]}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Interface pour les liens alternatifs (Hreflang / Multilingue)
3
+ * @see https://developers.google.com/search/docs/specialty/international/localized-versions#sitemap
4
+ */
5
+ interface SitemapAlternate {
6
+ hreflang: string;
7
+ href: string;
8
+ }
9
+ /**
10
+ * Interface pour les images dans le sitemap
11
+ * @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/image-sitemaps
12
+ */
13
+ interface SitemapImage {
14
+ loc: string;
15
+ caption?: string;
16
+ title?: string;
17
+ geoLocation?: string;
18
+ license?: string;
19
+ }
20
+ /**
21
+ * Interface pour les vidéos dans le sitemap
22
+ * @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/video-sitemaps
23
+ */
24
+ interface SitemapVideo {
25
+ thumbnail_loc: string;
26
+ title: string;
27
+ description: string;
28
+ content_loc?: string;
29
+ player_loc?: string;
30
+ duration?: number;
31
+ view_count?: number;
32
+ publication_date?: string | Date;
33
+ family_friendly?: 'yes' | 'no';
34
+ }
35
+ /**
36
+ * Interface pour Google News
37
+ * @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/news-sitemaps
38
+ */
39
+ interface SitemapNews {
40
+ name: string;
41
+ language: string;
42
+ publication_date: string | Date;
43
+ title: string;
44
+ }
45
+ /**
46
+ * Interface principale représentant une entrée du sitemap
47
+ */
48
+ interface SitemapEntry {
49
+ url: string;
50
+ lastmod?: string | Date;
51
+ changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
52
+ priority?: number;
53
+ images?: SitemapImage[];
54
+ videos?: SitemapVideo[];
55
+ news?: SitemapNews;
56
+ alternates?: SitemapAlternate[];
57
+ }
58
+
59
+ /**
60
+ * Génère une réponse HTTP compatible Next.js (App Router)
61
+ * * @param entries - Liste des entrées du sitemap
62
+ * @returns Une instance de Response contenant le flux XML
63
+ */
64
+ declare function getServerSitemapResponse(entries: SitemapEntry[]): Response;
65
+
66
+ export { type SitemapAlternate, type SitemapEntry, type SitemapImage, type SitemapNews, type SitemapVideo, getServerSitemapResponse };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Interface pour les liens alternatifs (Hreflang / Multilingue)
3
+ * @see https://developers.google.com/search/docs/specialty/international/localized-versions#sitemap
4
+ */
5
+ interface SitemapAlternate {
6
+ hreflang: string;
7
+ href: string;
8
+ }
9
+ /**
10
+ * Interface pour les images dans le sitemap
11
+ * @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/image-sitemaps
12
+ */
13
+ interface SitemapImage {
14
+ loc: string;
15
+ caption?: string;
16
+ title?: string;
17
+ geoLocation?: string;
18
+ license?: string;
19
+ }
20
+ /**
21
+ * Interface pour les vidéos dans le sitemap
22
+ * @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/video-sitemaps
23
+ */
24
+ interface SitemapVideo {
25
+ thumbnail_loc: string;
26
+ title: string;
27
+ description: string;
28
+ content_loc?: string;
29
+ player_loc?: string;
30
+ duration?: number;
31
+ view_count?: number;
32
+ publication_date?: string | Date;
33
+ family_friendly?: 'yes' | 'no';
34
+ }
35
+ /**
36
+ * Interface pour Google News
37
+ * @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/news-sitemaps
38
+ */
39
+ interface SitemapNews {
40
+ name: string;
41
+ language: string;
42
+ publication_date: string | Date;
43
+ title: string;
44
+ }
45
+ /**
46
+ * Interface principale représentant une entrée du sitemap
47
+ */
48
+ interface SitemapEntry {
49
+ url: string;
50
+ lastmod?: string | Date;
51
+ changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
52
+ priority?: number;
53
+ images?: SitemapImage[];
54
+ videos?: SitemapVideo[];
55
+ news?: SitemapNews;
56
+ alternates?: SitemapAlternate[];
57
+ }
58
+
59
+ /**
60
+ * Génère une réponse HTTP compatible Next.js (App Router)
61
+ * * @param entries - Liste des entrées du sitemap
62
+ * @returns Une instance de Response contenant le flux XML
63
+ */
64
+ declare function getServerSitemapResponse(entries: SitemapEntry[]): Response;
65
+
66
+ export { type SitemapAlternate, type SitemapEntry, type SitemapImage, type SitemapNews, type SitemapVideo, getServerSitemapResponse };
package/dist/index.js ADDED
@@ -0,0 +1,135 @@
1
+ // src/utils/xml-escape.ts
2
+ function escapeXml(unsafe) {
3
+ return unsafe.replace(/[<>&"']/g, (c) => {
4
+ switch (c) {
5
+ case "<":
6
+ return "&lt;";
7
+ case ">":
8
+ return "&gt;";
9
+ case "&":
10
+ return "&amp;";
11
+ case '"':
12
+ return "&quot;";
13
+ case "'":
14
+ return "&apos;";
15
+ default:
16
+ return c;
17
+ }
18
+ });
19
+ }
20
+
21
+ // src/core/generator.ts
22
+ function generateXml(entries) {
23
+ let xml = `<?xml version="1.0" encoding="UTF-8"?>
24
+ `;
25
+ xml += `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
26
+ `;
27
+ xml += ` xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
28
+ `;
29
+ xml += ` xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"
30
+ `;
31
+ xml += ` xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"
32
+ `;
33
+ xml += ` xmlns:xhtml="http://www.w3.org/1999/xhtml">
34
+ `;
35
+ for (const entry of entries) {
36
+ xml += ` <url>
37
+ `;
38
+ xml += ` <loc>${escapeXml(entry.url)}</loc>
39
+ `;
40
+ if (entry.alternates?.length) {
41
+ for (const alt of entry.alternates) {
42
+ xml += ` <xhtml:link rel="alternate" hreflang="${escapeXml(alt.hreflang)}" href="${escapeXml(alt.href)}" />
43
+ `;
44
+ }
45
+ }
46
+ if (entry.lastmod) {
47
+ const date = entry.lastmod instanceof Date ? entry.lastmod.toISOString() : entry.lastmod;
48
+ xml += ` <lastmod>${date}</lastmod>
49
+ `;
50
+ }
51
+ if (entry.changefreq) {
52
+ xml += ` <changefreq>${entry.changefreq}</changefreq>
53
+ `;
54
+ }
55
+ if (entry.priority !== void 0) {
56
+ xml += ` <priority>${entry.priority.toFixed(1)}</priority>
57
+ `;
58
+ }
59
+ if (entry.images?.length) {
60
+ for (const img of entry.images) {
61
+ xml += ` <image:image>
62
+ `;
63
+ xml += ` <image:loc>${escapeXml(img.loc)}</image:loc>
64
+ `;
65
+ if (img.title) xml += ` <image:title>${escapeXml(img.title)}</image:title>
66
+ `;
67
+ if (img.caption) xml += ` <image:caption>${escapeXml(img.caption)}</image:caption>
68
+ `;
69
+ xml += ` </image:image>
70
+ `;
71
+ }
72
+ }
73
+ if (entry.videos?.length) {
74
+ for (const vid of entry.videos) {
75
+ xml += ` <video:video>
76
+ `;
77
+ xml += ` <video:thumbnail_loc>${escapeXml(vid.thumbnail_loc)}</video:thumbnail_loc>
78
+ `;
79
+ xml += ` <video:title>${escapeXml(vid.title)}</video:title>
80
+ `;
81
+ xml += ` <video:description>${escapeXml(vid.description)}</video:description>
82
+ `;
83
+ if (vid.content_loc) xml += ` <video:content_loc>${escapeXml(vid.content_loc)}</video:content_loc>
84
+ `;
85
+ if (vid.player_loc) xml += ` <video:player_loc>${escapeXml(vid.player_loc)}</video:player_loc>
86
+ `;
87
+ if (vid.publication_date) {
88
+ const vDate = vid.publication_date instanceof Date ? vid.publication_date.toISOString() : vid.publication_date;
89
+ xml += ` <video:publication_date>${vDate}</video:publication_date>
90
+ `;
91
+ }
92
+ xml += ` </video:video>
93
+ `;
94
+ }
95
+ }
96
+ if (entry.news) {
97
+ const nDate = entry.news.publication_date instanceof Date ? entry.news.publication_date.toISOString() : entry.news.publication_date;
98
+ xml += ` <news:news>
99
+ `;
100
+ xml += ` <news:publication>
101
+ `;
102
+ xml += ` <news:name>${escapeXml(entry.news.name)}</news:name>
103
+ `;
104
+ xml += ` <news:language>${escapeXml(entry.news.language)}</news:language>
105
+ `;
106
+ xml += ` </news:publication>
107
+ `;
108
+ xml += ` <news:publication_date>${nDate}</news:publication_date>
109
+ `;
110
+ xml += ` <news:title>${escapeXml(entry.news.title)}</news:title>
111
+ `;
112
+ xml += ` </news:news>
113
+ `;
114
+ }
115
+ xml += ` </url>
116
+ `;
117
+ }
118
+ xml += `</urlset>`;
119
+ return xml;
120
+ }
121
+
122
+ // src/index.ts
123
+ function getServerSitemapResponse(entries) {
124
+ const xml = generateXml(entries);
125
+ return new Response(xml, {
126
+ headers: {
127
+ "Content-Type": "application/xml",
128
+ "Cache-Control": "public, s-maxage=86400, stale-while-revalidate"
129
+ }
130
+ });
131
+ }
132
+ export {
133
+ getServerSitemapResponse
134
+ };
135
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/xml-escape.ts","../src/core/generator.ts","../src/index.ts"],"sourcesContent":["export function escapeXml(unsafe: string): string {\r\n return unsafe.replace(/[<>&\"']/g, (c) => {\r\n switch (c) {\r\n case '<': return '&lt;';\r\n case '>': return '&gt;';\r\n case '&': return '&amp;';\r\n case '\"': return '&quot;';\r\n case \"'\": return '&apos;';\r\n default: return c;\r\n }\r\n });\r\n}","import { SitemapEntry } from '../types/sitemap.js';\r\nimport { escapeXml } from '../utils/xml-escape.js';\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 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 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 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 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 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 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}","import { 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 ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "next-advanced-sitemap",
3
+ "version": "1.0.0",
4
+ "type": "module",
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
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "dev": "tsup src/index.ts --watch",
21
+ "build": "tsup",
22
+ "test": "vitest run",
23
+ "prepublishOnly": "npm run test && npm run build"
24
+ },
25
+ "keywords": [
26
+ "nextjs",
27
+ "sitemap",
28
+ "seo",
29
+ "google-images",
30
+ "google-video",
31
+ "google-news",
32
+ "hreflang",
33
+ "multilingual",
34
+ "typescript",
35
+ "app-router"
36
+ ],
37
+ "author": "fomadev",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/fomadev/next-advanced-sitemap.git"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/fomadev/next-advanced-sitemap/issues"
45
+ },
46
+ "homepage": "https://github.com/fomadev/next-advanced-sitemap#readme"
47
+ }