next-advanced-sitemap 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -17
- package/README.md +68 -57
- package/dist/index.cjs +13 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
FomaDev Public License (FPL)
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026
|
|
3
|
+
Copyright (c) 2026 FomaDev. All rights reserved.
|
|
4
4
|
|
|
5
|
-
|
|
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:
|
|
5
|
+
This license protects next-advanced-sitemap innovation while encouraging community collaboration.
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
1. Personal and Educational Use
|
|
8
|
+
The use of the next-advanced-sitemap library is entirely free for developers and companies, including within commercial projects, as long as it is used as a dependency. You may integrate and use this tool in your websites or applications without any payment to FomaDev.
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
2. Contributions (Fork & Pull Request)
|
|
11
|
+
Forks are authorized ONLY for contributing to the official FomaDev project. Any improvements, bug fixes, or new features must be submitted via a Pull Request to the original repository. By contributing, you agree that your code will be integrated under this same license.
|
|
12
|
+
|
|
13
|
+
3. Commercial Use and Competition (PAID)
|
|
14
|
+
A paid commercial license is MANDATORY for the following cases:
|
|
15
|
+
- Selling, redistributing, or sub-licensing the source code of this library.
|
|
16
|
+
- Creating a derivative sitemap generation tool or library based on this source code.
|
|
17
|
+
- Integrating this source code directly into another commercial library or framework intended for sale.
|
|
18
|
+
- Building a paid "Sitemap as a Service" platform based on this specific engine.
|
|
19
|
+
|
|
20
|
+
4. Prohibition of Independent Forks
|
|
21
|
+
Maintaining a separate version (permanent Fork) of this project to bypass FomaDev's authority or to create a competing package on registries (such as npm) is strictly prohibited.
|
|
22
|
+
|
|
23
|
+
5. Licensing Contact
|
|
24
|
+
To obtain commercial authorization, negotiate an exploitation license, or for any questions regarding professional use, contact Fordi (FomaDev) directly.
|
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ While Next.js provides a built-in `MetadataRoute.Sitemap` utility, it currently
|
|
|
12
12
|
- **Google Video Support**: Improve search visibility for video content with thumbnail and description metadata.
|
|
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
|
+
- **Strict Validation (v1.0.1)**: Built-in safety checks to ensure all URLs follow absolute protocols (http/https), preventing search engine rejection.
|
|
15
16
|
- **Developer Experience**: Fully typed with TypeScript, zero external dependencies, and optimized for Next.js Route Handlers.
|
|
16
17
|
|
|
17
18
|
## Installation
|
|
@@ -24,11 +25,11 @@ npm install next-advanced-sitemap
|
|
|
24
25
|
|
|
25
26
|
To implement an advanced sitemap in the Next.js App Router, create a Route Handler at `app/sitemap.xml/route.ts`.
|
|
26
27
|
|
|
27
|
-
```
|
|
28
|
-
import { getServerSitemapResponse } from 'next-advanced-sitemap';
|
|
28
|
+
```typescript
|
|
29
|
+
import { getServerSitemapResponse, SitemapEntry } from 'next-advanced-sitemap';
|
|
29
30
|
|
|
30
31
|
export async function GET() {
|
|
31
|
-
const entries = [
|
|
32
|
+
const entries: SitemapEntry[] = [
|
|
32
33
|
{
|
|
33
34
|
url: 'https://fomadev.com',
|
|
34
35
|
lastmod: new Date(),
|
|
@@ -53,7 +54,7 @@ export async function GET() {
|
|
|
53
54
|
url: 'https://fomadev.com/video-tutorial',
|
|
54
55
|
videos: [
|
|
55
56
|
{
|
|
56
|
-
thumbnail_loc: 'https://fomadev.com/thumbs/tutorial.jpg',
|
|
57
|
+
thumbnail_loc: 'https://fomadev.com/thumbs/tutorial.jpg)',
|
|
57
58
|
title: 'Next.js Advanced SEO Tutorial',
|
|
58
59
|
description: 'Learn how to implement advanced sitemaps in Next.js.',
|
|
59
60
|
publication_date: new Date('2026-04-22')
|
|
@@ -75,65 +76,75 @@ Generates a standard Next.js `Response` object with the correct `application/xml
|
|
|
75
76
|
### SitemapEntry Object
|
|
76
77
|
|
|
77
78
|
<table>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
79
|
+
<thead>
|
|
80
|
+
<tr>
|
|
81
|
+
<th>Property</th>
|
|
82
|
+
<th>Type</th>
|
|
83
|
+
<th>Description</th>
|
|
84
|
+
</tr>
|
|
85
|
+
</thead>
|
|
86
|
+
<tbody>
|
|
87
|
+
<tr>
|
|
88
|
+
<td><code>url</code></td>
|
|
89
|
+
<td class="type-label">string</td>
|
|
90
|
+
<td>The absolute URL of the page (must start with <strong>http/https</strong>).</td>
|
|
91
|
+
</tr>
|
|
92
|
+
<tr>
|
|
93
|
+
<td><code>lastmod</code></td>
|
|
94
|
+
<td class="type-label">Date | string</td>
|
|
95
|
+
<td>(Optional) Last modification date in ISO format.</td>
|
|
96
|
+
</tr>
|
|
97
|
+
<tr>
|
|
98
|
+
<td><code>changefreq</code></td>
|
|
99
|
+
<td class="type-label">string</td>
|
|
100
|
+
<td>(Optional) Search engine hint (always, hourly, daily, etc.).</td>
|
|
101
|
+
</tr>
|
|
102
|
+
<tr>
|
|
103
|
+
<td><code>priority</code></td>
|
|
104
|
+
<td class="type-label">number</td>
|
|
105
|
+
<td>(Optional) Priority of the URL (0.0 to 1.0).</td>
|
|
106
|
+
</tr>
|
|
107
|
+
<tr>
|
|
108
|
+
<td><code>images</code></td>
|
|
109
|
+
<td class="type-label">SitemapImage[]</td>
|
|
110
|
+
<td>(Optional) Array of image metadata for Google Images.</td>
|
|
111
|
+
</tr>
|
|
112
|
+
<tr>
|
|
113
|
+
<td><code>videos</code></td>
|
|
114
|
+
<td class="type-label">SitemapVideo[]</td>
|
|
115
|
+
<td>(Optional) Array of video metadata for Google Videos.</td>
|
|
116
|
+
</tr>
|
|
117
|
+
<tr>
|
|
118
|
+
<td><code>news</code></td>
|
|
119
|
+
<td class="type-label">SitemapNews</td>
|
|
120
|
+
<td>(Optional) Metadata for Google News indexing.</td>
|
|
121
|
+
</tr>
|
|
122
|
+
<tr>
|
|
123
|
+
<td><code>alternates</code></td>
|
|
124
|
+
<td class="type-label">SitemapAlternate[]</td>
|
|
125
|
+
<td>(Optional) Regional alternate URLs (Hreflang).</td>
|
|
126
|
+
</tr>
|
|
127
|
+
</tbody>
|
|
127
128
|
</table>
|
|
128
129
|
|
|
129
130
|
## Technical Implementation
|
|
130
131
|
|
|
131
|
-
|
|
132
|
+
### Validation & Safety
|
|
133
|
+
|
|
134
|
+
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
|
+
### Performance
|
|
137
|
+
|
|
138
|
+
This library uses an efficient string-building approach to ensure a minimal memory footprint. It automatically handles XML entity escaping for special characters (e.g., `&`, `<`, `>`) to maintain document integrity.
|
|
132
139
|
|
|
133
140
|
## License
|
|
134
141
|
|
|
135
|
-
|
|
142
|
+
This project is licensed under the [FomaDev Public License (FPL)](LICENSE).
|
|
143
|
+
|
|
144
|
+
* **Free Use**: Authorized for personal and commercial projects as a dependency.
|
|
145
|
+
|
|
146
|
+
* **[Contributions](CONTRIBUTING.md)**: Authorized via Pull Requests to the official repository only.
|
|
136
147
|
|
|
137
|
-
|
|
148
|
+
* **Restrictions**: Independent forks, redistribution of source code, or building competing products based on this engine require a paid commercial license.
|
|
138
149
|
|
|
139
|
-
|
|
150
|
+
See the [LICENSE](LICENSE) file for the full legal text.
|
package/dist/index.cjs
CHANGED
|
@@ -45,6 +45,13 @@ function escapeXml(unsafe) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// src/core/generator.ts
|
|
48
|
+
function validateUrl(url, context) {
|
|
49
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`[next-advanced-sitemap] Invalid URL in ${context}: "${url}". URLs must start with http:// or https://`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
48
55
|
function generateXml(entries) {
|
|
49
56
|
let xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
50
57
|
`;
|
|
@@ -59,12 +66,14 @@ function generateXml(entries) {
|
|
|
59
66
|
xml += ` xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
|
60
67
|
`;
|
|
61
68
|
for (const entry of entries) {
|
|
69
|
+
validateUrl(entry.url, "main entry");
|
|
62
70
|
xml += ` <url>
|
|
63
71
|
`;
|
|
64
72
|
xml += ` <loc>${escapeXml(entry.url)}</loc>
|
|
65
73
|
`;
|
|
66
74
|
if (entry.alternates?.length) {
|
|
67
75
|
for (const alt of entry.alternates) {
|
|
76
|
+
validateUrl(alt.href, "alternate link");
|
|
68
77
|
xml += ` <xhtml:link rel="alternate" hreflang="${escapeXml(alt.hreflang)}" href="${escapeXml(alt.href)}" />
|
|
69
78
|
`;
|
|
70
79
|
}
|
|
@@ -84,6 +93,7 @@ function generateXml(entries) {
|
|
|
84
93
|
}
|
|
85
94
|
if (entry.images?.length) {
|
|
86
95
|
for (const img of entry.images) {
|
|
96
|
+
validateUrl(img.loc, "image location");
|
|
87
97
|
xml += ` <image:image>
|
|
88
98
|
`;
|
|
89
99
|
xml += ` <image:loc>${escapeXml(img.loc)}</image:loc>
|
|
@@ -98,6 +108,9 @@ function generateXml(entries) {
|
|
|
98
108
|
}
|
|
99
109
|
if (entry.videos?.length) {
|
|
100
110
|
for (const vid of entry.videos) {
|
|
111
|
+
validateUrl(vid.thumbnail_loc, "video thumbnail");
|
|
112
|
+
if (vid.content_loc) validateUrl(vid.content_loc, "video content location");
|
|
113
|
+
if (vid.player_loc) validateUrl(vid.player_loc, "video player location");
|
|
101
114
|
xml += ` <video:video>
|
|
102
115
|
`;
|
|
103
116
|
xml += ` <video:thumbnail_loc>${escapeXml(vid.thumbnail_loc)}</video:thumbnail_loc>
|
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":["
|
|
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":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,13 @@ function escapeXml(unsafe) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
// src/core/generator.ts
|
|
22
|
+
function validateUrl(url, context) {
|
|
23
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`[next-advanced-sitemap] Invalid URL in ${context}: "${url}". URLs must start with http:// or https://`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
22
29
|
function generateXml(entries) {
|
|
23
30
|
let xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
24
31
|
`;
|
|
@@ -33,12 +40,14 @@ function generateXml(entries) {
|
|
|
33
40
|
xml += ` xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
|
34
41
|
`;
|
|
35
42
|
for (const entry of entries) {
|
|
43
|
+
validateUrl(entry.url, "main entry");
|
|
36
44
|
xml += ` <url>
|
|
37
45
|
`;
|
|
38
46
|
xml += ` <loc>${escapeXml(entry.url)}</loc>
|
|
39
47
|
`;
|
|
40
48
|
if (entry.alternates?.length) {
|
|
41
49
|
for (const alt of entry.alternates) {
|
|
50
|
+
validateUrl(alt.href, "alternate link");
|
|
42
51
|
xml += ` <xhtml:link rel="alternate" hreflang="${escapeXml(alt.hreflang)}" href="${escapeXml(alt.href)}" />
|
|
43
52
|
`;
|
|
44
53
|
}
|
|
@@ -58,6 +67,7 @@ function generateXml(entries) {
|
|
|
58
67
|
}
|
|
59
68
|
if (entry.images?.length) {
|
|
60
69
|
for (const img of entry.images) {
|
|
70
|
+
validateUrl(img.loc, "image location");
|
|
61
71
|
xml += ` <image:image>
|
|
62
72
|
`;
|
|
63
73
|
xml += ` <image:loc>${escapeXml(img.loc)}</image:loc>
|
|
@@ -72,6 +82,9 @@ function generateXml(entries) {
|
|
|
72
82
|
}
|
|
73
83
|
if (entry.videos?.length) {
|
|
74
84
|
for (const vid of entry.videos) {
|
|
85
|
+
validateUrl(vid.thumbnail_loc, "video thumbnail");
|
|
86
|
+
if (vid.content_loc) validateUrl(vid.content_loc, "video content location");
|
|
87
|
+
if (vid.player_loc) validateUrl(vid.player_loc, "video player location");
|
|
75
88
|
xml += ` <video:video>
|
|
76
89
|
`;
|
|
77
90
|
xml += ` <video:thumbnail_loc>${escapeXml(vid.thumbnail_loc)}</video:thumbnail_loc>
|
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":["
|
|
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":"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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.1",
|
|
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",
|