@djangocfg/nextjs 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +449 -0
- package/package.json +133 -0
- package/src/components/HomePage.tsx +73 -0
- package/src/components/index.ts +7 -0
- package/src/config/base-next-config.ts +262 -0
- package/src/config/index.ts +6 -0
- package/src/contact/index.ts +13 -0
- package/src/contact/route.ts +102 -0
- package/src/contact/submit.ts +80 -0
- package/src/errors/ErrorLayout.tsx +228 -0
- package/src/errors/errorConfig.ts +118 -0
- package/src/errors/index.ts +10 -0
- package/src/health/index.ts +7 -0
- package/src/health/route.ts +65 -0
- package/src/health/types.ts +19 -0
- package/src/index.ts +36 -0
- package/src/legal/LegalPage.tsx +85 -0
- package/src/legal/configs.ts +131 -0
- package/src/legal/index.ts +24 -0
- package/src/legal/pages.tsx +58 -0
- package/src/legal/types.ts +15 -0
- package/src/navigation/index.ts +9 -0
- package/src/navigation/types.ts +68 -0
- package/src/navigation/utils.ts +181 -0
- package/src/og-image/README.md +66 -0
- package/src/og-image/components/DefaultTemplate.tsx +369 -0
- package/src/og-image/components/index.ts +9 -0
- package/src/og-image/index.ts +27 -0
- package/src/og-image/route.tsx +253 -0
- package/src/og-image/types.ts +46 -0
- package/src/og-image/utils/fonts.ts +150 -0
- package/src/og-image/utils/index.ts +28 -0
- package/src/og-image/utils/metadata.ts +235 -0
- package/src/og-image/utils/url.ts +327 -0
- package/src/sitemap/generator.ts +64 -0
- package/src/sitemap/index.ts +8 -0
- package/src/sitemap/route.ts +74 -0
- package/src/sitemap/types.ts +20 -0
- package/src/types.ts +35 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL Generation Helpers for OG Images
|
|
3
|
+
*
|
|
4
|
+
* Utilities to generate OG image URLs with proper query parameters
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Encode string to base64 with Unicode support
|
|
9
|
+
* Works in both browser and Node.js environments
|
|
10
|
+
*/
|
|
11
|
+
function encodeBase64(str: string): string {
|
|
12
|
+
// Node.js environment
|
|
13
|
+
if (typeof Buffer !== 'undefined') {
|
|
14
|
+
return Buffer.from(str, 'utf-8').toString('base64');
|
|
15
|
+
}
|
|
16
|
+
// Browser environment - handle Unicode via UTF-8 encoding
|
|
17
|
+
return btoa(unescape(encodeURIComponent(str)));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Decode base64 string with Unicode support
|
|
22
|
+
* Works in both browser, Node.js, and Edge Runtime environments
|
|
23
|
+
*/
|
|
24
|
+
function decodeBase64(str: string): string {
|
|
25
|
+
// Node.js environment
|
|
26
|
+
if (typeof Buffer !== 'undefined') {
|
|
27
|
+
return Buffer.from(str, 'base64').toString('utf-8');
|
|
28
|
+
}
|
|
29
|
+
// Edge Runtime / Browser environment - handle Unicode via UTF-8 decoding
|
|
30
|
+
// atob is available in Edge Runtime
|
|
31
|
+
try {
|
|
32
|
+
const binaryString = atob(str);
|
|
33
|
+
// Convert binary string to UTF-8
|
|
34
|
+
return decodeURIComponent(
|
|
35
|
+
binaryString
|
|
36
|
+
.split('')
|
|
37
|
+
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
|
38
|
+
.join('')
|
|
39
|
+
);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
// Fallback to simpler method if above fails
|
|
42
|
+
return decodeURIComponent(escape(atob(str)));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* OG Image URL parameters
|
|
48
|
+
*/
|
|
49
|
+
export interface OgImageUrlParams {
|
|
50
|
+
/** Page title */
|
|
51
|
+
title: string;
|
|
52
|
+
/** Page description (optional) */
|
|
53
|
+
description?: string;
|
|
54
|
+
/** Site name (optional) */
|
|
55
|
+
siteName?: string;
|
|
56
|
+
/** Logo URL (optional) */
|
|
57
|
+
logo?: string;
|
|
58
|
+
/** Additional custom parameters */
|
|
59
|
+
[key: string]: string | number | boolean | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generate OG image URL with query parameters or base64 encoding
|
|
64
|
+
*
|
|
65
|
+
* @param baseUrl - Base URL of the OG image API route (e.g., '/api/og' or 'https://example.com/api/og')
|
|
66
|
+
* @param params - URL parameters for the OG image
|
|
67
|
+
* @param useBase64 - If true, encode params as base64 for safer URLs (default: true)
|
|
68
|
+
* @returns Complete OG image URL with encoded parameters
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* // Base64 encoding (safe, default)
|
|
73
|
+
* const url = generateOgImageUrl('/api/og', {
|
|
74
|
+
* title: 'My Page Title',
|
|
75
|
+
* description: 'Page description here',
|
|
76
|
+
* });
|
|
77
|
+
* // Result: /api/og?data=eyJ0aXRsZSI6Ik15IFBhZ2UgVGl0bGUiLCJkZXNjcmlwdGlvbiI6IlBhZ2UgZGVzY3JpcHRpb24gaGVyZSJ9
|
|
78
|
+
*
|
|
79
|
+
* // Query params (legacy)
|
|
80
|
+
* const url = generateOgImageUrl('/api/og', { title: 'Hello' }, false);
|
|
81
|
+
* // Result: /api/og?title=Hello
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export function generateOgImageUrl(
|
|
85
|
+
baseUrl: string,
|
|
86
|
+
params: OgImageUrlParams,
|
|
87
|
+
useBase64: boolean = true
|
|
88
|
+
): string {
|
|
89
|
+
if (useBase64) {
|
|
90
|
+
// Clean params - remove undefined/null/empty values
|
|
91
|
+
const cleanParams: Record<string, string | number | boolean> = {};
|
|
92
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
93
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
94
|
+
cleanParams[key] = value;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Encode as base64 (Unicode-safe)
|
|
99
|
+
const jsonString = JSON.stringify(cleanParams);
|
|
100
|
+
const base64Data = encodeBase64(jsonString);
|
|
101
|
+
|
|
102
|
+
// CRITICAL: Use path parameter instead of query parameter
|
|
103
|
+
// Next.js strips query params in internal requests for metadata generation
|
|
104
|
+
// Using /api/og/[data] instead of /api/og?data=... preserves the data
|
|
105
|
+
return `${baseUrl}/${base64Data}`;
|
|
106
|
+
} else {
|
|
107
|
+
// Legacy query params mode
|
|
108
|
+
const searchParams = new URLSearchParams();
|
|
109
|
+
|
|
110
|
+
// Add all defined parameters
|
|
111
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
112
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
113
|
+
searchParams.append(key, String(value));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const query = searchParams.toString();
|
|
118
|
+
return query ? `${baseUrl}?${query}` : baseUrl;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get absolute OG image URL from relative path
|
|
124
|
+
*
|
|
125
|
+
* Useful for generating absolute URLs required by Open Graph meta tags
|
|
126
|
+
*
|
|
127
|
+
* @param relativePath - Relative OG image path (e.g., '/api/og?title=Hello')
|
|
128
|
+
* @param siteUrl - Base site URL (e.g., 'https://example.com')
|
|
129
|
+
* @returns Absolute URL
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```typescript
|
|
133
|
+
* const absolute = getAbsoluteOgImageUrl(
|
|
134
|
+
* '/api/og?title=Hello',
|
|
135
|
+
* 'https://example.com'
|
|
136
|
+
* );
|
|
137
|
+
* // Result: https://example.com/api/og?title=Hello
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export function getAbsoluteOgImageUrl(
|
|
141
|
+
relativePath: string,
|
|
142
|
+
siteUrl: string
|
|
143
|
+
): string {
|
|
144
|
+
// Remove trailing slash from site URL
|
|
145
|
+
const cleanSiteUrl = siteUrl.replace(/\/$/, '');
|
|
146
|
+
|
|
147
|
+
// Ensure relative path starts with /
|
|
148
|
+
const cleanPath = relativePath.startsWith('/')
|
|
149
|
+
? relativePath
|
|
150
|
+
: `/${relativePath}`;
|
|
151
|
+
|
|
152
|
+
return `${cleanSiteUrl}${cleanPath}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Create OG image URL builder with preset configuration
|
|
157
|
+
*
|
|
158
|
+
* Useful when you want to reuse the same base URL and default parameters
|
|
159
|
+
*
|
|
160
|
+
* @param baseUrl - Base URL of the OG image API route
|
|
161
|
+
* @param defaults - Default parameters to merge with each URL generation
|
|
162
|
+
* @returns URL builder function
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```typescript
|
|
166
|
+
* const buildOgUrl = createOgImageUrlBuilder('/api/og', {
|
|
167
|
+
* siteName: 'My Site',
|
|
168
|
+
* logo: '/logo.png'
|
|
169
|
+
* });
|
|
170
|
+
*
|
|
171
|
+
* const url1 = buildOgUrl({ title: 'Page 1' });
|
|
172
|
+
* const url2 = buildOgUrl({ title: 'Page 2', description: 'Custom desc' });
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
export function createOgImageUrlBuilder(
|
|
176
|
+
baseUrl: string,
|
|
177
|
+
defaults: Partial<OgImageUrlParams> = {}
|
|
178
|
+
) {
|
|
179
|
+
return (params: OgImageUrlParams): string => {
|
|
180
|
+
return generateOgImageUrl(baseUrl, {
|
|
181
|
+
...defaults,
|
|
182
|
+
...params,
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Parse OG image URL parameters from a URL string (legacy query params)
|
|
189
|
+
*
|
|
190
|
+
* @param url - Full or relative URL with query parameters
|
|
191
|
+
* @returns Parsed parameters object
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```typescript
|
|
195
|
+
* const params = parseOgImageUrl('/api/og?title=Hello&description=World');
|
|
196
|
+
* // Result: { title: 'Hello', description: 'World' }
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
export function parseOgImageUrl(url: string): Record<string, string> {
|
|
200
|
+
try {
|
|
201
|
+
const urlObj = new URL(url, 'http://dummy.com');
|
|
202
|
+
const params: Record<string, string> = {};
|
|
203
|
+
|
|
204
|
+
urlObj.searchParams.forEach((value, key) => {
|
|
205
|
+
params[key] = value;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return params;
|
|
209
|
+
} catch {
|
|
210
|
+
return {};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Parse OG image data from base64-encoded query parameter
|
|
216
|
+
*
|
|
217
|
+
* Use this in your API route to decode the `data` parameter
|
|
218
|
+
* Supports both base64 (new) and legacy query params format
|
|
219
|
+
*
|
|
220
|
+
* @param searchParams - URL search params or request object
|
|
221
|
+
* @returns Parsed OG image parameters
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```typescript
|
|
225
|
+
* // In Next.js API route (pages/api/og.ts)
|
|
226
|
+
* export default function handler(req) {
|
|
227
|
+
* const params = parseOgImageData(req.query);
|
|
228
|
+
* // { title: 'Hello', description: 'World' }
|
|
229
|
+
* }
|
|
230
|
+
*
|
|
231
|
+
* // In Next.js App Router (app/api/og/route.ts)
|
|
232
|
+
* export async function GET(request: Request) {
|
|
233
|
+
* const { searchParams } = new URL(request.url);
|
|
234
|
+
* const params = parseOgImageData(Object.fromEntries(searchParams));
|
|
235
|
+
* // { title: 'Hello', description: 'World' }
|
|
236
|
+
* }
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
export function parseOgImageData(
|
|
240
|
+
searchParams: Record<string, string | string[] | undefined> | URLSearchParams
|
|
241
|
+
): Record<string, string> {
|
|
242
|
+
try {
|
|
243
|
+
// Handle URLSearchParams
|
|
244
|
+
let params: Record<string, string | undefined>;
|
|
245
|
+
|
|
246
|
+
if (searchParams instanceof URLSearchParams) {
|
|
247
|
+
// Convert URLSearchParams to object
|
|
248
|
+
params = {};
|
|
249
|
+
for (const [key, value] of searchParams.entries()) {
|
|
250
|
+
params[key] = value;
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
params = searchParams as Record<string, string | undefined>;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Debug logging
|
|
257
|
+
if (process.env.NODE_ENV === 'development') {
|
|
258
|
+
console.log('[parseOgImageData] Input params keys:', Object.keys(params));
|
|
259
|
+
console.log('[parseOgImageData] Input params:', params);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check for base64-encoded data parameter
|
|
263
|
+
const dataParam = params.data;
|
|
264
|
+
if (dataParam && typeof dataParam === 'string' && dataParam.trim() !== '') {
|
|
265
|
+
if (process.env.NODE_ENV === 'development') {
|
|
266
|
+
console.log('[parseOgImageData] Found data param, length:', dataParam.length);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const decoded = decodeBase64(dataParam);
|
|
271
|
+
if (process.env.NODE_ENV === 'development') {
|
|
272
|
+
console.log('[parseOgImageData] Decoded string:', decoded.substring(0, 100));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const parsed = JSON.parse(decoded);
|
|
276
|
+
if (process.env.NODE_ENV === 'development') {
|
|
277
|
+
console.log('[parseOgImageData] Parsed JSON:', parsed);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Ensure all values are strings
|
|
281
|
+
const result: Record<string, string> = {};
|
|
282
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
283
|
+
if (value !== undefined && value !== null) {
|
|
284
|
+
result[key] = String(value);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (process.env.NODE_ENV === 'development') {
|
|
289
|
+
console.log('[parseOgImageData] Result:', result);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return result;
|
|
293
|
+
} catch (decodeError) {
|
|
294
|
+
console.error('[parseOgImageData] Error decoding/parsing data param:', decodeError);
|
|
295
|
+
if (decodeError instanceof Error) {
|
|
296
|
+
console.error('[parseOgImageData] Error message:', decodeError.message);
|
|
297
|
+
}
|
|
298
|
+
// Fall through to legacy query params
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
if (process.env.NODE_ENV === 'development') {
|
|
302
|
+
console.log('[parseOgImageData] No data param found or empty');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Fallback to legacy query params format
|
|
307
|
+
const result: Record<string, string> = {};
|
|
308
|
+
for (const [key, value] of Object.entries(params)) {
|
|
309
|
+
if (key !== 'data' && value !== undefined && value !== null) {
|
|
310
|
+
result[key] = Array.isArray(value) ? value[0] : String(value);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (process.env.NODE_ENV === 'development') {
|
|
315
|
+
console.log('[parseOgImageData] Fallback result:', result);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return result;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
console.error('[parseOgImageData] Unexpected error:', error);
|
|
321
|
+
return {};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Export base64 utilities for advanced use cases
|
|
326
|
+
export { encodeBase64, decodeBase64 };
|
|
327
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sitemap Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates XML sitemap from configuration
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { SitemapUrl } from '../types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate XML sitemap string from URLs
|
|
11
|
+
*/
|
|
12
|
+
export function generateSitemapXml(urls: SitemapUrl[]): string {
|
|
13
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
14
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
|
15
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
16
|
+
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
|
|
17
|
+
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
|
18
|
+
${urls
|
|
19
|
+
.map(
|
|
20
|
+
({ loc, lastmod, changefreq, priority }) => ` <url>
|
|
21
|
+
<loc>${escapeXml(loc)}</loc>
|
|
22
|
+
${lastmod ? `<lastmod>${formatDate(lastmod)}</lastmod>` : ''}
|
|
23
|
+
${changefreq ? `<changefreq>${changefreq}</changefreq>` : ''}
|
|
24
|
+
${priority !== undefined ? `<priority>${priority.toFixed(1)}</priority>` : ''}
|
|
25
|
+
</url>`
|
|
26
|
+
)
|
|
27
|
+
.join('\n')}
|
|
28
|
+
</urlset>`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Format date for sitemap (ISO 8601)
|
|
33
|
+
*/
|
|
34
|
+
function formatDate(date: string | Date): string {
|
|
35
|
+
if (typeof date === 'string') {
|
|
36
|
+
return date;
|
|
37
|
+
}
|
|
38
|
+
return date.toISOString().split('T')[0];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Escape XML special characters
|
|
43
|
+
*/
|
|
44
|
+
function escapeXml(unsafe: string): string {
|
|
45
|
+
return unsafe
|
|
46
|
+
.replace(/&/g, '&')
|
|
47
|
+
.replace(/</g, '<')
|
|
48
|
+
.replace(/>/g, '>')
|
|
49
|
+
.replace(/"/g, '"')
|
|
50
|
+
.replace(/'/g, ''');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Normalize URL (ensure absolute)
|
|
55
|
+
*/
|
|
56
|
+
export function normalizeUrl(url: string, siteUrl: string): string {
|
|
57
|
+
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
58
|
+
return url;
|
|
59
|
+
}
|
|
60
|
+
const baseUrl = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl;
|
|
61
|
+
const path = url.startsWith('/') ? url : `/${url}`;
|
|
62
|
+
return `${baseUrl}${path}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sitemap Route Handler for Next.js App Router
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```tsx
|
|
6
|
+
* // app/sitemap.ts
|
|
7
|
+
* import { createSitemapHandler } from '@djangocfg/nextjs/sitemap';
|
|
8
|
+
*
|
|
9
|
+
* export default createSitemapHandler({
|
|
10
|
+
* siteUrl: 'https://example.com',
|
|
11
|
+
* staticPages: [
|
|
12
|
+
* { loc: '/', changefreq: 'daily', priority: 1.0 },
|
|
13
|
+
* { loc: '/about', changefreq: 'monthly', priority: 0.8 },
|
|
14
|
+
* ],
|
|
15
|
+
* dynamicPages: async () => {
|
|
16
|
+
* // Fetch dynamic pages from API
|
|
17
|
+
* const posts = await fetchPosts();
|
|
18
|
+
* return posts.map(post => ({
|
|
19
|
+
* loc: `/posts/${post.slug}`,
|
|
20
|
+
* lastmod: post.updatedAt,
|
|
21
|
+
* changefreq: 'weekly',
|
|
22
|
+
* priority: 0.7,
|
|
23
|
+
* }));
|
|
24
|
+
* },
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { NextResponse } from 'next/server';
|
|
30
|
+
import type { SitemapGeneratorOptions } from './types';
|
|
31
|
+
import { generateSitemapXml, normalizeUrl } from './generator';
|
|
32
|
+
import type { SitemapUrl } from '../types';
|
|
33
|
+
|
|
34
|
+
export function createSitemapHandler(options: SitemapGeneratorOptions) {
|
|
35
|
+
const {
|
|
36
|
+
siteUrl,
|
|
37
|
+
staticPages = [],
|
|
38
|
+
dynamicPages = [],
|
|
39
|
+
cacheControl = 'public, s-maxage=86400, stale-while-revalidate',
|
|
40
|
+
} = options;
|
|
41
|
+
|
|
42
|
+
return async function GET() {
|
|
43
|
+
const urls: SitemapUrl[] = [...staticPages];
|
|
44
|
+
|
|
45
|
+
// Add dynamic pages
|
|
46
|
+
if (dynamicPages) {
|
|
47
|
+
if (typeof dynamicPages === 'function') {
|
|
48
|
+
const dynamicUrls = await dynamicPages();
|
|
49
|
+
urls.push(...dynamicUrls);
|
|
50
|
+
} else {
|
|
51
|
+
urls.push(...dynamicPages);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Normalize all URLs
|
|
56
|
+
const normalizedUrls = urls.map((url) => ({
|
|
57
|
+
...url,
|
|
58
|
+
loc: normalizeUrl(url.loc, siteUrl),
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
// Generate XML
|
|
62
|
+
const sitemap = generateSitemapXml(normalizedUrls);
|
|
63
|
+
|
|
64
|
+
// Return response
|
|
65
|
+
return new NextResponse(sitemap, {
|
|
66
|
+
status: 200,
|
|
67
|
+
headers: {
|
|
68
|
+
'Content-Type': 'application/xml',
|
|
69
|
+
'Cache-Control': cacheControl,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sitemap types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ChangeFreq, SitemapUrl } from '../types';
|
|
6
|
+
|
|
7
|
+
export interface SitemapGeneratorOptions {
|
|
8
|
+
siteUrl: string;
|
|
9
|
+
staticPages?: SitemapUrl[];
|
|
10
|
+
dynamicPages?: (() => Promise<SitemapUrl[]>) | SitemapUrl[];
|
|
11
|
+
cacheControl?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SitemapRoute {
|
|
15
|
+
path: string;
|
|
16
|
+
lastmod?: string | Date;
|
|
17
|
+
changefreq?: ChangeFreq;
|
|
18
|
+
priority?: number;
|
|
19
|
+
}
|
|
20
|
+
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common types for @djangocfg/nextjs package
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type ChangeFreq = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
|
|
6
|
+
|
|
7
|
+
export interface SitemapUrl {
|
|
8
|
+
loc: string;
|
|
9
|
+
lastmod?: string;
|
|
10
|
+
changefreq?: ChangeFreq;
|
|
11
|
+
priority?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SitemapConfig {
|
|
15
|
+
siteUrl: string;
|
|
16
|
+
urls: SitemapUrl[];
|
|
17
|
+
cacheControl?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface HealthResponse {
|
|
21
|
+
status: 'ok' | 'error';
|
|
22
|
+
timestamp: string;
|
|
23
|
+
uptime: number;
|
|
24
|
+
version?: string;
|
|
25
|
+
checks?: Record<string, boolean>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
export interface ErrorPageConfig {
|
|
30
|
+
code?: string | number;
|
|
31
|
+
title?: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
supportEmail?: string;
|
|
34
|
+
}
|
|
35
|
+
|