@onruntime/next-sitemap 0.1.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/README.md +271 -0
- package/dist/app/index.cjs +211 -0
- package/dist/app/index.d.cts +105 -0
- package/dist/app/index.d.ts +105 -0
- package/dist/app/index.js +208 -0
- package/dist/index.cjs +103 -0
- package/dist/index.d.cts +111 -0
- package/dist/index.d.ts +111 -0
- package/dist/index.js +95 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# @onruntime/next-sitemap
|
|
2
|
+
|
|
3
|
+
Dynamic sitemap generation for Next.js with automatic route discovery.
|
|
4
|
+
|
|
5
|
+
## Example
|
|
6
|
+
|
|
7
|
+
- [Next.js App Router](https://github.com/onRuntime/onruntime/tree/master/examples/next-sitemap/app)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Automatic route discovery using `require.context`
|
|
12
|
+
- Calls `generateStaticParams` for dynamic routes
|
|
13
|
+
- Multi-sitemap support with sitemap index (for sites with >50,000 URLs)
|
|
14
|
+
- hreflang alternates for i18n
|
|
15
|
+
- Fully static generation (SSG)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add @onruntime/next-sitemap
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### Next.js App Router
|
|
26
|
+
|
|
27
|
+
#### 1. Create the sitemap index route
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// app/sitemap.xml/route.ts
|
|
31
|
+
import { createSitemapIndexHandler } from "@onruntime/next-sitemap/app";
|
|
32
|
+
|
|
33
|
+
export const dynamic = "force-static";
|
|
34
|
+
|
|
35
|
+
// @ts-expect-error - require.context is a webpack/turbopack feature
|
|
36
|
+
const pagesContext = require.context("../", true, /\/page\.tsx$/);
|
|
37
|
+
|
|
38
|
+
const { GET } = createSitemapIndexHandler({
|
|
39
|
+
baseUrl: "https://example.com",
|
|
40
|
+
pagesContext,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export { GET };
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
#### 2. Create the individual sitemap route
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// app/sitemap.xml/[id]/route.ts
|
|
50
|
+
import { createSitemapHandler } from "@onruntime/next-sitemap/app";
|
|
51
|
+
|
|
52
|
+
export const dynamic = "force-static";
|
|
53
|
+
|
|
54
|
+
// @ts-expect-error - require.context is a webpack/turbopack feature
|
|
55
|
+
const pagesContext = require.context("../../", true, /\/page\.tsx$/);
|
|
56
|
+
|
|
57
|
+
const { generateStaticParams, GET } = createSitemapHandler({
|
|
58
|
+
baseUrl: "https://example.com",
|
|
59
|
+
pagesContext,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export { generateStaticParams, GET };
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
#### With i18n (optional)
|
|
66
|
+
|
|
67
|
+
If your app uses a `[locale]` segment for internationalization, just add `locales` and `defaultLocale`. The `localeSegment` is automatically set to `"[locale]"`:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// app/sitemap.xml/route.ts
|
|
71
|
+
const pagesContext = require.context("../[locale]", true, /\/page\.tsx$/);
|
|
72
|
+
|
|
73
|
+
const { GET } = createSitemapIndexHandler({
|
|
74
|
+
baseUrl: "https://example.com",
|
|
75
|
+
locales: ["en", "fr"],
|
|
76
|
+
defaultLocale: "en",
|
|
77
|
+
pagesContext,
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
If you use a different segment name (e.g., `[lang]`), specify it explicitly:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
localeSegment: "[lang]", // Custom segment name
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### 3. Add URL rewrite in next.config.ts
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// next.config.ts
|
|
91
|
+
import type { NextConfig } from "next";
|
|
92
|
+
|
|
93
|
+
const nextConfig: NextConfig = {
|
|
94
|
+
async rewrites() {
|
|
95
|
+
return [
|
|
96
|
+
{
|
|
97
|
+
source: "/sitemap-:id.xml",
|
|
98
|
+
destination: "/sitemap.xml/:id",
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export default nextConfig;
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Configuration Options
|
|
108
|
+
|
|
109
|
+
| Option | Type | Default | Description |
|
|
110
|
+
|--------|------|---------|-------------|
|
|
111
|
+
| `baseUrl` | `string` | required | Base URL of the site |
|
|
112
|
+
| `locales` | `string[]` | `[]` | List of supported locales |
|
|
113
|
+
| `defaultLocale` | `string` | `undefined` | Default locale (URLs without prefix) |
|
|
114
|
+
| `urlsPerSitemap` | `number` | `5000` | Max URLs per sitemap file |
|
|
115
|
+
| `localeSegment` | `string` | auto | Auto-detected as `"[locale]"` when i18n is configured. Override for custom names like `"[lang]"`. |
|
|
116
|
+
| `pagesContext` | `object` | required | Result of `require.context()` |
|
|
117
|
+
| `exclude` | `string[]` or `function` | `undefined` | Patterns or function to exclude routes |
|
|
118
|
+
| `priority` | `number`, `"auto"`, or `function` | `"auto"` | Priority calculation (auto = depth-based) |
|
|
119
|
+
| `changeFreq` | `ChangeFrequency` or `function` | `"weekly"` | Change frequency for entries |
|
|
120
|
+
| `additionalSitemaps` | `string[]` | `[]` | Additional sitemaps to include in index |
|
|
121
|
+
|
|
122
|
+
#### Exclude Routes
|
|
123
|
+
|
|
124
|
+
Exclude specific routes from the sitemap using glob patterns or a function:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// Using glob patterns
|
|
128
|
+
const { GET } = createSitemapIndexHandler({
|
|
129
|
+
baseUrl: "https://example.com",
|
|
130
|
+
pagesContext,
|
|
131
|
+
exclude: ["/admin/*", "/api/*", "/private/**"],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Using a function
|
|
135
|
+
const { GET } = createSitemapIndexHandler({
|
|
136
|
+
baseUrl: "https://example.com",
|
|
137
|
+
pagesContext,
|
|
138
|
+
exclude: (path) => path.startsWith("/internal"),
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### Priority
|
|
143
|
+
|
|
144
|
+
By default, priority is calculated automatically based on URL depth:
|
|
145
|
+
- `/` → 1.0
|
|
146
|
+
- `/about` → 0.8
|
|
147
|
+
- `/blog/post` → 0.6
|
|
148
|
+
- Minimum: 0.1
|
|
149
|
+
|
|
150
|
+
You can override with a fixed value or a function:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// Fixed priority for all URLs
|
|
154
|
+
priority: 0.8,
|
|
155
|
+
|
|
156
|
+
// Custom function
|
|
157
|
+
priority: (path) => {
|
|
158
|
+
if (path === "/") return 1.0;
|
|
159
|
+
if (path.startsWith("/products")) return 0.9;
|
|
160
|
+
return 0.5;
|
|
161
|
+
},
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### Change Frequency
|
|
165
|
+
|
|
166
|
+
Set a default change frequency or customize per route:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// Fixed value
|
|
170
|
+
changeFreq: "daily",
|
|
171
|
+
|
|
172
|
+
// Custom function
|
|
173
|
+
changeFreq: (path) => {
|
|
174
|
+
if (path === "/") return "daily";
|
|
175
|
+
if (path.startsWith("/blog")) return "weekly";
|
|
176
|
+
return "monthly";
|
|
177
|
+
},
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### Additional Sitemaps
|
|
181
|
+
|
|
182
|
+
Include custom sitemaps in the sitemap index (e.g., for API-fetched data):
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
const { GET } = createSitemapIndexHandler({
|
|
186
|
+
baseUrl: "https://example.com",
|
|
187
|
+
pagesContext,
|
|
188
|
+
additionalSitemaps: ["/products-sitemap.xml", "/blog-sitemap.xml"],
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### How It Works
|
|
193
|
+
|
|
194
|
+
1. `require.context` scans your app directory at build time
|
|
195
|
+
2. For each page found, it extracts the route path
|
|
196
|
+
3. For dynamic routes (e.g., `/projects/[id]`), it calls `generateStaticParams`
|
|
197
|
+
4. URLs are paginated into multiple sitemaps (default: 5000 URLs each)
|
|
198
|
+
5. A sitemap index lists all individual sitemaps
|
|
199
|
+
|
|
200
|
+
### Generated URLs
|
|
201
|
+
|
|
202
|
+
For a route structure like:
|
|
203
|
+
```
|
|
204
|
+
app/
|
|
205
|
+
├── page.tsx → /
|
|
206
|
+
├── about/page.tsx → /about
|
|
207
|
+
├── posts/page.tsx → /posts
|
|
208
|
+
└── posts/[slug]/page.tsx → /posts/hello-world, /posts/getting-started, ...
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The sitemap will include:
|
|
212
|
+
- All static routes
|
|
213
|
+
- All dynamic routes resolved via `generateStaticParams`
|
|
214
|
+
- hreflang alternates for each locale (if i18n is configured)
|
|
215
|
+
|
|
216
|
+
**Note:** Dynamic routes without `generateStaticParams` are skipped. If you need to include routes that fetch data at runtime (e.g., from an API), you can create a separate sitemap using [Next.js native sitemap generation](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap):
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
// app/custom-sitemap.xml/route.ts
|
|
220
|
+
import { generateSitemapXml } from "@onruntime/next-sitemap";
|
|
221
|
+
|
|
222
|
+
export async function GET() {
|
|
223
|
+
// Fetch your dynamic data
|
|
224
|
+
const products = await fetch("https://api.example.com/products").then(r => r.json());
|
|
225
|
+
|
|
226
|
+
const entries = products.map((p: { slug: string }) => ({
|
|
227
|
+
url: `https://example.com/products/${p.slug}`,
|
|
228
|
+
lastModified: new Date(),
|
|
229
|
+
}));
|
|
230
|
+
|
|
231
|
+
return new Response(generateSitemapXml(entries), {
|
|
232
|
+
headers: { "Content-Type": "application/xml" },
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Example Output
|
|
238
|
+
|
|
239
|
+
**Sitemap Index** (`/sitemap.xml`):
|
|
240
|
+
```xml
|
|
241
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
242
|
+
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
243
|
+
<sitemap>
|
|
244
|
+
<loc>https://example.com/sitemap-0.xml</loc>
|
|
245
|
+
<lastmod>2024-01-01T00:00:00.000Z</lastmod>
|
|
246
|
+
</sitemap>
|
|
247
|
+
</sitemapindex>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Individual Sitemap** (`/sitemap-0.xml`):
|
|
251
|
+
```xml
|
|
252
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
253
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
254
|
+
<url>
|
|
255
|
+
<loc>https://example.com/</loc>
|
|
256
|
+
<lastmod>2024-01-01T00:00:00.000Z</lastmod>
|
|
257
|
+
<changefreq>weekly</changefreq>
|
|
258
|
+
<priority>1</priority>
|
|
259
|
+
</url>
|
|
260
|
+
<url>
|
|
261
|
+
<loc>https://example.com/posts/hello-world</loc>
|
|
262
|
+
<lastmod>2024-01-01T00:00:00.000Z</lastmod>
|
|
263
|
+
<changefreq>weekly</changefreq>
|
|
264
|
+
<priority>0.7</priority>
|
|
265
|
+
</url>
|
|
266
|
+
</urlset>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## License
|
|
270
|
+
|
|
271
|
+
MIT
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
function calculateDepthPriority(pathname) {
|
|
5
|
+
if (pathname === "/") return 1;
|
|
6
|
+
const depth = pathname.split("/").filter(Boolean).length;
|
|
7
|
+
return Math.max(0.1, 1 - depth * 0.2);
|
|
8
|
+
}
|
|
9
|
+
function shouldExclude(pathname, exclude) {
|
|
10
|
+
if (!exclude) return false;
|
|
11
|
+
if (typeof exclude === "function") {
|
|
12
|
+
return exclude(pathname);
|
|
13
|
+
}
|
|
14
|
+
return exclude.some((pattern) => {
|
|
15
|
+
const regexPattern = pattern.replace(/\*\*/g, "{{DOUBLE_STAR}}").replace(/\*/g, "[^/]*").replace(/\{\{DOUBLE_STAR\}\}/g, ".*");
|
|
16
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
17
|
+
return regex.test(pathname);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function getPriority(pathname, priority) {
|
|
21
|
+
if (priority === void 0 || priority === "auto") {
|
|
22
|
+
return calculateDepthPriority(pathname);
|
|
23
|
+
}
|
|
24
|
+
if (typeof priority === "function") {
|
|
25
|
+
return priority(pathname);
|
|
26
|
+
}
|
|
27
|
+
return priority;
|
|
28
|
+
}
|
|
29
|
+
function getChangeFreq(pathname, changeFreq) {
|
|
30
|
+
if (changeFreq === void 0) {
|
|
31
|
+
return "weekly";
|
|
32
|
+
}
|
|
33
|
+
if (typeof changeFreq === "function") {
|
|
34
|
+
return changeFreq(pathname);
|
|
35
|
+
}
|
|
36
|
+
return changeFreq;
|
|
37
|
+
}
|
|
38
|
+
function buildUrl(baseUrl, pathname, locale, defaultLocale) {
|
|
39
|
+
if (!locale || locale === defaultLocale) {
|
|
40
|
+
return `${baseUrl}${pathname}`;
|
|
41
|
+
}
|
|
42
|
+
return `${baseUrl}/${locale}${pathname}`;
|
|
43
|
+
}
|
|
44
|
+
function generateSitemapXml(entries) {
|
|
45
|
+
const hasAlternates = entries.some((e) => e.alternates?.languages);
|
|
46
|
+
const xmlns = hasAlternates ? 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"' : 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"';
|
|
47
|
+
const urlEntries = entries.map((entry) => {
|
|
48
|
+
const parts = [` <loc>${entry.url}</loc>`];
|
|
49
|
+
if (entry.alternates?.languages) {
|
|
50
|
+
for (const [lang, href] of Object.entries(entry.alternates.languages)) {
|
|
51
|
+
parts.push(` <xhtml:link rel="alternate" hreflang="${lang}" href="${href}" />`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (entry.lastModified) {
|
|
55
|
+
const date = entry.lastModified instanceof Date ? entry.lastModified.toISOString() : entry.lastModified;
|
|
56
|
+
parts.push(` <lastmod>${date}</lastmod>`);
|
|
57
|
+
}
|
|
58
|
+
if (entry.changeFrequency) {
|
|
59
|
+
parts.push(` <changefreq>${entry.changeFrequency}</changefreq>`);
|
|
60
|
+
}
|
|
61
|
+
if (entry.priority !== void 0) {
|
|
62
|
+
parts.push(` <priority>${entry.priority}</priority>`);
|
|
63
|
+
}
|
|
64
|
+
return ` <url>
|
|
65
|
+
${parts.join("\n")}
|
|
66
|
+
</url>`;
|
|
67
|
+
}).join("\n");
|
|
68
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
69
|
+
<urlset ${xmlns}>
|
|
70
|
+
${urlEntries}
|
|
71
|
+
</urlset>`;
|
|
72
|
+
}
|
|
73
|
+
function generateSitemapIndexXml(baseUrl, sitemapCount, options) {
|
|
74
|
+
const { sitemapPattern = "/sitemap-{id}.xml", additionalSitemaps = [] } = options || {};
|
|
75
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
76
|
+
const paginatedEntries = Array.from({ length: sitemapCount }, (_, i) => {
|
|
77
|
+
const loc = `${baseUrl}${sitemapPattern.replace("{id}", String(i))}`;
|
|
78
|
+
return ` <sitemap>
|
|
79
|
+
<loc>${loc}</loc>
|
|
80
|
+
<lastmod>${now}</lastmod>
|
|
81
|
+
</sitemap>`;
|
|
82
|
+
});
|
|
83
|
+
const additionalEntries = additionalSitemaps.map((sitemap) => {
|
|
84
|
+
const loc = sitemap.startsWith("http") ? sitemap : `${baseUrl}${sitemap}`;
|
|
85
|
+
return ` <sitemap>
|
|
86
|
+
<loc>${loc}</loc>
|
|
87
|
+
<lastmod>${now}</lastmod>
|
|
88
|
+
</sitemap>`;
|
|
89
|
+
});
|
|
90
|
+
const allEntries = [...paginatedEntries, ...additionalEntries].join("\n");
|
|
91
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
92
|
+
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
93
|
+
${allEntries}
|
|
94
|
+
</sitemapindex>`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/app/index.ts
|
|
98
|
+
function extractRoutes(pagesContext, localeSegment) {
|
|
99
|
+
const routes = [];
|
|
100
|
+
for (const key of pagesContext.keys()) {
|
|
101
|
+
if (key.includes("[...")) continue;
|
|
102
|
+
const pageModule = pagesContext(key);
|
|
103
|
+
let pathname = key.replace("./", "/").replace("/page.tsx", "").replace(new RegExp(`^/${localeSegment.replace(/[[\]]/g, "\\$&")}`), "").replace(/\/\([^)]+\)/g, "") || "/";
|
|
104
|
+
if (pathname === "") pathname = "/";
|
|
105
|
+
const dynamicSegments = pathname.match(/\[([^\]]+)\]/g)?.map((s) => s.slice(1, -1)) || [];
|
|
106
|
+
routes.push({
|
|
107
|
+
pathname,
|
|
108
|
+
dynamicSegments,
|
|
109
|
+
getParams: pageModule.generateStaticParams || null
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return routes;
|
|
113
|
+
}
|
|
114
|
+
async function getAllPaths(routes) {
|
|
115
|
+
const allPaths = ["/"];
|
|
116
|
+
const seenPaths = /* @__PURE__ */ new Set(["/"]);
|
|
117
|
+
for (const route of routes) {
|
|
118
|
+
if (route.dynamicSegments.length === 0) {
|
|
119
|
+
if (route.pathname !== "/" && !seenPaths.has(route.pathname)) {
|
|
120
|
+
allPaths.push(route.pathname);
|
|
121
|
+
seenPaths.add(route.pathname);
|
|
122
|
+
}
|
|
123
|
+
} else if (route.getParams) {
|
|
124
|
+
const params = await route.getParams();
|
|
125
|
+
for (const param of params) {
|
|
126
|
+
let dynamicPath = route.pathname;
|
|
127
|
+
for (const segment of route.dynamicSegments) {
|
|
128
|
+
dynamicPath = dynamicPath.replace(`[${segment}]`, param[segment]);
|
|
129
|
+
}
|
|
130
|
+
if (!seenPaths.has(dynamicPath)) {
|
|
131
|
+
allPaths.push(dynamicPath);
|
|
132
|
+
seenPaths.add(dynamicPath);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return allPaths;
|
|
138
|
+
}
|
|
139
|
+
function pathsToEntries(paths, config) {
|
|
140
|
+
const { baseUrl, locales = [], defaultLocale, exclude, priority, changeFreq } = config;
|
|
141
|
+
const filteredPaths = paths.filter((pathname) => !shouldExclude(pathname, exclude));
|
|
142
|
+
return filteredPaths.map((pathname) => {
|
|
143
|
+
const entry = {
|
|
144
|
+
url: buildUrl(baseUrl, pathname, defaultLocale, defaultLocale),
|
|
145
|
+
lastModified: /* @__PURE__ */ new Date(),
|
|
146
|
+
changeFrequency: getChangeFreq(pathname, changeFreq),
|
|
147
|
+
priority: getPriority(pathname, priority)
|
|
148
|
+
};
|
|
149
|
+
if (locales.length > 0) {
|
|
150
|
+
entry.alternates = {
|
|
151
|
+
languages: Object.fromEntries(
|
|
152
|
+
locales.map((locale) => [
|
|
153
|
+
locale,
|
|
154
|
+
buildUrl(baseUrl, pathname, locale, defaultLocale)
|
|
155
|
+
])
|
|
156
|
+
)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return entry;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function createSitemapIndexHandler(options) {
|
|
163
|
+
const { urlsPerSitemap = 5e3, locales = [], defaultLocale, additionalSitemaps, exclude } = options;
|
|
164
|
+
const localeSegment = options.localeSegment ?? (locales.length > 0 || defaultLocale ? "[locale]" : "");
|
|
165
|
+
const routes = extractRoutes(options.pagesContext, localeSegment);
|
|
166
|
+
return {
|
|
167
|
+
GET: async () => {
|
|
168
|
+
const allPaths = await getAllPaths(routes);
|
|
169
|
+
const filteredPaths = allPaths.filter((pathname) => !shouldExclude(pathname, exclude));
|
|
170
|
+
const sitemapCount = Math.max(1, Math.ceil(filteredPaths.length / urlsPerSitemap));
|
|
171
|
+
const xml = generateSitemapIndexXml(options.baseUrl, sitemapCount, {
|
|
172
|
+
additionalSitemaps
|
|
173
|
+
});
|
|
174
|
+
return new Response(xml, {
|
|
175
|
+
headers: { "Content-Type": "application/xml" }
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function createSitemapHandler(options) {
|
|
181
|
+
const { urlsPerSitemap = 5e3, locales = [], defaultLocale, exclude } = options;
|
|
182
|
+
const localeSegment = options.localeSegment ?? (locales.length > 0 || defaultLocale ? "[locale]" : "");
|
|
183
|
+
const routes = extractRoutes(options.pagesContext, localeSegment);
|
|
184
|
+
const getFilteredPaths = async () => {
|
|
185
|
+
const allPaths = await getAllPaths(routes);
|
|
186
|
+
return allPaths.filter((pathname) => !shouldExclude(pathname, exclude));
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
generateStaticParams: async () => {
|
|
190
|
+
const filteredPaths = await getFilteredPaths();
|
|
191
|
+
const sitemapCount = Math.max(1, Math.ceil(filteredPaths.length / urlsPerSitemap));
|
|
192
|
+
return Array.from({ length: sitemapCount }, (_, i) => ({ id: String(i) }));
|
|
193
|
+
},
|
|
194
|
+
GET: async (_request, { params }) => {
|
|
195
|
+
const { id } = await params;
|
|
196
|
+
const sitemapId = parseInt(id, 10);
|
|
197
|
+
const filteredPaths = await getFilteredPaths();
|
|
198
|
+
const start = sitemapId * urlsPerSitemap;
|
|
199
|
+
const end = start + urlsPerSitemap;
|
|
200
|
+
const paths = filteredPaths.slice(start, end);
|
|
201
|
+
const entries = pathsToEntries(paths, { ...options, exclude: void 0 });
|
|
202
|
+
const xml = generateSitemapXml(entries);
|
|
203
|
+
return new Response(xml, {
|
|
204
|
+
headers: { "Content-Type": "application/xml" }
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
exports.createSitemapHandler = createSitemapHandler;
|
|
211
|
+
exports.createSitemapIndexHandler = createSitemapIndexHandler;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
type ChangeFrequency = "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
|
|
2
|
+
interface SitemapConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Base URL of the site (e.g., "https://example.com")
|
|
5
|
+
*/
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
/**
|
|
8
|
+
* List of supported locales (e.g., ["en", "fr"])
|
|
9
|
+
*/
|
|
10
|
+
locales?: string[];
|
|
11
|
+
/**
|
|
12
|
+
* Default locale (e.g., "en")
|
|
13
|
+
*/
|
|
14
|
+
defaultLocale?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Number of URLs per sitemap file (default: 5000)
|
|
17
|
+
*/
|
|
18
|
+
urlsPerSitemap?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Path to the app directory relative to the project root
|
|
21
|
+
* Default: "./src/app" or "./app"
|
|
22
|
+
*/
|
|
23
|
+
appDir?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Locale segment in the URL path (e.g., "[locale]")
|
|
26
|
+
* Auto-detected as "[locale]" when locales or defaultLocale is set
|
|
27
|
+
*/
|
|
28
|
+
localeSegment?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Exclude routes from the sitemap
|
|
31
|
+
* Can be an array of glob patterns or a function that returns true to exclude
|
|
32
|
+
* @example exclude: ["/admin/*", "/api/*"]
|
|
33
|
+
* @example exclude: (path) => path.startsWith("/private")
|
|
34
|
+
*/
|
|
35
|
+
exclude?: string[] | ((path: string) => boolean);
|
|
36
|
+
/**
|
|
37
|
+
* Priority for sitemap entries
|
|
38
|
+
* Can be a fixed number (0.0-1.0), a function, or "auto" for depth-based calculation
|
|
39
|
+
* Default: "auto" (1.0 - depth * 0.2, minimum 0.1)
|
|
40
|
+
* @example priority: 0.8
|
|
41
|
+
* @example priority: (path) => path === "/" ? 1.0 : 0.7
|
|
42
|
+
* @example priority: "auto"
|
|
43
|
+
*/
|
|
44
|
+
priority?: number | "auto" | ((path: string) => number);
|
|
45
|
+
/**
|
|
46
|
+
* Change frequency for sitemap entries
|
|
47
|
+
* Can be a fixed value or a function
|
|
48
|
+
* Default: "weekly"
|
|
49
|
+
* @example changeFreq: "daily"
|
|
50
|
+
* @example changeFreq: (path) => path === "/" ? "daily" : "weekly"
|
|
51
|
+
*/
|
|
52
|
+
changeFreq?: ChangeFrequency | ((path: string) => ChangeFrequency);
|
|
53
|
+
/**
|
|
54
|
+
* Additional sitemaps to include in the sitemap index
|
|
55
|
+
* Useful for custom sitemaps (e.g., products from an API)
|
|
56
|
+
* @example additionalSitemaps: ["/products-sitemap.xml", "/blog-sitemap.xml"]
|
|
57
|
+
*/
|
|
58
|
+
additionalSitemaps?: string[];
|
|
59
|
+
}
|
|
60
|
+
interface SitemapEntry {
|
|
61
|
+
url: string;
|
|
62
|
+
lastModified?: string | Date;
|
|
63
|
+
changeFrequency?: ChangeFrequency;
|
|
64
|
+
priority?: number;
|
|
65
|
+
alternates?: {
|
|
66
|
+
languages?: Record<string, string>;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
type PageModule = {
|
|
70
|
+
generateStaticParams?: () => Promise<Record<string, string>[]>;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
interface CreateSitemapHandlerOptions extends SitemapConfig {
|
|
74
|
+
/**
|
|
75
|
+
* The require.context result for page discovery
|
|
76
|
+
* Example: require.context('./[locale]', true, /\/page\.tsx$/)
|
|
77
|
+
*/
|
|
78
|
+
pagesContext: {
|
|
79
|
+
keys: () => string[];
|
|
80
|
+
(key: string): PageModule;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Create handlers for sitemap index route
|
|
85
|
+
* Use in: app/sitemap.xml/route.ts
|
|
86
|
+
*/
|
|
87
|
+
declare function createSitemapIndexHandler(options: CreateSitemapHandlerOptions): {
|
|
88
|
+
GET: () => Promise<Response>;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Create handlers for individual sitemap routes
|
|
92
|
+
* Use in: app/sitemap.xml/[id]/route.ts
|
|
93
|
+
*/
|
|
94
|
+
declare function createSitemapHandler(options: CreateSitemapHandlerOptions): {
|
|
95
|
+
generateStaticParams: () => Promise<{
|
|
96
|
+
id: string;
|
|
97
|
+
}[]>;
|
|
98
|
+
GET: (_request: Request, { params }: {
|
|
99
|
+
params: Promise<{
|
|
100
|
+
id: string;
|
|
101
|
+
}>;
|
|
102
|
+
}) => Promise<Response>;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export { type ChangeFrequency, type CreateSitemapHandlerOptions, type SitemapConfig, type SitemapEntry, createSitemapHandler, createSitemapIndexHandler };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
type ChangeFrequency = "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
|
|
2
|
+
interface SitemapConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Base URL of the site (e.g., "https://example.com")
|
|
5
|
+
*/
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
/**
|
|
8
|
+
* List of supported locales (e.g., ["en", "fr"])
|
|
9
|
+
*/
|
|
10
|
+
locales?: string[];
|
|
11
|
+
/**
|
|
12
|
+
* Default locale (e.g., "en")
|
|
13
|
+
*/
|
|
14
|
+
defaultLocale?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Number of URLs per sitemap file (default: 5000)
|
|
17
|
+
*/
|
|
18
|
+
urlsPerSitemap?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Path to the app directory relative to the project root
|
|
21
|
+
* Default: "./src/app" or "./app"
|
|
22
|
+
*/
|
|
23
|
+
appDir?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Locale segment in the URL path (e.g., "[locale]")
|
|
26
|
+
* Auto-detected as "[locale]" when locales or defaultLocale is set
|
|
27
|
+
*/
|
|
28
|
+
localeSegment?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Exclude routes from the sitemap
|
|
31
|
+
* Can be an array of glob patterns or a function that returns true to exclude
|
|
32
|
+
* @example exclude: ["/admin/*", "/api/*"]
|
|
33
|
+
* @example exclude: (path) => path.startsWith("/private")
|
|
34
|
+
*/
|
|
35
|
+
exclude?: string[] | ((path: string) => boolean);
|
|
36
|
+
/**
|
|
37
|
+
* Priority for sitemap entries
|
|
38
|
+
* Can be a fixed number (0.0-1.0), a function, or "auto" for depth-based calculation
|
|
39
|
+
* Default: "auto" (1.0 - depth * 0.2, minimum 0.1)
|
|
40
|
+
* @example priority: 0.8
|
|
41
|
+
* @example priority: (path) => path === "/" ? 1.0 : 0.7
|
|
42
|
+
* @example priority: "auto"
|
|
43
|
+
*/
|
|
44
|
+
priority?: number | "auto" | ((path: string) => number);
|
|
45
|
+
/**
|
|
46
|
+
* Change frequency for sitemap entries
|
|
47
|
+
* Can be a fixed value or a function
|
|
48
|
+
* Default: "weekly"
|
|
49
|
+
* @example changeFreq: "daily"
|
|
50
|
+
* @example changeFreq: (path) => path === "/" ? "daily" : "weekly"
|
|
51
|
+
*/
|
|
52
|
+
changeFreq?: ChangeFrequency | ((path: string) => ChangeFrequency);
|
|
53
|
+
/**
|
|
54
|
+
* Additional sitemaps to include in the sitemap index
|
|
55
|
+
* Useful for custom sitemaps (e.g., products from an API)
|
|
56
|
+
* @example additionalSitemaps: ["/products-sitemap.xml", "/blog-sitemap.xml"]
|
|
57
|
+
*/
|
|
58
|
+
additionalSitemaps?: string[];
|
|
59
|
+
}
|
|
60
|
+
interface SitemapEntry {
|
|
61
|
+
url: string;
|
|
62
|
+
lastModified?: string | Date;
|
|
63
|
+
changeFrequency?: ChangeFrequency;
|
|
64
|
+
priority?: number;
|
|
65
|
+
alternates?: {
|
|
66
|
+
languages?: Record<string, string>;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
type PageModule = {
|
|
70
|
+
generateStaticParams?: () => Promise<Record<string, string>[]>;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
interface CreateSitemapHandlerOptions extends SitemapConfig {
|
|
74
|
+
/**
|
|
75
|
+
* The require.context result for page discovery
|
|
76
|
+
* Example: require.context('./[locale]', true, /\/page\.tsx$/)
|
|
77
|
+
*/
|
|
78
|
+
pagesContext: {
|
|
79
|
+
keys: () => string[];
|
|
80
|
+
(key: string): PageModule;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Create handlers for sitemap index route
|
|
85
|
+
* Use in: app/sitemap.xml/route.ts
|
|
86
|
+
*/
|
|
87
|
+
declare function createSitemapIndexHandler(options: CreateSitemapHandlerOptions): {
|
|
88
|
+
GET: () => Promise<Response>;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Create handlers for individual sitemap routes
|
|
92
|
+
* Use in: app/sitemap.xml/[id]/route.ts
|
|
93
|
+
*/
|
|
94
|
+
declare function createSitemapHandler(options: CreateSitemapHandlerOptions): {
|
|
95
|
+
generateStaticParams: () => Promise<{
|
|
96
|
+
id: string;
|
|
97
|
+
}[]>;
|
|
98
|
+
GET: (_request: Request, { params }: {
|
|
99
|
+
params: Promise<{
|
|
100
|
+
id: string;
|
|
101
|
+
}>;
|
|
102
|
+
}) => Promise<Response>;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export { type ChangeFrequency, type CreateSitemapHandlerOptions, type SitemapConfig, type SitemapEntry, createSitemapHandler, createSitemapIndexHandler };
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
function calculateDepthPriority(pathname) {
|
|
3
|
+
if (pathname === "/") return 1;
|
|
4
|
+
const depth = pathname.split("/").filter(Boolean).length;
|
|
5
|
+
return Math.max(0.1, 1 - depth * 0.2);
|
|
6
|
+
}
|
|
7
|
+
function shouldExclude(pathname, exclude) {
|
|
8
|
+
if (!exclude) return false;
|
|
9
|
+
if (typeof exclude === "function") {
|
|
10
|
+
return exclude(pathname);
|
|
11
|
+
}
|
|
12
|
+
return exclude.some((pattern) => {
|
|
13
|
+
const regexPattern = pattern.replace(/\*\*/g, "{{DOUBLE_STAR}}").replace(/\*/g, "[^/]*").replace(/\{\{DOUBLE_STAR\}\}/g, ".*");
|
|
14
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
15
|
+
return regex.test(pathname);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function getPriority(pathname, priority) {
|
|
19
|
+
if (priority === void 0 || priority === "auto") {
|
|
20
|
+
return calculateDepthPriority(pathname);
|
|
21
|
+
}
|
|
22
|
+
if (typeof priority === "function") {
|
|
23
|
+
return priority(pathname);
|
|
24
|
+
}
|
|
25
|
+
return priority;
|
|
26
|
+
}
|
|
27
|
+
function getChangeFreq(pathname, changeFreq) {
|
|
28
|
+
if (changeFreq === void 0) {
|
|
29
|
+
return "weekly";
|
|
30
|
+
}
|
|
31
|
+
if (typeof changeFreq === "function") {
|
|
32
|
+
return changeFreq(pathname);
|
|
33
|
+
}
|
|
34
|
+
return changeFreq;
|
|
35
|
+
}
|
|
36
|
+
function buildUrl(baseUrl, pathname, locale, defaultLocale) {
|
|
37
|
+
if (!locale || locale === defaultLocale) {
|
|
38
|
+
return `${baseUrl}${pathname}`;
|
|
39
|
+
}
|
|
40
|
+
return `${baseUrl}/${locale}${pathname}`;
|
|
41
|
+
}
|
|
42
|
+
function generateSitemapXml(entries) {
|
|
43
|
+
const hasAlternates = entries.some((e) => e.alternates?.languages);
|
|
44
|
+
const xmlns = hasAlternates ? 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"' : 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"';
|
|
45
|
+
const urlEntries = entries.map((entry) => {
|
|
46
|
+
const parts = [` <loc>${entry.url}</loc>`];
|
|
47
|
+
if (entry.alternates?.languages) {
|
|
48
|
+
for (const [lang, href] of Object.entries(entry.alternates.languages)) {
|
|
49
|
+
parts.push(` <xhtml:link rel="alternate" hreflang="${lang}" href="${href}" />`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (entry.lastModified) {
|
|
53
|
+
const date = entry.lastModified instanceof Date ? entry.lastModified.toISOString() : entry.lastModified;
|
|
54
|
+
parts.push(` <lastmod>${date}</lastmod>`);
|
|
55
|
+
}
|
|
56
|
+
if (entry.changeFrequency) {
|
|
57
|
+
parts.push(` <changefreq>${entry.changeFrequency}</changefreq>`);
|
|
58
|
+
}
|
|
59
|
+
if (entry.priority !== void 0) {
|
|
60
|
+
parts.push(` <priority>${entry.priority}</priority>`);
|
|
61
|
+
}
|
|
62
|
+
return ` <url>
|
|
63
|
+
${parts.join("\n")}
|
|
64
|
+
</url>`;
|
|
65
|
+
}).join("\n");
|
|
66
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
67
|
+
<urlset ${xmlns}>
|
|
68
|
+
${urlEntries}
|
|
69
|
+
</urlset>`;
|
|
70
|
+
}
|
|
71
|
+
function generateSitemapIndexXml(baseUrl, sitemapCount, options) {
|
|
72
|
+
const { sitemapPattern = "/sitemap-{id}.xml", additionalSitemaps = [] } = options || {};
|
|
73
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
74
|
+
const paginatedEntries = Array.from({ length: sitemapCount }, (_, i) => {
|
|
75
|
+
const loc = `${baseUrl}${sitemapPattern.replace("{id}", String(i))}`;
|
|
76
|
+
return ` <sitemap>
|
|
77
|
+
<loc>${loc}</loc>
|
|
78
|
+
<lastmod>${now}</lastmod>
|
|
79
|
+
</sitemap>`;
|
|
80
|
+
});
|
|
81
|
+
const additionalEntries = additionalSitemaps.map((sitemap) => {
|
|
82
|
+
const loc = sitemap.startsWith("http") ? sitemap : `${baseUrl}${sitemap}`;
|
|
83
|
+
return ` <sitemap>
|
|
84
|
+
<loc>${loc}</loc>
|
|
85
|
+
<lastmod>${now}</lastmod>
|
|
86
|
+
</sitemap>`;
|
|
87
|
+
});
|
|
88
|
+
const allEntries = [...paginatedEntries, ...additionalEntries].join("\n");
|
|
89
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
90
|
+
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
91
|
+
${allEntries}
|
|
92
|
+
</sitemapindex>`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/app/index.ts
|
|
96
|
+
function extractRoutes(pagesContext, localeSegment) {
|
|
97
|
+
const routes = [];
|
|
98
|
+
for (const key of pagesContext.keys()) {
|
|
99
|
+
if (key.includes("[...")) continue;
|
|
100
|
+
const pageModule = pagesContext(key);
|
|
101
|
+
let pathname = key.replace("./", "/").replace("/page.tsx", "").replace(new RegExp(`^/${localeSegment.replace(/[[\]]/g, "\\$&")}`), "").replace(/\/\([^)]+\)/g, "") || "/";
|
|
102
|
+
if (pathname === "") pathname = "/";
|
|
103
|
+
const dynamicSegments = pathname.match(/\[([^\]]+)\]/g)?.map((s) => s.slice(1, -1)) || [];
|
|
104
|
+
routes.push({
|
|
105
|
+
pathname,
|
|
106
|
+
dynamicSegments,
|
|
107
|
+
getParams: pageModule.generateStaticParams || null
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return routes;
|
|
111
|
+
}
|
|
112
|
+
async function getAllPaths(routes) {
|
|
113
|
+
const allPaths = ["/"];
|
|
114
|
+
const seenPaths = /* @__PURE__ */ new Set(["/"]);
|
|
115
|
+
for (const route of routes) {
|
|
116
|
+
if (route.dynamicSegments.length === 0) {
|
|
117
|
+
if (route.pathname !== "/" && !seenPaths.has(route.pathname)) {
|
|
118
|
+
allPaths.push(route.pathname);
|
|
119
|
+
seenPaths.add(route.pathname);
|
|
120
|
+
}
|
|
121
|
+
} else if (route.getParams) {
|
|
122
|
+
const params = await route.getParams();
|
|
123
|
+
for (const param of params) {
|
|
124
|
+
let dynamicPath = route.pathname;
|
|
125
|
+
for (const segment of route.dynamicSegments) {
|
|
126
|
+
dynamicPath = dynamicPath.replace(`[${segment}]`, param[segment]);
|
|
127
|
+
}
|
|
128
|
+
if (!seenPaths.has(dynamicPath)) {
|
|
129
|
+
allPaths.push(dynamicPath);
|
|
130
|
+
seenPaths.add(dynamicPath);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return allPaths;
|
|
136
|
+
}
|
|
137
|
+
function pathsToEntries(paths, config) {
|
|
138
|
+
const { baseUrl, locales = [], defaultLocale, exclude, priority, changeFreq } = config;
|
|
139
|
+
const filteredPaths = paths.filter((pathname) => !shouldExclude(pathname, exclude));
|
|
140
|
+
return filteredPaths.map((pathname) => {
|
|
141
|
+
const entry = {
|
|
142
|
+
url: buildUrl(baseUrl, pathname, defaultLocale, defaultLocale),
|
|
143
|
+
lastModified: /* @__PURE__ */ new Date(),
|
|
144
|
+
changeFrequency: getChangeFreq(pathname, changeFreq),
|
|
145
|
+
priority: getPriority(pathname, priority)
|
|
146
|
+
};
|
|
147
|
+
if (locales.length > 0) {
|
|
148
|
+
entry.alternates = {
|
|
149
|
+
languages: Object.fromEntries(
|
|
150
|
+
locales.map((locale) => [
|
|
151
|
+
locale,
|
|
152
|
+
buildUrl(baseUrl, pathname, locale, defaultLocale)
|
|
153
|
+
])
|
|
154
|
+
)
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return entry;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function createSitemapIndexHandler(options) {
|
|
161
|
+
const { urlsPerSitemap = 5e3, locales = [], defaultLocale, additionalSitemaps, exclude } = options;
|
|
162
|
+
const localeSegment = options.localeSegment ?? (locales.length > 0 || defaultLocale ? "[locale]" : "");
|
|
163
|
+
const routes = extractRoutes(options.pagesContext, localeSegment);
|
|
164
|
+
return {
|
|
165
|
+
GET: async () => {
|
|
166
|
+
const allPaths = await getAllPaths(routes);
|
|
167
|
+
const filteredPaths = allPaths.filter((pathname) => !shouldExclude(pathname, exclude));
|
|
168
|
+
const sitemapCount = Math.max(1, Math.ceil(filteredPaths.length / urlsPerSitemap));
|
|
169
|
+
const xml = generateSitemapIndexXml(options.baseUrl, sitemapCount, {
|
|
170
|
+
additionalSitemaps
|
|
171
|
+
});
|
|
172
|
+
return new Response(xml, {
|
|
173
|
+
headers: { "Content-Type": "application/xml" }
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function createSitemapHandler(options) {
|
|
179
|
+
const { urlsPerSitemap = 5e3, locales = [], defaultLocale, exclude } = options;
|
|
180
|
+
const localeSegment = options.localeSegment ?? (locales.length > 0 || defaultLocale ? "[locale]" : "");
|
|
181
|
+
const routes = extractRoutes(options.pagesContext, localeSegment);
|
|
182
|
+
const getFilteredPaths = async () => {
|
|
183
|
+
const allPaths = await getAllPaths(routes);
|
|
184
|
+
return allPaths.filter((pathname) => !shouldExclude(pathname, exclude));
|
|
185
|
+
};
|
|
186
|
+
return {
|
|
187
|
+
generateStaticParams: async () => {
|
|
188
|
+
const filteredPaths = await getFilteredPaths();
|
|
189
|
+
const sitemapCount = Math.max(1, Math.ceil(filteredPaths.length / urlsPerSitemap));
|
|
190
|
+
return Array.from({ length: sitemapCount }, (_, i) => ({ id: String(i) }));
|
|
191
|
+
},
|
|
192
|
+
GET: async (_request, { params }) => {
|
|
193
|
+
const { id } = await params;
|
|
194
|
+
const sitemapId = parseInt(id, 10);
|
|
195
|
+
const filteredPaths = await getFilteredPaths();
|
|
196
|
+
const start = sitemapId * urlsPerSitemap;
|
|
197
|
+
const end = start + urlsPerSitemap;
|
|
198
|
+
const paths = filteredPaths.slice(start, end);
|
|
199
|
+
const entries = pathsToEntries(paths, { ...options, exclude: void 0 });
|
|
200
|
+
const xml = generateSitemapXml(entries);
|
|
201
|
+
return new Response(xml, {
|
|
202
|
+
headers: { "Content-Type": "application/xml" }
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export { createSitemapHandler, createSitemapIndexHandler };
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
function calculateDepthPriority(pathname) {
|
|
5
|
+
if (pathname === "/") return 1;
|
|
6
|
+
const depth = pathname.split("/").filter(Boolean).length;
|
|
7
|
+
return Math.max(0.1, 1 - depth * 0.2);
|
|
8
|
+
}
|
|
9
|
+
function shouldExclude(pathname, exclude) {
|
|
10
|
+
if (!exclude) return false;
|
|
11
|
+
if (typeof exclude === "function") {
|
|
12
|
+
return exclude(pathname);
|
|
13
|
+
}
|
|
14
|
+
return exclude.some((pattern) => {
|
|
15
|
+
const regexPattern = pattern.replace(/\*\*/g, "{{DOUBLE_STAR}}").replace(/\*/g, "[^/]*").replace(/\{\{DOUBLE_STAR\}\}/g, ".*");
|
|
16
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
17
|
+
return regex.test(pathname);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function getPriority(pathname, priority) {
|
|
21
|
+
if (priority === void 0 || priority === "auto") {
|
|
22
|
+
return calculateDepthPriority(pathname);
|
|
23
|
+
}
|
|
24
|
+
if (typeof priority === "function") {
|
|
25
|
+
return priority(pathname);
|
|
26
|
+
}
|
|
27
|
+
return priority;
|
|
28
|
+
}
|
|
29
|
+
function getChangeFreq(pathname, changeFreq) {
|
|
30
|
+
if (changeFreq === void 0) {
|
|
31
|
+
return "weekly";
|
|
32
|
+
}
|
|
33
|
+
if (typeof changeFreq === "function") {
|
|
34
|
+
return changeFreq(pathname);
|
|
35
|
+
}
|
|
36
|
+
return changeFreq;
|
|
37
|
+
}
|
|
38
|
+
function buildUrl(baseUrl, pathname, locale, defaultLocale) {
|
|
39
|
+
if (!locale || locale === defaultLocale) {
|
|
40
|
+
return `${baseUrl}${pathname}`;
|
|
41
|
+
}
|
|
42
|
+
return `${baseUrl}/${locale}${pathname}`;
|
|
43
|
+
}
|
|
44
|
+
function generateSitemapXml(entries) {
|
|
45
|
+
const hasAlternates = entries.some((e) => e.alternates?.languages);
|
|
46
|
+
const xmlns = hasAlternates ? 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"' : 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"';
|
|
47
|
+
const urlEntries = entries.map((entry) => {
|
|
48
|
+
const parts = [` <loc>${entry.url}</loc>`];
|
|
49
|
+
if (entry.alternates?.languages) {
|
|
50
|
+
for (const [lang, href] of Object.entries(entry.alternates.languages)) {
|
|
51
|
+
parts.push(` <xhtml:link rel="alternate" hreflang="${lang}" href="${href}" />`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (entry.lastModified) {
|
|
55
|
+
const date = entry.lastModified instanceof Date ? entry.lastModified.toISOString() : entry.lastModified;
|
|
56
|
+
parts.push(` <lastmod>${date}</lastmod>`);
|
|
57
|
+
}
|
|
58
|
+
if (entry.changeFrequency) {
|
|
59
|
+
parts.push(` <changefreq>${entry.changeFrequency}</changefreq>`);
|
|
60
|
+
}
|
|
61
|
+
if (entry.priority !== void 0) {
|
|
62
|
+
parts.push(` <priority>${entry.priority}</priority>`);
|
|
63
|
+
}
|
|
64
|
+
return ` <url>
|
|
65
|
+
${parts.join("\n")}
|
|
66
|
+
</url>`;
|
|
67
|
+
}).join("\n");
|
|
68
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
69
|
+
<urlset ${xmlns}>
|
|
70
|
+
${urlEntries}
|
|
71
|
+
</urlset>`;
|
|
72
|
+
}
|
|
73
|
+
function generateSitemapIndexXml(baseUrl, sitemapCount, options) {
|
|
74
|
+
const { sitemapPattern = "/sitemap-{id}.xml", additionalSitemaps = [] } = options || {};
|
|
75
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
76
|
+
const paginatedEntries = Array.from({ length: sitemapCount }, (_, i) => {
|
|
77
|
+
const loc = `${baseUrl}${sitemapPattern.replace("{id}", String(i))}`;
|
|
78
|
+
return ` <sitemap>
|
|
79
|
+
<loc>${loc}</loc>
|
|
80
|
+
<lastmod>${now}</lastmod>
|
|
81
|
+
</sitemap>`;
|
|
82
|
+
});
|
|
83
|
+
const additionalEntries = additionalSitemaps.map((sitemap) => {
|
|
84
|
+
const loc = sitemap.startsWith("http") ? sitemap : `${baseUrl}${sitemap}`;
|
|
85
|
+
return ` <sitemap>
|
|
86
|
+
<loc>${loc}</loc>
|
|
87
|
+
<lastmod>${now}</lastmod>
|
|
88
|
+
</sitemap>`;
|
|
89
|
+
});
|
|
90
|
+
const allEntries = [...paginatedEntries, ...additionalEntries].join("\n");
|
|
91
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
92
|
+
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
93
|
+
${allEntries}
|
|
94
|
+
</sitemapindex>`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
exports.buildUrl = buildUrl;
|
|
98
|
+
exports.calculateDepthPriority = calculateDepthPriority;
|
|
99
|
+
exports.generateSitemapIndexXml = generateSitemapIndexXml;
|
|
100
|
+
exports.generateSitemapXml = generateSitemapXml;
|
|
101
|
+
exports.getChangeFreq = getChangeFreq;
|
|
102
|
+
exports.getPriority = getPriority;
|
|
103
|
+
exports.shouldExclude = shouldExclude;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
type ChangeFrequency = "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
|
|
2
|
+
interface SitemapConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Base URL of the site (e.g., "https://example.com")
|
|
5
|
+
*/
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
/**
|
|
8
|
+
* List of supported locales (e.g., ["en", "fr"])
|
|
9
|
+
*/
|
|
10
|
+
locales?: string[];
|
|
11
|
+
/**
|
|
12
|
+
* Default locale (e.g., "en")
|
|
13
|
+
*/
|
|
14
|
+
defaultLocale?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Number of URLs per sitemap file (default: 5000)
|
|
17
|
+
*/
|
|
18
|
+
urlsPerSitemap?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Path to the app directory relative to the project root
|
|
21
|
+
* Default: "./src/app" or "./app"
|
|
22
|
+
*/
|
|
23
|
+
appDir?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Locale segment in the URL path (e.g., "[locale]")
|
|
26
|
+
* Auto-detected as "[locale]" when locales or defaultLocale is set
|
|
27
|
+
*/
|
|
28
|
+
localeSegment?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Exclude routes from the sitemap
|
|
31
|
+
* Can be an array of glob patterns or a function that returns true to exclude
|
|
32
|
+
* @example exclude: ["/admin/*", "/api/*"]
|
|
33
|
+
* @example exclude: (path) => path.startsWith("/private")
|
|
34
|
+
*/
|
|
35
|
+
exclude?: string[] | ((path: string) => boolean);
|
|
36
|
+
/**
|
|
37
|
+
* Priority for sitemap entries
|
|
38
|
+
* Can be a fixed number (0.0-1.0), a function, or "auto" for depth-based calculation
|
|
39
|
+
* Default: "auto" (1.0 - depth * 0.2, minimum 0.1)
|
|
40
|
+
* @example priority: 0.8
|
|
41
|
+
* @example priority: (path) => path === "/" ? 1.0 : 0.7
|
|
42
|
+
* @example priority: "auto"
|
|
43
|
+
*/
|
|
44
|
+
priority?: number | "auto" | ((path: string) => number);
|
|
45
|
+
/**
|
|
46
|
+
* Change frequency for sitemap entries
|
|
47
|
+
* Can be a fixed value or a function
|
|
48
|
+
* Default: "weekly"
|
|
49
|
+
* @example changeFreq: "daily"
|
|
50
|
+
* @example changeFreq: (path) => path === "/" ? "daily" : "weekly"
|
|
51
|
+
*/
|
|
52
|
+
changeFreq?: ChangeFrequency | ((path: string) => ChangeFrequency);
|
|
53
|
+
/**
|
|
54
|
+
* Additional sitemaps to include in the sitemap index
|
|
55
|
+
* Useful for custom sitemaps (e.g., products from an API)
|
|
56
|
+
* @example additionalSitemaps: ["/products-sitemap.xml", "/blog-sitemap.xml"]
|
|
57
|
+
*/
|
|
58
|
+
additionalSitemaps?: string[];
|
|
59
|
+
}
|
|
60
|
+
interface SitemapEntry {
|
|
61
|
+
url: string;
|
|
62
|
+
lastModified?: string | Date;
|
|
63
|
+
changeFrequency?: ChangeFrequency;
|
|
64
|
+
priority?: number;
|
|
65
|
+
alternates?: {
|
|
66
|
+
languages?: Record<string, string>;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Calculate priority based on URL depth
|
|
71
|
+
* Root (/) = 1.0, /about = 0.8, /blog/post = 0.6, etc.
|
|
72
|
+
* Minimum priority is 0.1
|
|
73
|
+
*/
|
|
74
|
+
declare function calculateDepthPriority(pathname: string): number;
|
|
75
|
+
/**
|
|
76
|
+
* Check if a path should be excluded based on patterns or function
|
|
77
|
+
*/
|
|
78
|
+
declare function shouldExclude(pathname: string, exclude?: string[] | ((path: string) => boolean)): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Get priority for a pathname based on config
|
|
81
|
+
*/
|
|
82
|
+
declare function getPriority(pathname: string, priority?: number | "auto" | ((path: string) => number)): number;
|
|
83
|
+
/**
|
|
84
|
+
* Get change frequency for a pathname based on config
|
|
85
|
+
*/
|
|
86
|
+
declare function getChangeFreq(pathname: string, changeFreq?: ChangeFrequency | ((path: string) => ChangeFrequency)): ChangeFrequency;
|
|
87
|
+
interface RouteInfo {
|
|
88
|
+
pathname: string;
|
|
89
|
+
dynamicSegments: string[];
|
|
90
|
+
hasGenerateStaticParams: boolean;
|
|
91
|
+
}
|
|
92
|
+
type PageModule = {
|
|
93
|
+
generateStaticParams?: () => Promise<Record<string, string>[]>;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Generate the full URL for a pathname
|
|
97
|
+
*/
|
|
98
|
+
declare function buildUrl(baseUrl: string, pathname: string, locale?: string, defaultLocale?: string): string;
|
|
99
|
+
/**
|
|
100
|
+
* Generate sitemap XML from entries
|
|
101
|
+
*/
|
|
102
|
+
declare function generateSitemapXml(entries: SitemapEntry[]): string;
|
|
103
|
+
/**
|
|
104
|
+
* Generate sitemap index XML
|
|
105
|
+
*/
|
|
106
|
+
declare function generateSitemapIndexXml(baseUrl: string, sitemapCount: number, options?: {
|
|
107
|
+
sitemapPattern?: string;
|
|
108
|
+
additionalSitemaps?: string[];
|
|
109
|
+
}): string;
|
|
110
|
+
|
|
111
|
+
export { type ChangeFrequency, type PageModule, type RouteInfo, type SitemapConfig, type SitemapEntry, buildUrl, calculateDepthPriority, generateSitemapIndexXml, generateSitemapXml, getChangeFreq, getPriority, shouldExclude };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
type ChangeFrequency = "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
|
|
2
|
+
interface SitemapConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Base URL of the site (e.g., "https://example.com")
|
|
5
|
+
*/
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
/**
|
|
8
|
+
* List of supported locales (e.g., ["en", "fr"])
|
|
9
|
+
*/
|
|
10
|
+
locales?: string[];
|
|
11
|
+
/**
|
|
12
|
+
* Default locale (e.g., "en")
|
|
13
|
+
*/
|
|
14
|
+
defaultLocale?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Number of URLs per sitemap file (default: 5000)
|
|
17
|
+
*/
|
|
18
|
+
urlsPerSitemap?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Path to the app directory relative to the project root
|
|
21
|
+
* Default: "./src/app" or "./app"
|
|
22
|
+
*/
|
|
23
|
+
appDir?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Locale segment in the URL path (e.g., "[locale]")
|
|
26
|
+
* Auto-detected as "[locale]" when locales or defaultLocale is set
|
|
27
|
+
*/
|
|
28
|
+
localeSegment?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Exclude routes from the sitemap
|
|
31
|
+
* Can be an array of glob patterns or a function that returns true to exclude
|
|
32
|
+
* @example exclude: ["/admin/*", "/api/*"]
|
|
33
|
+
* @example exclude: (path) => path.startsWith("/private")
|
|
34
|
+
*/
|
|
35
|
+
exclude?: string[] | ((path: string) => boolean);
|
|
36
|
+
/**
|
|
37
|
+
* Priority for sitemap entries
|
|
38
|
+
* Can be a fixed number (0.0-1.0), a function, or "auto" for depth-based calculation
|
|
39
|
+
* Default: "auto" (1.0 - depth * 0.2, minimum 0.1)
|
|
40
|
+
* @example priority: 0.8
|
|
41
|
+
* @example priority: (path) => path === "/" ? 1.0 : 0.7
|
|
42
|
+
* @example priority: "auto"
|
|
43
|
+
*/
|
|
44
|
+
priority?: number | "auto" | ((path: string) => number);
|
|
45
|
+
/**
|
|
46
|
+
* Change frequency for sitemap entries
|
|
47
|
+
* Can be a fixed value or a function
|
|
48
|
+
* Default: "weekly"
|
|
49
|
+
* @example changeFreq: "daily"
|
|
50
|
+
* @example changeFreq: (path) => path === "/" ? "daily" : "weekly"
|
|
51
|
+
*/
|
|
52
|
+
changeFreq?: ChangeFrequency | ((path: string) => ChangeFrequency);
|
|
53
|
+
/**
|
|
54
|
+
* Additional sitemaps to include in the sitemap index
|
|
55
|
+
* Useful for custom sitemaps (e.g., products from an API)
|
|
56
|
+
* @example additionalSitemaps: ["/products-sitemap.xml", "/blog-sitemap.xml"]
|
|
57
|
+
*/
|
|
58
|
+
additionalSitemaps?: string[];
|
|
59
|
+
}
|
|
60
|
+
interface SitemapEntry {
|
|
61
|
+
url: string;
|
|
62
|
+
lastModified?: string | Date;
|
|
63
|
+
changeFrequency?: ChangeFrequency;
|
|
64
|
+
priority?: number;
|
|
65
|
+
alternates?: {
|
|
66
|
+
languages?: Record<string, string>;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Calculate priority based on URL depth
|
|
71
|
+
* Root (/) = 1.0, /about = 0.8, /blog/post = 0.6, etc.
|
|
72
|
+
* Minimum priority is 0.1
|
|
73
|
+
*/
|
|
74
|
+
declare function calculateDepthPriority(pathname: string): number;
|
|
75
|
+
/**
|
|
76
|
+
* Check if a path should be excluded based on patterns or function
|
|
77
|
+
*/
|
|
78
|
+
declare function shouldExclude(pathname: string, exclude?: string[] | ((path: string) => boolean)): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Get priority for a pathname based on config
|
|
81
|
+
*/
|
|
82
|
+
declare function getPriority(pathname: string, priority?: number | "auto" | ((path: string) => number)): number;
|
|
83
|
+
/**
|
|
84
|
+
* Get change frequency for a pathname based on config
|
|
85
|
+
*/
|
|
86
|
+
declare function getChangeFreq(pathname: string, changeFreq?: ChangeFrequency | ((path: string) => ChangeFrequency)): ChangeFrequency;
|
|
87
|
+
interface RouteInfo {
|
|
88
|
+
pathname: string;
|
|
89
|
+
dynamicSegments: string[];
|
|
90
|
+
hasGenerateStaticParams: boolean;
|
|
91
|
+
}
|
|
92
|
+
type PageModule = {
|
|
93
|
+
generateStaticParams?: () => Promise<Record<string, string>[]>;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Generate the full URL for a pathname
|
|
97
|
+
*/
|
|
98
|
+
declare function buildUrl(baseUrl: string, pathname: string, locale?: string, defaultLocale?: string): string;
|
|
99
|
+
/**
|
|
100
|
+
* Generate sitemap XML from entries
|
|
101
|
+
*/
|
|
102
|
+
declare function generateSitemapXml(entries: SitemapEntry[]): string;
|
|
103
|
+
/**
|
|
104
|
+
* Generate sitemap index XML
|
|
105
|
+
*/
|
|
106
|
+
declare function generateSitemapIndexXml(baseUrl: string, sitemapCount: number, options?: {
|
|
107
|
+
sitemapPattern?: string;
|
|
108
|
+
additionalSitemaps?: string[];
|
|
109
|
+
}): string;
|
|
110
|
+
|
|
111
|
+
export { type ChangeFrequency, type PageModule, type RouteInfo, type SitemapConfig, type SitemapEntry, buildUrl, calculateDepthPriority, generateSitemapIndexXml, generateSitemapXml, getChangeFreq, getPriority, shouldExclude };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
function calculateDepthPriority(pathname) {
|
|
3
|
+
if (pathname === "/") return 1;
|
|
4
|
+
const depth = pathname.split("/").filter(Boolean).length;
|
|
5
|
+
return Math.max(0.1, 1 - depth * 0.2);
|
|
6
|
+
}
|
|
7
|
+
function shouldExclude(pathname, exclude) {
|
|
8
|
+
if (!exclude) return false;
|
|
9
|
+
if (typeof exclude === "function") {
|
|
10
|
+
return exclude(pathname);
|
|
11
|
+
}
|
|
12
|
+
return exclude.some((pattern) => {
|
|
13
|
+
const regexPattern = pattern.replace(/\*\*/g, "{{DOUBLE_STAR}}").replace(/\*/g, "[^/]*").replace(/\{\{DOUBLE_STAR\}\}/g, ".*");
|
|
14
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
15
|
+
return regex.test(pathname);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function getPriority(pathname, priority) {
|
|
19
|
+
if (priority === void 0 || priority === "auto") {
|
|
20
|
+
return calculateDepthPriority(pathname);
|
|
21
|
+
}
|
|
22
|
+
if (typeof priority === "function") {
|
|
23
|
+
return priority(pathname);
|
|
24
|
+
}
|
|
25
|
+
return priority;
|
|
26
|
+
}
|
|
27
|
+
function getChangeFreq(pathname, changeFreq) {
|
|
28
|
+
if (changeFreq === void 0) {
|
|
29
|
+
return "weekly";
|
|
30
|
+
}
|
|
31
|
+
if (typeof changeFreq === "function") {
|
|
32
|
+
return changeFreq(pathname);
|
|
33
|
+
}
|
|
34
|
+
return changeFreq;
|
|
35
|
+
}
|
|
36
|
+
function buildUrl(baseUrl, pathname, locale, defaultLocale) {
|
|
37
|
+
if (!locale || locale === defaultLocale) {
|
|
38
|
+
return `${baseUrl}${pathname}`;
|
|
39
|
+
}
|
|
40
|
+
return `${baseUrl}/${locale}${pathname}`;
|
|
41
|
+
}
|
|
42
|
+
function generateSitemapXml(entries) {
|
|
43
|
+
const hasAlternates = entries.some((e) => e.alternates?.languages);
|
|
44
|
+
const xmlns = hasAlternates ? 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"' : 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"';
|
|
45
|
+
const urlEntries = entries.map((entry) => {
|
|
46
|
+
const parts = [` <loc>${entry.url}</loc>`];
|
|
47
|
+
if (entry.alternates?.languages) {
|
|
48
|
+
for (const [lang, href] of Object.entries(entry.alternates.languages)) {
|
|
49
|
+
parts.push(` <xhtml:link rel="alternate" hreflang="${lang}" href="${href}" />`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (entry.lastModified) {
|
|
53
|
+
const date = entry.lastModified instanceof Date ? entry.lastModified.toISOString() : entry.lastModified;
|
|
54
|
+
parts.push(` <lastmod>${date}</lastmod>`);
|
|
55
|
+
}
|
|
56
|
+
if (entry.changeFrequency) {
|
|
57
|
+
parts.push(` <changefreq>${entry.changeFrequency}</changefreq>`);
|
|
58
|
+
}
|
|
59
|
+
if (entry.priority !== void 0) {
|
|
60
|
+
parts.push(` <priority>${entry.priority}</priority>`);
|
|
61
|
+
}
|
|
62
|
+
return ` <url>
|
|
63
|
+
${parts.join("\n")}
|
|
64
|
+
</url>`;
|
|
65
|
+
}).join("\n");
|
|
66
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
67
|
+
<urlset ${xmlns}>
|
|
68
|
+
${urlEntries}
|
|
69
|
+
</urlset>`;
|
|
70
|
+
}
|
|
71
|
+
function generateSitemapIndexXml(baseUrl, sitemapCount, options) {
|
|
72
|
+
const { sitemapPattern = "/sitemap-{id}.xml", additionalSitemaps = [] } = options || {};
|
|
73
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
74
|
+
const paginatedEntries = Array.from({ length: sitemapCount }, (_, i) => {
|
|
75
|
+
const loc = `${baseUrl}${sitemapPattern.replace("{id}", String(i))}`;
|
|
76
|
+
return ` <sitemap>
|
|
77
|
+
<loc>${loc}</loc>
|
|
78
|
+
<lastmod>${now}</lastmod>
|
|
79
|
+
</sitemap>`;
|
|
80
|
+
});
|
|
81
|
+
const additionalEntries = additionalSitemaps.map((sitemap) => {
|
|
82
|
+
const loc = sitemap.startsWith("http") ? sitemap : `${baseUrl}${sitemap}`;
|
|
83
|
+
return ` <sitemap>
|
|
84
|
+
<loc>${loc}</loc>
|
|
85
|
+
<lastmod>${now}</lastmod>
|
|
86
|
+
</sitemap>`;
|
|
87
|
+
});
|
|
88
|
+
const allEntries = [...paginatedEntries, ...additionalEntries].join("\n");
|
|
89
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
90
|
+
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
91
|
+
${allEntries}
|
|
92
|
+
</sitemapindex>`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export { buildUrl, calculateDepthPriority, generateSitemapIndexXml, generateSitemapXml, getChangeFreq, getPriority, shouldExclude };
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@onruntime/next-sitemap",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Dynamic sitemap generation for Next.js with automatic route discovery",
|
|
5
|
+
"author": "onRuntime Studio <contact@onruntime.com>",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/onRuntime/onruntime.git",
|
|
9
|
+
"directory": "packages/next-sitemap"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/onRuntime/onruntime/tree/master/packages/next-sitemap#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/onRuntime/onruntime/issues"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.js",
|
|
25
|
+
"require": "./dist/index.cjs"
|
|
26
|
+
},
|
|
27
|
+
"./app": {
|
|
28
|
+
"types": "./dist/app/index.d.ts",
|
|
29
|
+
"import": "./dist/app/index.js",
|
|
30
|
+
"require": "./dist/app/index.cjs"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"dev": "tsup --watch",
|
|
39
|
+
"type-check": "tsc --noEmit"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"next": ">=14.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^22.0.0",
|
|
46
|
+
"next": "^16.1.1",
|
|
47
|
+
"tsup": "^8",
|
|
48
|
+
"typescript": "^5"
|
|
49
|
+
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"sitemap",
|
|
52
|
+
"nextjs",
|
|
53
|
+
"app-router",
|
|
54
|
+
"seo",
|
|
55
|
+
"xml",
|
|
56
|
+
"dynamic-sitemap"
|
|
57
|
+
],
|
|
58
|
+
"license": "MIT"
|
|
59
|
+
}
|