@onruntime/next-sitemap 0.2.1 → 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 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 |
@@ -118,6 +205,7 @@ export default nextConfig;
118
205
  | `priority` | `number`, `"auto"`, or `function` | `"auto"` | Priority calculation (auto = depth-based) |
119
206
  | `changeFreq` | `ChangeFrequency` or `function` | `"weekly"` | Change frequency for entries |
120
207
  | `additionalSitemaps` | `string[]` | `[]` | Additional sitemaps to include in index |
208
+ | `debug` | `boolean` | `false` | Enable debug logging to diagnose route discovery issues |
121
209
 
122
210
  #### Exclude Routes
123
211
 
@@ -191,9 +279,9 @@ const { GET } = createSitemapIndexHandler({
191
279
 
192
280
  ### How It Works
193
281
 
194
- 1. `require.context` scans your app directory at build time
282
+ 1. `require.context` scans your app/pages directory at build time
195
283
  2. For each page found, it extracts the route path
196
- 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)
197
285
  4. URLs are paginated into multiple sitemaps (default: 5000 URLs each)
198
286
  5. A sitemap index lists all individual sitemaps
199
287
 
@@ -266,6 +354,81 @@ export async function GET() {
266
354
  </urlset>
267
355
  ```
268
356
 
357
+ ## Troubleshooting
358
+
359
+ ### Dynamic routes not included in sitemap
360
+
361
+ If your dynamic routes (e.g., `/articles/[slug]`) are not appearing in the sitemap, enable debug mode to diagnose:
362
+
363
+ ```typescript
364
+ const { GET } = createSitemapIndexHandler({
365
+ baseUrl: "https://example.com",
366
+ pagesContext,
367
+ debug: true, // Enable debug logging
368
+ });
369
+ ```
370
+
371
+ This will log:
372
+ - All discovered routes and whether `generateStaticParams` was found
373
+ - Number of params returned by each `generateStaticParams` call
374
+ - Any errors that occur during param generation
375
+
376
+ **Common issues:**
377
+
378
+ 1. **`generateStaticParams`/`getStaticPaths` not detected**: Make sure it's exported at the top level of your page file:
379
+ ```typescript
380
+ // ✅ Correct (App Router)
381
+ export async function generateStaticParams() { ... }
382
+
383
+ // ✅ Correct (Pages Router)
384
+ export async function getStaticPaths() { ... }
385
+
386
+ // ❌ Wrong - not exported
387
+ async function generateStaticParams() { ... }
388
+ ```
389
+
390
+ 2. **Database/API errors**: If `generateStaticParams` fetches data from a database or API, errors are caught and logged. Check the console for error messages.
391
+
392
+ 3. **Empty params returned**: If `generateStaticParams` returns an empty array, no dynamic paths will be generated.
393
+
394
+ ### Recommended approach for API/Database routes
395
+
396
+ For routes that fetch data from external sources (APIs, databases like Payload CMS), we recommend using `additionalSitemaps` with a custom sitemap route:
397
+
398
+ ```typescript
399
+ // app/sitemap.xml/route.ts
400
+ const { GET } = createSitemapIndexHandler({
401
+ baseUrl: "https://example.com",
402
+ pagesContext,
403
+ additionalSitemaps: ["/articles-sitemap.xml"],
404
+ });
405
+
406
+ // app/articles-sitemap.xml/route.ts
407
+ import { generateSitemapXml } from "@onruntime/next-sitemap";
408
+ import { getPayload } from "payload";
409
+
410
+ export async function GET() {
411
+ const payload = await getPayload({ config: configPromise });
412
+ const articles = await payload.find({
413
+ collection: "articles",
414
+ limit: 1000,
415
+ select: { slug: true, updatedAt: true },
416
+ });
417
+
418
+ const entries = articles.docs.map((article) => ({
419
+ url: `https://example.com/articles/${article.slug}`,
420
+ lastModified: article.updatedAt,
421
+ priority: 0.7,
422
+ }));
423
+
424
+ return new Response(generateSitemapXml(entries), {
425
+ headers: { "Content-Type": "application/xml" },
426
+ });
427
+ }
428
+ ```
429
+
430
+ This approach gives you full control over data fetching and error handling.
431
+
269
432
  ## License
270
433
 
271
434
  MIT
@@ -123,7 +123,7 @@ function extractRoutes(pagesContext, localeSegment) {
123
123
  }
124
124
  return routes;
125
125
  }
126
- async function getAllPaths(routes) {
126
+ async function getAllPaths(routes, debug = false) {
127
127
  const allPaths = ["/"];
128
128
  const seenPaths = /* @__PURE__ */ new Set(["/"]);
129
129
  for (const route of routes) {
@@ -133,16 +133,36 @@ async function getAllPaths(routes) {
133
133
  seenPaths.add(route.pathname);
134
134
  }
135
135
  } else if (route.getParams) {
136
- const params = await route.getParams();
137
- for (const param of params) {
138
- let dynamicPath = route.pathname;
139
- for (const segment of route.dynamicSegments) {
140
- dynamicPath = dynamicPath.replace(`[${segment}]`, param[segment]);
136
+ try {
137
+ const params = await route.getParams();
138
+ if (debug) {
139
+ console.log(`[next-sitemap] ${route.pathname}: generateStaticParams returned ${params.length} params`);
141
140
  }
142
- if (!seenPaths.has(dynamicPath)) {
143
- allPaths.push(dynamicPath);
144
- seenPaths.add(dynamicPath);
141
+ for (const param of params) {
142
+ let dynamicPath = route.pathname;
143
+ for (const segment of route.dynamicSegments) {
144
+ const value = param[segment];
145
+ if (value === void 0) {
146
+ if (debug) {
147
+ console.warn(`[next-sitemap] ${route.pathname}: missing param "${segment}" in`, param);
148
+ }
149
+ continue;
150
+ }
151
+ dynamicPath = dynamicPath.replace(`[${segment}]`, value);
152
+ }
153
+ if (!seenPaths.has(dynamicPath)) {
154
+ allPaths.push(dynamicPath);
155
+ seenPaths.add(dynamicPath);
156
+ }
145
157
  }
158
+ } catch (error) {
159
+ console.error(`[next-sitemap] Error calling generateStaticParams for ${route.pathname}:`, error);
160
+ }
161
+ } else if (route.dynamicSegments.length > 0) {
162
+ if (debug) {
163
+ console.warn(
164
+ `[next-sitemap] Skipping dynamic route ${route.pathname}: no generateStaticParams exported. Use additionalSitemaps for routes that fetch data at runtime.`
165
+ );
146
166
  }
147
167
  }
148
168
  }
@@ -172,12 +192,20 @@ function pathsToEntries(paths, config) {
172
192
  });
173
193
  }
174
194
  function createSitemapIndexHandler(options) {
175
- const { urlsPerSitemap = 5e3, locales = [], defaultLocale, additionalSitemaps, exclude } = options;
195
+ const { urlsPerSitemap = 5e3, locales = [], defaultLocale, additionalSitemaps, exclude, debug = false } = options;
176
196
  const localeSegment = options.localeSegment ?? (locales.length > 0 || defaultLocale ? "[locale]" : "");
177
197
  const routes = extractRoutes(options.pagesContext, localeSegment);
198
+ if (debug) {
199
+ console.log(`[next-sitemap] Found ${routes.length} routes:`);
200
+ routes.forEach((r) => {
201
+ const hasParams = r.getParams ? "\u2713 generateStaticParams" : "\u2717 no generateStaticParams";
202
+ const segments = r.dynamicSegments.length > 0 ? ` [${r.dynamicSegments.join(", ")}]` : "";
203
+ console.log(` ${r.pathname}${segments} - ${hasParams}`);
204
+ });
205
+ }
178
206
  return {
179
207
  GET: async () => {
180
- const allPaths = await getAllPaths(routes);
208
+ const allPaths = await getAllPaths(routes, debug);
181
209
  const filteredPaths = allPaths.filter((pathname) => !shouldExclude(pathname, exclude));
182
210
  const sitemapCount = Math.max(1, Math.ceil(filteredPaths.length / urlsPerSitemap));
183
211
  const xml = generateSitemapIndexXml(options.baseUrl, sitemapCount, {
@@ -190,11 +218,11 @@ function createSitemapIndexHandler(options) {
190
218
  };
191
219
  }
192
220
  function createSitemapHandler(options) {
193
- const { urlsPerSitemap = 5e3, locales = [], defaultLocale, exclude } = options;
221
+ const { urlsPerSitemap = 5e3, locales = [], defaultLocale, exclude, debug = false } = options;
194
222
  const localeSegment = options.localeSegment ?? (locales.length > 0 || defaultLocale ? "[locale]" : "");
195
223
  const routes = extractRoutes(options.pagesContext, localeSegment);
196
224
  const getFilteredPaths = async () => {
197
- const allPaths = await getAllPaths(routes);
225
+ const allPaths = await getAllPaths(routes, debug);
198
226
  return allPaths.filter((pathname) => !shouldExclude(pathname, exclude));
199
227
  };
200
228
  return {
@@ -79,6 +79,12 @@ interface CreateSitemapHandlerOptions extends SitemapConfig {
79
79
  keys: () => string[];
80
80
  (key: string): PageModule;
81
81
  };
82
+ /**
83
+ * Enable debug logging to diagnose issues with route discovery
84
+ * Logs info about generateStaticParams calls and skipped routes
85
+ * @default false
86
+ */
87
+ debug?: boolean;
82
88
  }
83
89
  /**
84
90
  * Create handlers for sitemap index route
@@ -79,6 +79,12 @@ interface CreateSitemapHandlerOptions extends SitemapConfig {
79
79
  keys: () => string[];
80
80
  (key: string): PageModule;
81
81
  };
82
+ /**
83
+ * Enable debug logging to diagnose issues with route discovery
84
+ * Logs info about generateStaticParams calls and skipped routes
85
+ * @default false
86
+ */
87
+ debug?: boolean;
82
88
  }
83
89
  /**
84
90
  * Create handlers for sitemap index route
package/dist/app/index.js CHANGED
@@ -121,7 +121,7 @@ function extractRoutes(pagesContext, localeSegment) {
121
121
  }
122
122
  return routes;
123
123
  }
124
- async function getAllPaths(routes) {
124
+ async function getAllPaths(routes, debug = false) {
125
125
  const allPaths = ["/"];
126
126
  const seenPaths = /* @__PURE__ */ new Set(["/"]);
127
127
  for (const route of routes) {
@@ -131,16 +131,36 @@ async function getAllPaths(routes) {
131
131
  seenPaths.add(route.pathname);
132
132
  }
133
133
  } else if (route.getParams) {
134
- const params = await route.getParams();
135
- for (const param of params) {
136
- let dynamicPath = route.pathname;
137
- for (const segment of route.dynamicSegments) {
138
- dynamicPath = dynamicPath.replace(`[${segment}]`, param[segment]);
134
+ try {
135
+ const params = await route.getParams();
136
+ if (debug) {
137
+ console.log(`[next-sitemap] ${route.pathname}: generateStaticParams returned ${params.length} params`);
139
138
  }
140
- if (!seenPaths.has(dynamicPath)) {
141
- allPaths.push(dynamicPath);
142
- seenPaths.add(dynamicPath);
139
+ for (const param of params) {
140
+ let dynamicPath = route.pathname;
141
+ for (const segment of route.dynamicSegments) {
142
+ const value = param[segment];
143
+ if (value === void 0) {
144
+ if (debug) {
145
+ console.warn(`[next-sitemap] ${route.pathname}: missing param "${segment}" in`, param);
146
+ }
147
+ continue;
148
+ }
149
+ dynamicPath = dynamicPath.replace(`[${segment}]`, value);
150
+ }
151
+ if (!seenPaths.has(dynamicPath)) {
152
+ allPaths.push(dynamicPath);
153
+ seenPaths.add(dynamicPath);
154
+ }
143
155
  }
156
+ } catch (error) {
157
+ console.error(`[next-sitemap] Error calling generateStaticParams for ${route.pathname}:`, error);
158
+ }
159
+ } else if (route.dynamicSegments.length > 0) {
160
+ if (debug) {
161
+ console.warn(
162
+ `[next-sitemap] Skipping dynamic route ${route.pathname}: no generateStaticParams exported. Use additionalSitemaps for routes that fetch data at runtime.`
163
+ );
144
164
  }
145
165
  }
146
166
  }
@@ -170,12 +190,20 @@ function pathsToEntries(paths, config) {
170
190
  });
171
191
  }
172
192
  function createSitemapIndexHandler(options) {
173
- const { urlsPerSitemap = 5e3, locales = [], defaultLocale, additionalSitemaps, exclude } = options;
193
+ const { urlsPerSitemap = 5e3, locales = [], defaultLocale, additionalSitemaps, exclude, debug = false } = options;
174
194
  const localeSegment = options.localeSegment ?? (locales.length > 0 || defaultLocale ? "[locale]" : "");
175
195
  const routes = extractRoutes(options.pagesContext, localeSegment);
196
+ if (debug) {
197
+ console.log(`[next-sitemap] Found ${routes.length} routes:`);
198
+ routes.forEach((r) => {
199
+ const hasParams = r.getParams ? "\u2713 generateStaticParams" : "\u2717 no generateStaticParams";
200
+ const segments = r.dynamicSegments.length > 0 ? ` [${r.dynamicSegments.join(", ")}]` : "";
201
+ console.log(` ${r.pathname}${segments} - ${hasParams}`);
202
+ });
203
+ }
176
204
  return {
177
205
  GET: async () => {
178
- const allPaths = await getAllPaths(routes);
206
+ const allPaths = await getAllPaths(routes, debug);
179
207
  const filteredPaths = allPaths.filter((pathname) => !shouldExclude(pathname, exclude));
180
208
  const sitemapCount = Math.max(1, Math.ceil(filteredPaths.length / urlsPerSitemap));
181
209
  const xml = generateSitemapIndexXml(options.baseUrl, sitemapCount, {
@@ -188,11 +216,11 @@ function createSitemapIndexHandler(options) {
188
216
  };
189
217
  }
190
218
  function createSitemapHandler(options) {
191
- const { urlsPerSitemap = 5e3, locales = [], defaultLocale, exclude } = options;
219
+ const { urlsPerSitemap = 5e3, locales = [], defaultLocale, exclude, debug = false } = options;
192
220
  const localeSegment = options.localeSegment ?? (locales.length > 0 || defaultLocale ? "[locale]" : "");
193
221
  const routes = extractRoutes(options.pagesContext, localeSegment);
194
222
  const getFilteredPaths = async () => {
195
- const allPaths = await getAllPaths(routes);
223
+ const allPaths = await getAllPaths(routes, debug);
196
224
  return allPaths.filter((pathname) => !shouldExclude(pathname, exclude));
197
225
  };
198
226
  return {
@@ -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.2.1",
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"