@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 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;
@@ -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 };
@@ -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
+ }