@onruntime/next-sitemap 0.3.0 → 0.4.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 +95 -5
- package/dist/pages/index.cjs +267 -0
- package/dist/pages/index.d.cts +114 -0
- package/dist/pages/index.d.ts +114 -0
- package/dist/pages/index.js +263 -0
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -5,11 +5,15 @@ Dynamic sitemap generation for Next.js with automatic route discovery.
|
|
|
5
5
|
## Example
|
|
6
6
|
|
|
7
7
|
- [Next.js App Router](https://github.com/onRuntime/onruntime/tree/master/examples/next-sitemap/app)
|
|
8
|
+
- [Next.js App Router with i18n](https://github.com/onRuntime/onruntime/tree/master/examples/next-sitemap/app-with-locales)
|
|
9
|
+
- [Next.js Pages Router](https://github.com/onRuntime/onruntime/tree/master/examples/next-sitemap/pages)
|
|
10
|
+
- [Next.js Pages Router with i18n](https://github.com/onRuntime/onruntime/tree/master/examples/next-sitemap/pages-with-locales)
|
|
8
11
|
|
|
9
12
|
## Features
|
|
10
13
|
|
|
14
|
+
- **App Router** and **Pages Router** support
|
|
11
15
|
- Automatic route discovery using `require.context`
|
|
12
|
-
- Calls `generateStaticParams` for dynamic routes
|
|
16
|
+
- Calls `generateStaticParams` (App Router) or `getStaticPaths` (Pages Router) for dynamic routes
|
|
13
17
|
- Multi-sitemap support with sitemap index (for sites with >50,000 URLs)
|
|
14
18
|
- hreflang alternates for i18n
|
|
15
19
|
- Fully static generation (SSG)
|
|
@@ -104,6 +108,89 @@ const nextConfig: NextConfig = {
|
|
|
104
108
|
export default nextConfig;
|
|
105
109
|
```
|
|
106
110
|
|
|
111
|
+
### Next.js Pages Router
|
|
112
|
+
|
|
113
|
+
#### 1. Create the sitemap index API route
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// pages/api/sitemap.xml.ts
|
|
117
|
+
import { createSitemapIndexApiHandler } from "@onruntime/next-sitemap/pages";
|
|
118
|
+
|
|
119
|
+
// @ts-expect-error - require.context is a webpack/turbopack feature
|
|
120
|
+
const pagesContext = require.context("../", true, /^\.\/(?!\[|_|api\/).*\.tsx$/);
|
|
121
|
+
|
|
122
|
+
export default createSitemapIndexApiHandler({
|
|
123
|
+
baseUrl: "https://example.com",
|
|
124
|
+
pagesContext,
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### 2. Create the individual sitemap API route
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// pages/api/sitemap/[id].ts
|
|
132
|
+
import { createSitemapApiHandler } from "@onruntime/next-sitemap/pages";
|
|
133
|
+
|
|
134
|
+
// @ts-expect-error - require.context is a webpack/turbopack feature
|
|
135
|
+
const pagesContext = require.context("../../", true, /^\.\/(?!\[|_|api\/).*\.tsx$/);
|
|
136
|
+
|
|
137
|
+
export default createSitemapApiHandler({
|
|
138
|
+
baseUrl: "https://example.com",
|
|
139
|
+
pagesContext,
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### 3. Add URL rewrite in next.config.ts
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// next.config.ts
|
|
147
|
+
import type { NextConfig } from "next";
|
|
148
|
+
|
|
149
|
+
const nextConfig: NextConfig = {
|
|
150
|
+
async rewrites() {
|
|
151
|
+
return [
|
|
152
|
+
{
|
|
153
|
+
source: "/sitemap.xml",
|
|
154
|
+
destination: "/api/sitemap.xml",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
source: "/sitemap-:id.xml",
|
|
158
|
+
destination: "/api/sitemap/:id",
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export default nextConfig;
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### With i18n (optional)
|
|
168
|
+
|
|
169
|
+
The Pages Router uses Next.js native i18n config in `next.config.js`. Pages stay in `pages/` (no `[locale]` folder), and you just need to provide `locales` and `defaultLocale`:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// next.config.js
|
|
173
|
+
module.exports = {
|
|
174
|
+
i18n: {
|
|
175
|
+
locales: ['en', 'fr'],
|
|
176
|
+
defaultLocale: 'en',
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// pages/api/sitemap.xml.ts
|
|
181
|
+
import { createSitemapIndexApiHandler } from "@onruntime/next-sitemap/pages";
|
|
182
|
+
|
|
183
|
+
// @ts-expect-error - require.context is a webpack/turbopack feature
|
|
184
|
+
const pagesContext = require.context("../", true, /^\.\/(?!\[|_|api\/).*\.tsx$/);
|
|
185
|
+
|
|
186
|
+
export default createSitemapIndexApiHandler({
|
|
187
|
+
baseUrl: "https://example.com",
|
|
188
|
+
locales: ["en", "fr"],
|
|
189
|
+
defaultLocale: "en",
|
|
190
|
+
pagesContext,
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
107
194
|
### Configuration Options
|
|
108
195
|
|
|
109
196
|
| Option | Type | Default | Description |
|
|
@@ -192,9 +279,9 @@ const { GET } = createSitemapIndexHandler({
|
|
|
192
279
|
|
|
193
280
|
### How It Works
|
|
194
281
|
|
|
195
|
-
1. `require.context` scans your app directory at build time
|
|
282
|
+
1. `require.context` scans your app/pages directory at build time
|
|
196
283
|
2. For each page found, it extracts the route path
|
|
197
|
-
3. For dynamic routes (e.g., `/projects/[id]`), it calls `generateStaticParams`
|
|
284
|
+
3. For dynamic routes (e.g., `/projects/[id]`), it calls `generateStaticParams` (App Router) or `getStaticPaths` (Pages Router)
|
|
198
285
|
4. URLs are paginated into multiple sitemaps (default: 5000 URLs each)
|
|
199
286
|
5. A sitemap index lists all individual sitemaps
|
|
200
287
|
|
|
@@ -288,11 +375,14 @@ This will log:
|
|
|
288
375
|
|
|
289
376
|
**Common issues:**
|
|
290
377
|
|
|
291
|
-
1. **`generateStaticParams` not detected**: Make sure it's exported
|
|
378
|
+
1. **`generateStaticParams`/`getStaticPaths` not detected**: Make sure it's exported at the top level of your page file:
|
|
292
379
|
```typescript
|
|
293
|
-
// ✅ Correct
|
|
380
|
+
// ✅ Correct (App Router)
|
|
294
381
|
export async function generateStaticParams() { ... }
|
|
295
382
|
|
|
383
|
+
// ✅ Correct (Pages Router)
|
|
384
|
+
export async function getStaticPaths() { ... }
|
|
385
|
+
|
|
296
386
|
// ❌ Wrong - not exported
|
|
297
387
|
async function generateStaticParams() { ... }
|
|
298
388
|
```
|
|
@@ -0,0 +1,267 @@
|
|
|
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/pages/index.ts
|
|
98
|
+
function extractRoutes(pagesContext, localeSegment) {
|
|
99
|
+
const routes = [];
|
|
100
|
+
for (const key of pagesContext.keys()) {
|
|
101
|
+
if (key.includes("[...")) continue;
|
|
102
|
+
if (key.includes("/api/")) continue;
|
|
103
|
+
if (key.includes("/_")) continue;
|
|
104
|
+
const pageModule = pagesContext(key);
|
|
105
|
+
let pathname = key.replace(/^\.\//, "/").replace(/\.tsx?$/, "").replace(/\/index$/, "/");
|
|
106
|
+
if (pathname === "") pathname = "/";
|
|
107
|
+
if (localeSegment) {
|
|
108
|
+
pathname = pathname.replace(
|
|
109
|
+
new RegExp(`^/${localeSegment.replace(/[[\]]/g, "\\$&")}`),
|
|
110
|
+
""
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (/(?:^|\/)(src|pages)(?:\/|$)/.test(pathname)) continue;
|
|
114
|
+
if (!pathname || pathname === "") {
|
|
115
|
+
pathname = "/";
|
|
116
|
+
} else if (!pathname.startsWith("/")) {
|
|
117
|
+
pathname = "/" + pathname;
|
|
118
|
+
}
|
|
119
|
+
const dynamicSegments = pathname.match(/\[([^\]]+)\]/g)?.map((s) => s.slice(1, -1)) || [];
|
|
120
|
+
const getStaticPaths = pageModule.getStaticPaths;
|
|
121
|
+
let getParams = null;
|
|
122
|
+
if (getStaticPaths) {
|
|
123
|
+
getParams = async () => {
|
|
124
|
+
const result = await getStaticPaths();
|
|
125
|
+
return result.paths.map((p) => p.params);
|
|
126
|
+
};
|
|
127
|
+
} else if (pageModule.generateStaticParams) {
|
|
128
|
+
getParams = pageModule.generateStaticParams;
|
|
129
|
+
}
|
|
130
|
+
routes.push({
|
|
131
|
+
pathname,
|
|
132
|
+
dynamicSegments,
|
|
133
|
+
getParams
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return routes;
|
|
137
|
+
}
|
|
138
|
+
async function getAllPaths(routes, debug = false) {
|
|
139
|
+
const allPaths = ["/"];
|
|
140
|
+
const seenPaths = /* @__PURE__ */ new Set(["/"]);
|
|
141
|
+
for (const route of routes) {
|
|
142
|
+
if (route.dynamicSegments.length === 0) {
|
|
143
|
+
if (route.pathname !== "/" && !seenPaths.has(route.pathname)) {
|
|
144
|
+
allPaths.push(route.pathname);
|
|
145
|
+
seenPaths.add(route.pathname);
|
|
146
|
+
}
|
|
147
|
+
} else if (route.getParams) {
|
|
148
|
+
try {
|
|
149
|
+
const params = await route.getParams();
|
|
150
|
+
if (debug) {
|
|
151
|
+
console.log(`[next-sitemap] ${route.pathname}: getStaticPaths returned ${params.length} params`);
|
|
152
|
+
}
|
|
153
|
+
for (const param of params) {
|
|
154
|
+
let dynamicPath = route.pathname;
|
|
155
|
+
for (const segment of route.dynamicSegments) {
|
|
156
|
+
const value = param[segment];
|
|
157
|
+
if (value === void 0) {
|
|
158
|
+
if (debug) {
|
|
159
|
+
console.warn(`[next-sitemap] ${route.pathname}: missing param "${segment}" in`, param);
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
dynamicPath = dynamicPath.replace(`[${segment}]`, value);
|
|
164
|
+
}
|
|
165
|
+
if (!seenPaths.has(dynamicPath)) {
|
|
166
|
+
allPaths.push(dynamicPath);
|
|
167
|
+
seenPaths.add(dynamicPath);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error(`[next-sitemap] Error calling getStaticPaths for ${route.pathname}:`, error);
|
|
172
|
+
}
|
|
173
|
+
} else if (route.dynamicSegments.length > 0) {
|
|
174
|
+
if (debug) {
|
|
175
|
+
console.warn(
|
|
176
|
+
`[next-sitemap] Skipping dynamic route ${route.pathname}: no getStaticPaths exported. Use additionalSitemaps for routes that fetch data at runtime.`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return allPaths;
|
|
182
|
+
}
|
|
183
|
+
function pathsToEntries(paths, config) {
|
|
184
|
+
const { baseUrl, locales = [], defaultLocale, exclude, priority, changeFreq } = config;
|
|
185
|
+
const filteredPaths = paths.filter((pathname) => !shouldExclude(pathname, exclude));
|
|
186
|
+
return filteredPaths.map((pathname) => {
|
|
187
|
+
const entry = {
|
|
188
|
+
url: buildUrl(baseUrl, pathname, defaultLocale, defaultLocale),
|
|
189
|
+
lastModified: /* @__PURE__ */ new Date(),
|
|
190
|
+
changeFrequency: getChangeFreq(pathname, changeFreq),
|
|
191
|
+
priority: getPriority(pathname, priority)
|
|
192
|
+
};
|
|
193
|
+
if (locales.length > 0) {
|
|
194
|
+
entry.alternates = {
|
|
195
|
+
languages: Object.fromEntries(
|
|
196
|
+
locales.map((locale) => [
|
|
197
|
+
locale,
|
|
198
|
+
buildUrl(baseUrl, pathname, locale, defaultLocale)
|
|
199
|
+
])
|
|
200
|
+
)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return entry;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function createSitemapIndexApiHandler(options) {
|
|
207
|
+
const { urlsPerSitemap = 5e3, additionalSitemaps, exclude, debug = false } = options;
|
|
208
|
+
const localeSegment = options.localeSegment ?? "";
|
|
209
|
+
const routes = extractRoutes(options.pagesContext, localeSegment);
|
|
210
|
+
if (debug) {
|
|
211
|
+
console.log(`[next-sitemap] Found ${routes.length} routes:`);
|
|
212
|
+
routes.forEach((r) => {
|
|
213
|
+
const hasParams = r.getParams ? "\u2713 getStaticPaths" : "\u2717 no getStaticPaths";
|
|
214
|
+
const segments = r.dynamicSegments.length > 0 ? ` [${r.dynamicSegments.join(", ")}]` : "";
|
|
215
|
+
console.log(` ${r.pathname}${segments} - ${hasParams}`);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return async function handler(_req, res) {
|
|
219
|
+
const allPaths = await getAllPaths(routes, debug);
|
|
220
|
+
const filteredPaths = allPaths.filter((pathname) => !shouldExclude(pathname, exclude));
|
|
221
|
+
const sitemapCount = Math.max(1, Math.ceil(filteredPaths.length / urlsPerSitemap));
|
|
222
|
+
const xml = generateSitemapIndexXml(options.baseUrl, sitemapCount, {
|
|
223
|
+
additionalSitemaps
|
|
224
|
+
});
|
|
225
|
+
res.setHeader("Content-Type", "application/xml");
|
|
226
|
+
res.status(200).send(xml);
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function createSitemapApiHandler(options) {
|
|
230
|
+
const { urlsPerSitemap = 5e3, exclude, debug = false } = options;
|
|
231
|
+
const localeSegment = options.localeSegment ?? "";
|
|
232
|
+
const routes = extractRoutes(options.pagesContext, localeSegment);
|
|
233
|
+
const getFilteredPaths = async () => {
|
|
234
|
+
const allPaths = await getAllPaths(routes, debug);
|
|
235
|
+
return allPaths.filter((pathname) => !shouldExclude(pathname, exclude));
|
|
236
|
+
};
|
|
237
|
+
return async function handler(req, res) {
|
|
238
|
+
const { id } = req.query;
|
|
239
|
+
const sitemapId = parseInt(Array.isArray(id) ? id[0] : id || "0", 10);
|
|
240
|
+
const filteredPaths = await getFilteredPaths();
|
|
241
|
+
const start = sitemapId * urlsPerSitemap;
|
|
242
|
+
const end = start + urlsPerSitemap;
|
|
243
|
+
const paths = filteredPaths.slice(start, end);
|
|
244
|
+
const entries = pathsToEntries(paths, { ...options, exclude: void 0 });
|
|
245
|
+
const xml = generateSitemapXml(entries);
|
|
246
|
+
res.setHeader("Content-Type", "application/xml");
|
|
247
|
+
res.status(200).send(xml);
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
async function getSitemapStaticPaths(options) {
|
|
251
|
+
const { urlsPerSitemap = 5e3, exclude, debug = false } = options;
|
|
252
|
+
const localeSegment = options.localeSegment ?? "";
|
|
253
|
+
const routes = extractRoutes(options.pagesContext, localeSegment);
|
|
254
|
+
const allPaths = await getAllPaths(routes, debug);
|
|
255
|
+
const filteredPaths = allPaths.filter((pathname) => !shouldExclude(pathname, exclude));
|
|
256
|
+
const sitemapCount = Math.max(1, Math.ceil(filteredPaths.length / urlsPerSitemap));
|
|
257
|
+
return {
|
|
258
|
+
paths: Array.from({ length: sitemapCount }, (_, i) => ({
|
|
259
|
+
params: { id: String(i) }
|
|
260
|
+
})),
|
|
261
|
+
fallback: false
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
exports.createSitemapApiHandler = createSitemapApiHandler;
|
|
266
|
+
exports.createSitemapIndexApiHandler = createSitemapIndexApiHandler;
|
|
267
|
+
exports.getSitemapStaticPaths = getSitemapStaticPaths;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { NextApiRequest, NextApiResponse } from 'next';
|
|
2
|
+
|
|
3
|
+
type ChangeFrequency = "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
|
|
4
|
+
interface SitemapConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Base URL of the site (e.g., "https://example.com")
|
|
7
|
+
*/
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
/**
|
|
10
|
+
* List of supported locales (e.g., ["en", "fr"])
|
|
11
|
+
*/
|
|
12
|
+
locales?: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Default locale (e.g., "en")
|
|
15
|
+
*/
|
|
16
|
+
defaultLocale?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Number of URLs per sitemap file (default: 5000)
|
|
19
|
+
*/
|
|
20
|
+
urlsPerSitemap?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Path to the app directory relative to the project root
|
|
23
|
+
* Default: "./src/app" or "./app"
|
|
24
|
+
*/
|
|
25
|
+
appDir?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Locale segment in the URL path (e.g., "[locale]")
|
|
28
|
+
* Auto-detected as "[locale]" when locales or defaultLocale is set
|
|
29
|
+
*/
|
|
30
|
+
localeSegment?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Exclude routes from the sitemap
|
|
33
|
+
* Can be an array of glob patterns or a function that returns true to exclude
|
|
34
|
+
* @example exclude: ["/admin/*", "/api/*"]
|
|
35
|
+
* @example exclude: (path) => path.startsWith("/private")
|
|
36
|
+
*/
|
|
37
|
+
exclude?: string[] | ((path: string) => boolean);
|
|
38
|
+
/**
|
|
39
|
+
* Priority for sitemap entries
|
|
40
|
+
* Can be a fixed number (0.0-1.0), a function, or "auto" for depth-based calculation
|
|
41
|
+
* Default: "auto" (1.0 - depth * 0.2, minimum 0.1)
|
|
42
|
+
* @example priority: 0.8
|
|
43
|
+
* @example priority: (path) => path === "/" ? 1.0 : 0.7
|
|
44
|
+
* @example priority: "auto"
|
|
45
|
+
*/
|
|
46
|
+
priority?: number | "auto" | ((path: string) => number);
|
|
47
|
+
/**
|
|
48
|
+
* Change frequency for sitemap entries
|
|
49
|
+
* Can be a fixed value or a function
|
|
50
|
+
* Default: "weekly"
|
|
51
|
+
* @example changeFreq: "daily"
|
|
52
|
+
* @example changeFreq: (path) => path === "/" ? "daily" : "weekly"
|
|
53
|
+
*/
|
|
54
|
+
changeFreq?: ChangeFrequency | ((path: string) => ChangeFrequency);
|
|
55
|
+
/**
|
|
56
|
+
* Additional sitemaps to include in the sitemap index
|
|
57
|
+
* Useful for custom sitemaps (e.g., products from an API)
|
|
58
|
+
* @example additionalSitemaps: ["/products-sitemap.xml", "/blog-sitemap.xml"]
|
|
59
|
+
*/
|
|
60
|
+
additionalSitemaps?: string[];
|
|
61
|
+
}
|
|
62
|
+
interface SitemapEntry {
|
|
63
|
+
url: string;
|
|
64
|
+
lastModified?: string | Date;
|
|
65
|
+
changeFrequency?: ChangeFrequency;
|
|
66
|
+
priority?: number;
|
|
67
|
+
alternates?: {
|
|
68
|
+
languages?: Record<string, string>;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
type PageModule = {
|
|
72
|
+
generateStaticParams?: () => Promise<Record<string, string>[]>;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
interface CreateSitemapApiHandlerOptions extends SitemapConfig {
|
|
76
|
+
/**
|
|
77
|
+
* The require.context result for page discovery
|
|
78
|
+
* Example: require.context('./', true, /^\.\/(?!\[|_|api\/).*\.tsx$/)
|
|
79
|
+
*/
|
|
80
|
+
pagesContext: {
|
|
81
|
+
keys: () => string[];
|
|
82
|
+
(key: string): PageModule;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Enable debug logging to diagnose issues with route discovery
|
|
86
|
+
* Logs info about getStaticPaths calls and skipped routes
|
|
87
|
+
* @default false
|
|
88
|
+
*/
|
|
89
|
+
debug?: boolean;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Create API handler for sitemap index route
|
|
93
|
+
* Use in: pages/api/sitemap.xml.ts or pages/sitemap.xml.ts (with rewrites)
|
|
94
|
+
*/
|
|
95
|
+
declare function createSitemapIndexApiHandler(options: CreateSitemapApiHandlerOptions): (_req: NextApiRequest, res: NextApiResponse) => Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Create API handler for individual sitemap routes
|
|
98
|
+
* Use in: pages/api/sitemap/[id].ts or pages/sitemap-[id].xml.ts (with rewrites)
|
|
99
|
+
*/
|
|
100
|
+
declare function createSitemapApiHandler(options: CreateSitemapApiHandlerOptions): (req: NextApiRequest, res: NextApiResponse) => Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Get the list of sitemap IDs for getStaticPaths
|
|
103
|
+
* Use this to pre-render sitemap pages at build time
|
|
104
|
+
*/
|
|
105
|
+
declare function getSitemapStaticPaths(options: CreateSitemapApiHandlerOptions): Promise<{
|
|
106
|
+
paths: {
|
|
107
|
+
params: {
|
|
108
|
+
id: string;
|
|
109
|
+
};
|
|
110
|
+
}[];
|
|
111
|
+
fallback: boolean;
|
|
112
|
+
}>;
|
|
113
|
+
|
|
114
|
+
export { type ChangeFrequency, type CreateSitemapApiHandlerOptions, type SitemapConfig, type SitemapEntry, createSitemapApiHandler, createSitemapIndexApiHandler, getSitemapStaticPaths };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { NextApiRequest, NextApiResponse } from 'next';
|
|
2
|
+
|
|
3
|
+
type ChangeFrequency = "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
|
|
4
|
+
interface SitemapConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Base URL of the site (e.g., "https://example.com")
|
|
7
|
+
*/
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
/**
|
|
10
|
+
* List of supported locales (e.g., ["en", "fr"])
|
|
11
|
+
*/
|
|
12
|
+
locales?: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Default locale (e.g., "en")
|
|
15
|
+
*/
|
|
16
|
+
defaultLocale?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Number of URLs per sitemap file (default: 5000)
|
|
19
|
+
*/
|
|
20
|
+
urlsPerSitemap?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Path to the app directory relative to the project root
|
|
23
|
+
* Default: "./src/app" or "./app"
|
|
24
|
+
*/
|
|
25
|
+
appDir?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Locale segment in the URL path (e.g., "[locale]")
|
|
28
|
+
* Auto-detected as "[locale]" when locales or defaultLocale is set
|
|
29
|
+
*/
|
|
30
|
+
localeSegment?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Exclude routes from the sitemap
|
|
33
|
+
* Can be an array of glob patterns or a function that returns true to exclude
|
|
34
|
+
* @example exclude: ["/admin/*", "/api/*"]
|
|
35
|
+
* @example exclude: (path) => path.startsWith("/private")
|
|
36
|
+
*/
|
|
37
|
+
exclude?: string[] | ((path: string) => boolean);
|
|
38
|
+
/**
|
|
39
|
+
* Priority for sitemap entries
|
|
40
|
+
* Can be a fixed number (0.0-1.0), a function, or "auto" for depth-based calculation
|
|
41
|
+
* Default: "auto" (1.0 - depth * 0.2, minimum 0.1)
|
|
42
|
+
* @example priority: 0.8
|
|
43
|
+
* @example priority: (path) => path === "/" ? 1.0 : 0.7
|
|
44
|
+
* @example priority: "auto"
|
|
45
|
+
*/
|
|
46
|
+
priority?: number | "auto" | ((path: string) => number);
|
|
47
|
+
/**
|
|
48
|
+
* Change frequency for sitemap entries
|
|
49
|
+
* Can be a fixed value or a function
|
|
50
|
+
* Default: "weekly"
|
|
51
|
+
* @example changeFreq: "daily"
|
|
52
|
+
* @example changeFreq: (path) => path === "/" ? "daily" : "weekly"
|
|
53
|
+
*/
|
|
54
|
+
changeFreq?: ChangeFrequency | ((path: string) => ChangeFrequency);
|
|
55
|
+
/**
|
|
56
|
+
* Additional sitemaps to include in the sitemap index
|
|
57
|
+
* Useful for custom sitemaps (e.g., products from an API)
|
|
58
|
+
* @example additionalSitemaps: ["/products-sitemap.xml", "/blog-sitemap.xml"]
|
|
59
|
+
*/
|
|
60
|
+
additionalSitemaps?: string[];
|
|
61
|
+
}
|
|
62
|
+
interface SitemapEntry {
|
|
63
|
+
url: string;
|
|
64
|
+
lastModified?: string | Date;
|
|
65
|
+
changeFrequency?: ChangeFrequency;
|
|
66
|
+
priority?: number;
|
|
67
|
+
alternates?: {
|
|
68
|
+
languages?: Record<string, string>;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
type PageModule = {
|
|
72
|
+
generateStaticParams?: () => Promise<Record<string, string>[]>;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
interface CreateSitemapApiHandlerOptions extends SitemapConfig {
|
|
76
|
+
/**
|
|
77
|
+
* The require.context result for page discovery
|
|
78
|
+
* Example: require.context('./', true, /^\.\/(?!\[|_|api\/).*\.tsx$/)
|
|
79
|
+
*/
|
|
80
|
+
pagesContext: {
|
|
81
|
+
keys: () => string[];
|
|
82
|
+
(key: string): PageModule;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Enable debug logging to diagnose issues with route discovery
|
|
86
|
+
* Logs info about getStaticPaths calls and skipped routes
|
|
87
|
+
* @default false
|
|
88
|
+
*/
|
|
89
|
+
debug?: boolean;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Create API handler for sitemap index route
|
|
93
|
+
* Use in: pages/api/sitemap.xml.ts or pages/sitemap.xml.ts (with rewrites)
|
|
94
|
+
*/
|
|
95
|
+
declare function createSitemapIndexApiHandler(options: CreateSitemapApiHandlerOptions): (_req: NextApiRequest, res: NextApiResponse) => Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Create API handler for individual sitemap routes
|
|
98
|
+
* Use in: pages/api/sitemap/[id].ts or pages/sitemap-[id].xml.ts (with rewrites)
|
|
99
|
+
*/
|
|
100
|
+
declare function createSitemapApiHandler(options: CreateSitemapApiHandlerOptions): (req: NextApiRequest, res: NextApiResponse) => Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Get the list of sitemap IDs for getStaticPaths
|
|
103
|
+
* Use this to pre-render sitemap pages at build time
|
|
104
|
+
*/
|
|
105
|
+
declare function getSitemapStaticPaths(options: CreateSitemapApiHandlerOptions): Promise<{
|
|
106
|
+
paths: {
|
|
107
|
+
params: {
|
|
108
|
+
id: string;
|
|
109
|
+
};
|
|
110
|
+
}[];
|
|
111
|
+
fallback: boolean;
|
|
112
|
+
}>;
|
|
113
|
+
|
|
114
|
+
export { type ChangeFrequency, type CreateSitemapApiHandlerOptions, type SitemapConfig, type SitemapEntry, createSitemapApiHandler, createSitemapIndexApiHandler, getSitemapStaticPaths };
|
|
@@ -0,0 +1,263 @@
|
|
|
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/pages/index.ts
|
|
96
|
+
function extractRoutes(pagesContext, localeSegment) {
|
|
97
|
+
const routes = [];
|
|
98
|
+
for (const key of pagesContext.keys()) {
|
|
99
|
+
if (key.includes("[...")) continue;
|
|
100
|
+
if (key.includes("/api/")) continue;
|
|
101
|
+
if (key.includes("/_")) continue;
|
|
102
|
+
const pageModule = pagesContext(key);
|
|
103
|
+
let pathname = key.replace(/^\.\//, "/").replace(/\.tsx?$/, "").replace(/\/index$/, "/");
|
|
104
|
+
if (pathname === "") pathname = "/";
|
|
105
|
+
if (localeSegment) {
|
|
106
|
+
pathname = pathname.replace(
|
|
107
|
+
new RegExp(`^/${localeSegment.replace(/[[\]]/g, "\\$&")}`),
|
|
108
|
+
""
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (/(?:^|\/)(src|pages)(?:\/|$)/.test(pathname)) continue;
|
|
112
|
+
if (!pathname || pathname === "") {
|
|
113
|
+
pathname = "/";
|
|
114
|
+
} else if (!pathname.startsWith("/")) {
|
|
115
|
+
pathname = "/" + pathname;
|
|
116
|
+
}
|
|
117
|
+
const dynamicSegments = pathname.match(/\[([^\]]+)\]/g)?.map((s) => s.slice(1, -1)) || [];
|
|
118
|
+
const getStaticPaths = pageModule.getStaticPaths;
|
|
119
|
+
let getParams = null;
|
|
120
|
+
if (getStaticPaths) {
|
|
121
|
+
getParams = async () => {
|
|
122
|
+
const result = await getStaticPaths();
|
|
123
|
+
return result.paths.map((p) => p.params);
|
|
124
|
+
};
|
|
125
|
+
} else if (pageModule.generateStaticParams) {
|
|
126
|
+
getParams = pageModule.generateStaticParams;
|
|
127
|
+
}
|
|
128
|
+
routes.push({
|
|
129
|
+
pathname,
|
|
130
|
+
dynamicSegments,
|
|
131
|
+
getParams
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return routes;
|
|
135
|
+
}
|
|
136
|
+
async function getAllPaths(routes, debug = false) {
|
|
137
|
+
const allPaths = ["/"];
|
|
138
|
+
const seenPaths = /* @__PURE__ */ new Set(["/"]);
|
|
139
|
+
for (const route of routes) {
|
|
140
|
+
if (route.dynamicSegments.length === 0) {
|
|
141
|
+
if (route.pathname !== "/" && !seenPaths.has(route.pathname)) {
|
|
142
|
+
allPaths.push(route.pathname);
|
|
143
|
+
seenPaths.add(route.pathname);
|
|
144
|
+
}
|
|
145
|
+
} else if (route.getParams) {
|
|
146
|
+
try {
|
|
147
|
+
const params = await route.getParams();
|
|
148
|
+
if (debug) {
|
|
149
|
+
console.log(`[next-sitemap] ${route.pathname}: getStaticPaths returned ${params.length} params`);
|
|
150
|
+
}
|
|
151
|
+
for (const param of params) {
|
|
152
|
+
let dynamicPath = route.pathname;
|
|
153
|
+
for (const segment of route.dynamicSegments) {
|
|
154
|
+
const value = param[segment];
|
|
155
|
+
if (value === void 0) {
|
|
156
|
+
if (debug) {
|
|
157
|
+
console.warn(`[next-sitemap] ${route.pathname}: missing param "${segment}" in`, param);
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
dynamicPath = dynamicPath.replace(`[${segment}]`, value);
|
|
162
|
+
}
|
|
163
|
+
if (!seenPaths.has(dynamicPath)) {
|
|
164
|
+
allPaths.push(dynamicPath);
|
|
165
|
+
seenPaths.add(dynamicPath);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error(`[next-sitemap] Error calling getStaticPaths for ${route.pathname}:`, error);
|
|
170
|
+
}
|
|
171
|
+
} else if (route.dynamicSegments.length > 0) {
|
|
172
|
+
if (debug) {
|
|
173
|
+
console.warn(
|
|
174
|
+
`[next-sitemap] Skipping dynamic route ${route.pathname}: no getStaticPaths exported. Use additionalSitemaps for routes that fetch data at runtime.`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return allPaths;
|
|
180
|
+
}
|
|
181
|
+
function pathsToEntries(paths, config) {
|
|
182
|
+
const { baseUrl, locales = [], defaultLocale, exclude, priority, changeFreq } = config;
|
|
183
|
+
const filteredPaths = paths.filter((pathname) => !shouldExclude(pathname, exclude));
|
|
184
|
+
return filteredPaths.map((pathname) => {
|
|
185
|
+
const entry = {
|
|
186
|
+
url: buildUrl(baseUrl, pathname, defaultLocale, defaultLocale),
|
|
187
|
+
lastModified: /* @__PURE__ */ new Date(),
|
|
188
|
+
changeFrequency: getChangeFreq(pathname, changeFreq),
|
|
189
|
+
priority: getPriority(pathname, priority)
|
|
190
|
+
};
|
|
191
|
+
if (locales.length > 0) {
|
|
192
|
+
entry.alternates = {
|
|
193
|
+
languages: Object.fromEntries(
|
|
194
|
+
locales.map((locale) => [
|
|
195
|
+
locale,
|
|
196
|
+
buildUrl(baseUrl, pathname, locale, defaultLocale)
|
|
197
|
+
])
|
|
198
|
+
)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return entry;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function createSitemapIndexApiHandler(options) {
|
|
205
|
+
const { urlsPerSitemap = 5e3, additionalSitemaps, exclude, debug = false } = options;
|
|
206
|
+
const localeSegment = options.localeSegment ?? "";
|
|
207
|
+
const routes = extractRoutes(options.pagesContext, localeSegment);
|
|
208
|
+
if (debug) {
|
|
209
|
+
console.log(`[next-sitemap] Found ${routes.length} routes:`);
|
|
210
|
+
routes.forEach((r) => {
|
|
211
|
+
const hasParams = r.getParams ? "\u2713 getStaticPaths" : "\u2717 no getStaticPaths";
|
|
212
|
+
const segments = r.dynamicSegments.length > 0 ? ` [${r.dynamicSegments.join(", ")}]` : "";
|
|
213
|
+
console.log(` ${r.pathname}${segments} - ${hasParams}`);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return async function handler(_req, res) {
|
|
217
|
+
const allPaths = await getAllPaths(routes, debug);
|
|
218
|
+
const filteredPaths = allPaths.filter((pathname) => !shouldExclude(pathname, exclude));
|
|
219
|
+
const sitemapCount = Math.max(1, Math.ceil(filteredPaths.length / urlsPerSitemap));
|
|
220
|
+
const xml = generateSitemapIndexXml(options.baseUrl, sitemapCount, {
|
|
221
|
+
additionalSitemaps
|
|
222
|
+
});
|
|
223
|
+
res.setHeader("Content-Type", "application/xml");
|
|
224
|
+
res.status(200).send(xml);
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function createSitemapApiHandler(options) {
|
|
228
|
+
const { urlsPerSitemap = 5e3, exclude, debug = false } = options;
|
|
229
|
+
const localeSegment = options.localeSegment ?? "";
|
|
230
|
+
const routes = extractRoutes(options.pagesContext, localeSegment);
|
|
231
|
+
const getFilteredPaths = async () => {
|
|
232
|
+
const allPaths = await getAllPaths(routes, debug);
|
|
233
|
+
return allPaths.filter((pathname) => !shouldExclude(pathname, exclude));
|
|
234
|
+
};
|
|
235
|
+
return async function handler(req, res) {
|
|
236
|
+
const { id } = req.query;
|
|
237
|
+
const sitemapId = parseInt(Array.isArray(id) ? id[0] : id || "0", 10);
|
|
238
|
+
const filteredPaths = await getFilteredPaths();
|
|
239
|
+
const start = sitemapId * urlsPerSitemap;
|
|
240
|
+
const end = start + urlsPerSitemap;
|
|
241
|
+
const paths = filteredPaths.slice(start, end);
|
|
242
|
+
const entries = pathsToEntries(paths, { ...options, exclude: void 0 });
|
|
243
|
+
const xml = generateSitemapXml(entries);
|
|
244
|
+
res.setHeader("Content-Type", "application/xml");
|
|
245
|
+
res.status(200).send(xml);
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
async function getSitemapStaticPaths(options) {
|
|
249
|
+
const { urlsPerSitemap = 5e3, exclude, debug = false } = options;
|
|
250
|
+
const localeSegment = options.localeSegment ?? "";
|
|
251
|
+
const routes = extractRoutes(options.pagesContext, localeSegment);
|
|
252
|
+
const allPaths = await getAllPaths(routes, debug);
|
|
253
|
+
const filteredPaths = allPaths.filter((pathname) => !shouldExclude(pathname, exclude));
|
|
254
|
+
const sitemapCount = Math.max(1, Math.ceil(filteredPaths.length / urlsPerSitemap));
|
|
255
|
+
return {
|
|
256
|
+
paths: Array.from({ length: sitemapCount }, (_, i) => ({
|
|
257
|
+
params: { id: String(i) }
|
|
258
|
+
})),
|
|
259
|
+
fallback: false
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export { createSitemapApiHandler, createSitemapIndexApiHandler, getSitemapStaticPaths };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onruntime/next-sitemap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Dynamic sitemap generation for Next.js with automatic route discovery",
|
|
5
5
|
"author": "onRuntime Studio <contact@onruntime.com>",
|
|
6
6
|
"repository": {
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"types": "./dist/app/index.d.ts",
|
|
29
29
|
"import": "./dist/app/index.js",
|
|
30
30
|
"require": "./dist/app/index.cjs"
|
|
31
|
+
},
|
|
32
|
+
"./pages": {
|
|
33
|
+
"types": "./dist/pages/index.d.ts",
|
|
34
|
+
"import": "./dist/pages/index.js",
|
|
35
|
+
"require": "./dist/pages/index.cjs"
|
|
31
36
|
}
|
|
32
37
|
},
|
|
33
38
|
"files": [
|
|
@@ -46,6 +51,7 @@
|
|
|
46
51
|
"sitemap",
|
|
47
52
|
"nextjs",
|
|
48
53
|
"app-router",
|
|
54
|
+
"pages-router",
|
|
49
55
|
"seo",
|
|
50
56
|
"xml",
|
|
51
57
|
"dynamic-sitemap"
|