@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,181 @@
1
+ /**
2
+ * Navigation Utilities
3
+ *
4
+ * Common utilities for route definitions and navigation
5
+ */
6
+
7
+ import type { RouteDefinition, RouteMetadata, MenuItem } from './types';
8
+
9
+ // ─────────────────────────────────────────────────────────────────────────
10
+ // Route Definition Helper
11
+ // ─────────────────────────────────────────────────────────────────────────
12
+
13
+ export interface RouteConfig {
14
+ /** Base path for static builds (optional) */
15
+ basePath?: string;
16
+ /** Whether this is a static build (optional, defaults to false) */
17
+ isStaticBuild?: boolean;
18
+ }
19
+
20
+ /**
21
+ * Manage base path for routes
22
+ */
23
+ function manageBasePath(path: string, config?: RouteConfig): string {
24
+ if (!config?.isStaticBuild || !config?.basePath) {
25
+ return path;
26
+ }
27
+ return `${config.basePath}${path}`;
28
+ }
29
+
30
+ /**
31
+ * Define a route with automatic basePath injection
32
+ *
33
+ * NOTE: For static builds, Next.js automatically prepends basePath to <Link> hrefs,
34
+ * so we don't add it here. For dev mode, we need to add it manually.
35
+ */
36
+ export function defineRoute(
37
+ path: string,
38
+ metadata: RouteMetadata,
39
+ config?: RouteConfig
40
+ ): RouteDefinition {
41
+ // In static builds, Next.js handles basePath automatically
42
+ // In dev mode, we need to add it manually for consistency
43
+ const fullPath = manageBasePath(path, config);
44
+
45
+ return {
46
+ path: fullPath,
47
+ metadata,
48
+ };
49
+ }
50
+
51
+ // ─────────────────────────────────────────────────────────────────────────
52
+ // Route Guards
53
+ // ─────────────────────────────────────────────────────────────────────────
54
+
55
+ /**
56
+ * Get redirect path for unauthenticated users
57
+ */
58
+ export function getUnauthenticatedRedirect(
59
+ path: string,
60
+ authPath: string = '/auth',
61
+ config?: RouteConfig
62
+ ): string | null {
63
+ if (path.startsWith('/private') || path.startsWith('/admin')) {
64
+ // In static builds, Next.js handles basePath automatically
65
+ return manageBasePath(authPath, config);
66
+ }
67
+ return null;
68
+ }
69
+
70
+ /**
71
+ * Get redirect path to auth page
72
+ */
73
+ export function redirectToAuth(
74
+ authPath: string = '/auth',
75
+ config?: RouteConfig
76
+ ): string {
77
+ // In static builds, Next.js handles basePath automatically
78
+ return manageBasePath(authPath, config);
79
+ }
80
+
81
+ // ─────────────────────────────────────────────────────────────────────────
82
+ // Route Lookup
83
+ // ─────────────────────────────────────────────────────────────────────────
84
+
85
+ export function findRoute(
86
+ routes: RouteDefinition[],
87
+ path: string
88
+ ): RouteDefinition | undefined {
89
+ return routes.find((r) => r.path === path);
90
+ }
91
+
92
+ export function findRouteByPattern(
93
+ routes: RouteDefinition[],
94
+ path: string
95
+ ): RouteDefinition | undefined {
96
+ const exact = findRoute(routes, path);
97
+ if (exact) return exact;
98
+
99
+ const segments = path.split('/').filter(Boolean);
100
+ for (let i = segments.length; i > 0; i--) {
101
+ const parentPath = '/' + segments.slice(0, i).join('/');
102
+ const parent = findRoute(routes, parentPath);
103
+ if (parent) return parent;
104
+ }
105
+
106
+ return undefined;
107
+ }
108
+
109
+ // ─────────────────────────────────────────────────────────────────────────
110
+ // Page Title
111
+ // ─────────────────────────────────────────────────────────────────────────
112
+
113
+ export function getPageTitle(
114
+ routes: RouteDefinition[],
115
+ path: string,
116
+ fallback = 'Dashboard'
117
+ ): string {
118
+ const route = findRouteByPattern(routes, path);
119
+ return route?.metadata.label || fallback;
120
+ }
121
+
122
+ // ─────────────────────────────────────────────────────────────────────────
123
+ // Active Route
124
+ // ─────────────────────────────────────────────────────────────────────────
125
+
126
+ /**
127
+ * Check if a route is active
128
+ *
129
+ * @param current - Current pathname
130
+ * @param target - Target route path to check
131
+ * @param allRoutes - Optional array of all routes to prevent parent paths from being active when child paths are active
132
+ * @returns true if the route is active
133
+ */
134
+ export function isActive(
135
+ current: string,
136
+ target: string,
137
+ allRoutes?: RouteDefinition[]
138
+ ): boolean {
139
+ const matches =
140
+ current === target || (target !== '/' && current.startsWith(target + '/'));
141
+
142
+ // If allRoutes is provided, check for more specific paths
143
+ if (matches && allRoutes) {
144
+ return !allRoutes.some(
145
+ (otherRoute) =>
146
+ otherRoute.path !== target &&
147
+ otherRoute.path.startsWith(target + '/') &&
148
+ (current === otherRoute.path ||
149
+ current.startsWith(otherRoute.path + '/'))
150
+ );
151
+ }
152
+
153
+ return matches;
154
+ }
155
+
156
+ // ─────────────────────────────────────────────────────────────────────────
157
+ // Menu Generation Helper
158
+ // ─────────────────────────────────────────────────────────────────────────
159
+
160
+ /**
161
+ * Filter and convert routes to menu items
162
+ */
163
+ export function routesToMenuItems(
164
+ routes: RouteDefinition[],
165
+ groupName: string
166
+ ): MenuItem[] {
167
+ return routes
168
+ .filter(
169
+ (r) =>
170
+ r.metadata.group === groupName &&
171
+ r.metadata.icon &&
172
+ (r.metadata.show === undefined || r.metadata.show === true)
173
+ )
174
+ .sort((a, b) => (a.metadata.order || 0) - (b.metadata.order || 0))
175
+ .map((r) => ({
176
+ path: r.path,
177
+ label: r.metadata.label,
178
+ icon: r.metadata.icon!,
179
+ }));
180
+ }
181
+
@@ -0,0 +1,66 @@
1
+ # OG Image Metadata Helper
2
+
3
+ Automatically add `og:image` to Next.js metadata.
4
+
5
+ ## Usage
6
+
7
+ ### In layout.tsx or page.tsx
8
+
9
+ ```typescript
10
+ import type { Metadata } from 'next';
11
+ import { generateOgImageMetadata } from '@djangocfg/nextjs/og-image';
12
+ import { settings } from '@core/settings';
13
+
14
+ export const metadata: Metadata = generateOgImageMetadata(
15
+ {
16
+ title: 'My Page',
17
+ description: 'Page description',
18
+ },
19
+ {
20
+ title: 'My Page',
21
+ description: 'Page description',
22
+ },
23
+ {
24
+ ogImageBaseUrl: '/api/og',
25
+ siteUrl: settings.app.siteUrl,
26
+ defaultParams: {
27
+ siteName: settings.app.name,
28
+ logo: settings.app.icons.logoVector,
29
+ },
30
+ }
31
+ );
32
+ ```
33
+
34
+ ### With Generator (Recommended)
35
+
36
+ ```typescript
37
+ // lib/metadata.ts
38
+ import { createOgImageMetadataGenerator } from '@djangocfg/nextjs/og-image';
39
+ import { settings } from '@core/settings';
40
+
41
+ export const generateMetadata = createOgImageMetadataGenerator({
42
+ ogImageBaseUrl: '/api/og',
43
+ siteUrl: settings.app.siteUrl,
44
+ defaultParams: {
45
+ siteName: settings.app.name,
46
+ logo: settings.app.icons.logoVector,
47
+ },
48
+ });
49
+
50
+ // page.tsx
51
+ import { generateMetadata } from '@/lib/metadata';
52
+
53
+ export const metadata = generateMetadata(
54
+ { title: 'My Page', description: 'Description' },
55
+ { title: 'My Page', description: 'Description' }
56
+ );
57
+ ```
58
+
59
+ ## What It Does
60
+
61
+ Automatically adds to HTML:
62
+ - `<meta property="og:image" content="...">`
63
+ - `<meta name="twitter:image" content="...">`
64
+ - `<meta name="twitter:card" content="summary_large_image">`
65
+
66
+ The URL is generated based on parameters and automatically added to metadata.
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Default OG Image Template
3
+ *
4
+ * A modern, gradient-based template for OG images
5
+ */
6
+
7
+ import type { ReactElement } from 'react';
8
+ import type { OgImageTemplateProps } from '../types';
9
+
10
+ /**
11
+ * Default OG Image Template Component
12
+ *
13
+ * Features:
14
+ * - Modern gradient background
15
+ * - Responsive text sizing
16
+ * - Optional logo and site name
17
+ * - Clean typography
18
+ * - Full customization support
19
+ *
20
+ * @param props - Template props with title, description, siteName, logo and optional customization
21
+ */
22
+ export function DefaultTemplate({
23
+ title,
24
+ description,
25
+ siteName,
26
+ logo,
27
+ // Visibility flags
28
+ showLogo = true,
29
+ showSiteName = true,
30
+ // Background customization
31
+ backgroundType = 'gradient',
32
+ gradientStart = '#667eea',
33
+ gradientEnd = '#764ba2',
34
+ backgroundColor = '#ffffff',
35
+ // Typography - Title
36
+ titleSize,
37
+ titleWeight = 800,
38
+ titleColor = 'white',
39
+ // Typography - Description
40
+ descriptionSize = 32,
41
+ descriptionColor = 'rgba(255, 255, 255, 0.85)',
42
+ // Typography - Site Name
43
+ siteNameSize = 28,
44
+ siteNameColor = 'rgba(255, 255, 255, 0.95)',
45
+ // Layout
46
+ padding = 80,
47
+ logoSize = 48,
48
+ // Dev mode
49
+ devMode = false,
50
+ }: OgImageTemplateProps): ReactElement {
51
+ // Calculate title size if not provided (responsive based on title length)
52
+ const calculatedTitleSize = titleSize || (title.length > 60 ? 56 : 72);
53
+
54
+ // Determine background style
55
+ const backgroundStyle =
56
+ backgroundType === 'gradient'
57
+ ? `linear-gradient(135deg, ${gradientStart} 0%, ${gradientEnd} 100%)`
58
+ : backgroundColor;
59
+
60
+ // Grid overlay for dev mode
61
+ const gridOverlay = devMode ? (
62
+ <div
63
+ style={{
64
+ position: 'absolute',
65
+ top: 0,
66
+ left: 0,
67
+ right: 0,
68
+ bottom: 0,
69
+ backgroundImage: `
70
+ linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px),
71
+ linear-gradient(90deg, rgba(0, 0, 0, 0.1) 1px, transparent 1px)
72
+ `,
73
+ backgroundSize: '20px 20px',
74
+ pointerEvents: 'none',
75
+ zIndex: 10,
76
+ }}
77
+ />
78
+ ) : null;
79
+
80
+ return (
81
+ <div
82
+ style={{
83
+ height: '100%',
84
+ width: '100%',
85
+ display: 'flex',
86
+ flexDirection: 'column',
87
+ alignItems: 'flex-start',
88
+ justifyContent: 'space-between',
89
+ background: backgroundStyle,
90
+ padding: `${padding}px`,
91
+ fontFamily: 'system-ui, -apple-system, sans-serif',
92
+ position: 'relative',
93
+ }}
94
+ >
95
+ {gridOverlay}
96
+
97
+ {/* Header with logo and site name */}
98
+ {((showLogo && logo) || (showSiteName && siteName)) && (
99
+ <div
100
+ style={{
101
+ display: 'flex',
102
+ alignItems: 'center',
103
+ gap: '16px',
104
+ }}
105
+ >
106
+ {showLogo && logo && (
107
+ // eslint-disable-next-line @next/next/no-img-element
108
+ <img
109
+ src={logo}
110
+ alt="Logo"
111
+ width={logoSize}
112
+ height={logoSize}
113
+ style={{
114
+ borderRadius: '8px',
115
+ }}
116
+ />
117
+ )}
118
+ {showSiteName && siteName && (
119
+ <div
120
+ style={{
121
+ fontSize: siteNameSize,
122
+ fontWeight: 600,
123
+ color: siteNameColor,
124
+ letterSpacing: '-0.02em',
125
+ }}
126
+ >
127
+ {siteName}
128
+ </div>
129
+ )}
130
+ </div>
131
+ )}
132
+
133
+ {/* Main content */}
134
+ <div
135
+ style={{
136
+ display: 'flex',
137
+ flexDirection: 'column',
138
+ gap: '24px',
139
+ flex: 1,
140
+ justifyContent: 'center',
141
+ }}
142
+ >
143
+ {/* Title */}
144
+ <div
145
+ style={{
146
+ fontSize: calculatedTitleSize,
147
+ fontWeight: titleWeight,
148
+ color: titleColor,
149
+ lineHeight: 1.1,
150
+ letterSpacing: '-0.03em',
151
+ textShadow: backgroundType === 'gradient' ? '0 2px 20px rgba(0, 0, 0, 0.2)' : 'none',
152
+ maxWidth: '100%',
153
+ wordWrap: 'break-word',
154
+ }}
155
+ >
156
+ {title}
157
+ </div>
158
+
159
+ {/* Description */}
160
+ {description && (
161
+ <div
162
+ style={{
163
+ fontSize: descriptionSize,
164
+ fontWeight: 400,
165
+ color: descriptionColor,
166
+ lineHeight: 1.5,
167
+ letterSpacing: '-0.01em',
168
+ maxWidth: '90%',
169
+ display: '-webkit-box',
170
+ WebkitLineClamp: 2,
171
+ WebkitBoxOrient: 'vertical',
172
+ overflow: 'hidden',
173
+ }}
174
+ >
175
+ {description}
176
+ </div>
177
+ )}
178
+ </div>
179
+
180
+ {/* Footer decoration */}
181
+ <div
182
+ style={{
183
+ display: 'flex',
184
+ width: '100%',
185
+ height: '4px',
186
+ background: backgroundType === 'gradient'
187
+ ? `linear-gradient(90deg, ${gradientStart} 0%, ${gradientEnd} 100%)`
188
+ : gradientStart,
189
+ borderRadius: '2px',
190
+ }}
191
+ />
192
+ </div>
193
+ );
194
+ }
195
+
196
+ /**
197
+ * Simple light template variant
198
+ *
199
+ * Light background variant with dark text
200
+ */
201
+ export function LightTemplate({
202
+ title,
203
+ description,
204
+ siteName,
205
+ logo,
206
+ // Visibility flags
207
+ showLogo = true,
208
+ showSiteName = true,
209
+ // Background customization (defaults to light theme)
210
+ backgroundType = 'solid',
211
+ gradientStart = '#667eea',
212
+ gradientEnd = '#764ba2',
213
+ backgroundColor = '#ffffff',
214
+ // Typography - Title
215
+ titleSize,
216
+ titleWeight = 800,
217
+ titleColor = '#111',
218
+ // Typography - Description
219
+ descriptionSize = 32,
220
+ descriptionColor = '#666',
221
+ // Typography - Site Name
222
+ siteNameSize = 28,
223
+ siteNameColor = '#111',
224
+ // Layout
225
+ padding = 80,
226
+ logoSize = 48,
227
+ // Dev mode
228
+ devMode = false,
229
+ }: OgImageTemplateProps): ReactElement {
230
+ // Calculate title size if not provided (responsive based on title length)
231
+ const calculatedTitleSize = titleSize || (title.length > 60 ? 56 : 72);
232
+
233
+ // Determine background style
234
+ const backgroundStyle =
235
+ backgroundType === 'gradient'
236
+ ? `linear-gradient(135deg, ${gradientStart} 0%, ${gradientEnd} 100%)`
237
+ : backgroundColor;
238
+
239
+ // Grid overlay for dev mode
240
+ const gridOverlay = devMode ? (
241
+ <div
242
+ style={{
243
+ position: 'absolute',
244
+ top: 0,
245
+ left: 0,
246
+ right: 0,
247
+ bottom: 0,
248
+ backgroundImage: `
249
+ linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px),
250
+ linear-gradient(90deg, rgba(0, 0, 0, 0.1) 1px, transparent 1px)
251
+ `,
252
+ backgroundSize: '20px 20px',
253
+ pointerEvents: 'none',
254
+ zIndex: 10,
255
+ }}
256
+ />
257
+ ) : null;
258
+
259
+ return (
260
+ <div
261
+ style={{
262
+ height: '100%',
263
+ width: '100%',
264
+ display: 'flex',
265
+ flexDirection: 'column',
266
+ alignItems: 'flex-start',
267
+ justifyContent: 'space-between',
268
+ background: backgroundStyle,
269
+ padding: `${padding}px`,
270
+ fontFamily: 'system-ui, -apple-system, sans-serif',
271
+ position: 'relative',
272
+ }}
273
+ >
274
+ {gridOverlay}
275
+
276
+ {/* Header with logo and site name */}
277
+ {((showLogo && logo) || (showSiteName && siteName)) && (
278
+ <div
279
+ style={{
280
+ display: 'flex',
281
+ alignItems: 'center',
282
+ gap: '16px',
283
+ }}
284
+ >
285
+ {showLogo && logo && (
286
+ // eslint-disable-next-line @next/next/no-img-element
287
+ <img
288
+ src={logo}
289
+ alt="Logo"
290
+ width={logoSize}
291
+ height={logoSize}
292
+ style={{
293
+ borderRadius: '8px',
294
+ }}
295
+ />
296
+ )}
297
+ {showSiteName && siteName && (
298
+ <div
299
+ style={{
300
+ fontSize: siteNameSize,
301
+ fontWeight: 600,
302
+ color: siteNameColor,
303
+ letterSpacing: '-0.02em',
304
+ }}
305
+ >
306
+ {siteName}
307
+ </div>
308
+ )}
309
+ </div>
310
+ )}
311
+
312
+ {/* Main content */}
313
+ <div
314
+ style={{
315
+ display: 'flex',
316
+ flexDirection: 'column',
317
+ gap: '24px',
318
+ flex: 1,
319
+ justifyContent: 'center',
320
+ }}
321
+ >
322
+ {/* Title */}
323
+ <div
324
+ style={{
325
+ fontSize: calculatedTitleSize,
326
+ fontWeight: titleWeight,
327
+ color: titleColor,
328
+ lineHeight: 1.1,
329
+ letterSpacing: '-0.03em',
330
+ maxWidth: '100%',
331
+ wordWrap: 'break-word',
332
+ }}
333
+ >
334
+ {title}
335
+ </div>
336
+
337
+ {/* Description */}
338
+ {description && (
339
+ <div
340
+ style={{
341
+ fontSize: descriptionSize,
342
+ fontWeight: 400,
343
+ color: descriptionColor,
344
+ lineHeight: 1.5,
345
+ letterSpacing: '-0.01em',
346
+ maxWidth: '90%',
347
+ }}
348
+ >
349
+ {description}
350
+ </div>
351
+ )}
352
+ </div>
353
+
354
+ {/* Footer decoration */}
355
+ <div
356
+ style={{
357
+ display: 'flex',
358
+ width: '100%',
359
+ height: '4px',
360
+ background: backgroundType === 'gradient'
361
+ ? `linear-gradient(90deg, ${gradientStart} 0%, ${gradientEnd} 100%)`
362
+ : gradientStart,
363
+ borderRadius: '2px',
364
+ }}
365
+ />
366
+ </div>
367
+ );
368
+ }
369
+
@@ -0,0 +1,9 @@
1
+ /**
2
+ * OG Image Template Components
3
+ *
4
+ * Client-safe exports (no server dependencies)
5
+ */
6
+
7
+ export { DefaultTemplate, LightTemplate } from './DefaultTemplate';
8
+ export type { OgImageTemplateProps } from '../types';
9
+
@@ -0,0 +1,27 @@
1
+ /**
2
+ * OG Image exports
3
+ */
4
+
5
+ export { createOgImageHandler, createOgImageDynamicRoute } from './route';
6
+ export type { OgImageHandlerConfig } from './route';
7
+ export type { OgImageTemplateProps } from './types';
8
+
9
+ export { DefaultTemplate, LightTemplate } from './components';
10
+
11
+ export {
12
+ loadGoogleFont,
13
+ loadGoogleFonts,
14
+ createFontLoader,
15
+ generateOgImageUrl,
16
+ getAbsoluteOgImageUrl,
17
+ createOgImageUrlBuilder,
18
+ parseOgImageUrl,
19
+ parseOgImageData,
20
+ encodeBase64,
21
+ decodeBase64,
22
+ generateOgImageMetadata,
23
+ createOgImageMetadataGenerator,
24
+ type FontConfig,
25
+ type OgImageUrlParams,
26
+ type OgImageMetadataOptions,
27
+ } from './utils';