@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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +449 -0
  3. package/package.json +133 -0
  4. package/src/components/HomePage.tsx +73 -0
  5. package/src/components/index.ts +7 -0
  6. package/src/config/base-next-config.ts +262 -0
  7. package/src/config/index.ts +6 -0
  8. package/src/contact/index.ts +13 -0
  9. package/src/contact/route.ts +102 -0
  10. package/src/contact/submit.ts +80 -0
  11. package/src/errors/ErrorLayout.tsx +228 -0
  12. package/src/errors/errorConfig.ts +118 -0
  13. package/src/errors/index.ts +10 -0
  14. package/src/health/index.ts +7 -0
  15. package/src/health/route.ts +65 -0
  16. package/src/health/types.ts +19 -0
  17. package/src/index.ts +36 -0
  18. package/src/legal/LegalPage.tsx +85 -0
  19. package/src/legal/configs.ts +131 -0
  20. package/src/legal/index.ts +24 -0
  21. package/src/legal/pages.tsx +58 -0
  22. package/src/legal/types.ts +15 -0
  23. package/src/navigation/index.ts +9 -0
  24. package/src/navigation/types.ts +68 -0
  25. package/src/navigation/utils.ts +181 -0
  26. package/src/og-image/README.md +66 -0
  27. package/src/og-image/components/DefaultTemplate.tsx +369 -0
  28. package/src/og-image/components/index.ts +9 -0
  29. package/src/og-image/index.ts +27 -0
  30. package/src/og-image/route.tsx +253 -0
  31. package/src/og-image/types.ts +46 -0
  32. package/src/og-image/utils/fonts.ts +150 -0
  33. package/src/og-image/utils/index.ts +28 -0
  34. package/src/og-image/utils/metadata.ts +235 -0
  35. package/src/og-image/utils/url.ts +327 -0
  36. package/src/sitemap/generator.ts +64 -0
  37. package/src/sitemap/index.ts +8 -0
  38. package/src/sitemap/route.ts +74 -0
  39. package/src/sitemap/types.ts +20 -0
  40. 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, '&amp;')
47
+ .replace(/</g, '&lt;')
48
+ .replace(/>/g, '&gt;')
49
+ .replace(/"/g, '&quot;')
50
+ .replace(/'/g, '&apos;');
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,8 @@
1
+ /**
2
+ * Sitemap exports
3
+ */
4
+
5
+ export { createSitemapHandler } from './route';
6
+ export { generateSitemapXml, normalizeUrl } from './generator';
7
+ export type { SitemapGeneratorOptions, SitemapRoute } from './types';
8
+
@@ -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
+