@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,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,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';
|