@djangocfg/nextjs 1.0.1 → 1.0.3
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/package.json +6 -6
- package/src/components/HomePage.tsx +1 -1
- package/src/config/base-next-config.ts +20 -47
- package/src/config/deepMerge.ts +33 -0
- package/src/navigation/index.ts +0 -1
- package/src/navigation/utils.ts +16 -32
- package/src/og-image/route.tsx +66 -9
- package/src/og-image/utils/metadata.ts +21 -9
- package/src/og-image/utils/url.ts +47 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/nextjs",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Next.js utilities and components: sitemap, health, OG images, legal pages, error pages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nextjs",
|
|
@@ -109,14 +109,14 @@
|
|
|
109
109
|
"next": "^15.4.4",
|
|
110
110
|
"react": "^19.1.0",
|
|
111
111
|
"react-dom": "^19.1.0",
|
|
112
|
-
"@djangocfg/ui": "^1.4.
|
|
113
|
-
"@djangocfg/layouts": "^2.0.
|
|
114
|
-
"@djangocfg/api": "^1.4.
|
|
112
|
+
"@djangocfg/ui": "^1.4.33",
|
|
113
|
+
"@djangocfg/layouts": "^2.0.3",
|
|
114
|
+
"@djangocfg/api": "^1.4.33",
|
|
115
115
|
"lucide-react": "^0.469.0"
|
|
116
116
|
},
|
|
117
117
|
"devDependencies": {
|
|
118
|
-
"@djangocfg/typescript-config": "^1.4.
|
|
119
|
-
"@djangocfg/layouts": "^2.0.
|
|
118
|
+
"@djangocfg/typescript-config": "^1.4.33",
|
|
119
|
+
"@djangocfg/layouts": "^2.0.3",
|
|
120
120
|
"@types/node": "^24.7.2",
|
|
121
121
|
"@types/react": "19.2.2",
|
|
122
122
|
"@types/react-dom": "19.2.1",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useEffect } from 'react';
|
|
4
|
-
import { useRouter } from 'next/navigation';
|
|
5
4
|
import { useAuth } from '@djangocfg/layouts';
|
|
5
|
+
import { useRouter } from '@djangocfg/ui/hooks';
|
|
6
6
|
import { Loader2 } from 'lucide-react';
|
|
7
7
|
|
|
8
8
|
export interface HomePageProps {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import type { NextConfig } from 'next';
|
|
25
25
|
import type { Configuration as WebpackConfig } from 'webpack';
|
|
26
|
+
import { deepMerge } from './deepMerge';
|
|
26
27
|
|
|
27
28
|
// ─────────────────────────────────────────────────────────────────────────
|
|
28
29
|
// Standard Environment Variables
|
|
@@ -36,10 +37,8 @@ const isDev = process.env.NODE_ENV === 'development';
|
|
|
36
37
|
// ─────────────────────────────────────────────────────────────────────────
|
|
37
38
|
|
|
38
39
|
export interface BaseNextConfigOptions {
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
/** Static build path - used when NEXT_PUBLIC_STATIC_BUILD=true (overrides basePath for static builds) */
|
|
42
|
-
staticBuildPath?: string;
|
|
40
|
+
/** Static build path (used when NEXT_PUBLIC_STATIC_BUILD=true) */
|
|
41
|
+
isDefaultCfgAdmin?: boolean;
|
|
43
42
|
/** Additional transpile packages (merged with defaults) */
|
|
44
43
|
transpilePackages?: string[];
|
|
45
44
|
/** Additional optimize package imports (merged with defaults) */
|
|
@@ -47,7 +46,7 @@ export interface BaseNextConfigOptions {
|
|
|
47
46
|
/** Custom webpack configuration function (called after base webpack logic) */
|
|
48
47
|
webpack?: (
|
|
49
48
|
config: WebpackConfig,
|
|
50
|
-
options: { isServer: boolean; dev: boolean;
|
|
49
|
+
options: { isServer: boolean; dev: boolean;[key: string]: any }
|
|
51
50
|
) => WebpackConfig | void;
|
|
52
51
|
/** Custom experimental options (merged with defaults) */
|
|
53
52
|
experimental?: NextConfig['experimental'];
|
|
@@ -57,38 +56,6 @@ export interface BaseNextConfigOptions {
|
|
|
57
56
|
[key: string]: any;
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
61
|
-
// Deep Merge Helper (simple implementation)
|
|
62
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T {
|
|
65
|
-
const output = { ...target };
|
|
66
|
-
|
|
67
|
-
for (const key in source) {
|
|
68
|
-
if (source[key] === undefined) continue;
|
|
69
|
-
|
|
70
|
-
// Arrays: replace (don't merge arrays)
|
|
71
|
-
if (Array.isArray(source[key])) {
|
|
72
|
-
output[key] = source[key] as any;
|
|
73
|
-
}
|
|
74
|
-
// Objects: deep merge
|
|
75
|
-
else if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
76
|
-
const targetValue = output[key];
|
|
77
|
-
if (targetValue && typeof targetValue === 'object' && !Array.isArray(targetValue)) {
|
|
78
|
-
output[key] = deepMerge(targetValue, source[key] as any);
|
|
79
|
-
} else {
|
|
80
|
-
output[key] = source[key] as any;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
// Primitives: replace
|
|
84
|
-
else {
|
|
85
|
-
output[key] = source[key] as any;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return output;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
59
|
// ─────────────────────────────────────────────────────────────────────────
|
|
93
60
|
// Base Configuration Factory
|
|
94
61
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -102,10 +69,12 @@ function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>)
|
|
|
102
69
|
export function createBaseNextConfig(
|
|
103
70
|
options: BaseNextConfigOptions = {}
|
|
104
71
|
): NextConfig {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
72
|
+
|
|
73
|
+
// If static build not provided, use default for nextjs-admin development
|
|
74
|
+
const staticBuildPath = options.isDefaultCfgAdmin ? '/cfg/admin' : '/cfg/nextjs-admin';
|
|
75
|
+
const basePath = isStaticBuild ? staticBuildPath : '';
|
|
76
|
+
const apiUrl = isStaticBuild ? '' : process.env.NEXT_PUBLIC_API_URL || '';
|
|
77
|
+
const siteUrl = isStaticBuild ? '' : process.env.NEXT_PUBLIC_SITE_URL || '';
|
|
109
78
|
|
|
110
79
|
// Base configuration
|
|
111
80
|
const baseConfig: NextConfig = {
|
|
@@ -116,8 +85,8 @@ export function createBaseNextConfig(
|
|
|
116
85
|
...(isStaticBuild && {
|
|
117
86
|
output: 'export' as const,
|
|
118
87
|
distDir: 'out',
|
|
119
|
-
basePath,
|
|
120
|
-
assetPrefix: basePath,
|
|
88
|
+
basePath: basePath,
|
|
89
|
+
// assetPrefix: basePath,
|
|
121
90
|
// Fix for Next.js 15.5.4: prevent 500.html generation issue
|
|
122
91
|
//
|
|
123
92
|
// PROBLEM: In App Router, error.tsx is a client component ('use client')
|
|
@@ -139,7 +108,8 @@ export function createBaseNextConfig(
|
|
|
139
108
|
// Environment variables
|
|
140
109
|
env: {
|
|
141
110
|
NEXT_PUBLIC_BASE_PATH: basePath,
|
|
142
|
-
NEXT_PUBLIC_API_URL:
|
|
111
|
+
NEXT_PUBLIC_API_URL: apiUrl,
|
|
112
|
+
NEXT_PUBLIC_SITE_URL: siteUrl,
|
|
143
113
|
...options.env,
|
|
144
114
|
},
|
|
145
115
|
|
|
@@ -180,7 +150,7 @@ export function createBaseNextConfig(
|
|
|
180
150
|
},
|
|
181
151
|
|
|
182
152
|
// Webpack configuration
|
|
183
|
-
webpack: (config: WebpackConfig, webpackOptions: { isServer: boolean; dev: boolean;
|
|
153
|
+
webpack: (config: WebpackConfig, webpackOptions: { isServer: boolean; dev: boolean;[key: string]: any }) => {
|
|
184
154
|
const { isServer, dev } = webpackOptions;
|
|
185
155
|
|
|
186
156
|
// Dev mode optimizations
|
|
@@ -199,13 +169,17 @@ export function createBaseNextConfig(
|
|
|
199
169
|
buildDependencies: {},
|
|
200
170
|
};
|
|
201
171
|
|
|
172
|
+
// Note: Dynamic API routes should use `export const dynamic = 'force-dynamic'`
|
|
173
|
+
// in the route file itself to exclude them from static export.
|
|
174
|
+
// This is simpler than using webpack plugins.
|
|
175
|
+
|
|
202
176
|
// Compression plugins (only for static build in production)
|
|
203
177
|
// Note: compression-webpack-plugin should be installed in the consuming project
|
|
204
178
|
if (!isServer && isStaticBuild && !dev) {
|
|
205
179
|
try {
|
|
206
180
|
// Dynamic import to avoid bundling compression-webpack-plugin
|
|
207
181
|
const CompressionPlugin = require('compression-webpack-plugin');
|
|
208
|
-
|
|
182
|
+
|
|
209
183
|
// Gzip compression
|
|
210
184
|
config.plugins.push(
|
|
211
185
|
new CompressionPlugin({
|
|
@@ -251,7 +225,6 @@ export function createBaseNextConfig(
|
|
|
251
225
|
|
|
252
226
|
// Cleanup: Remove our custom options that are not part of NextConfig
|
|
253
227
|
// These are internal to BaseNextConfigOptions and should not be in the final config
|
|
254
|
-
delete (finalConfig as any).basePath;
|
|
255
228
|
delete (finalConfig as any).optimizePackageImports;
|
|
256
229
|
// Note: We don't delete transpilePackages, experimental, env, webpack
|
|
257
230
|
// as they are valid NextConfig keys and may have been overridden by user
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
|
|
2
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
3
|
+
// Deep Merge Helper (simple implementation)
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T {
|
|
7
|
+
const output = { ...target };
|
|
8
|
+
|
|
9
|
+
for (const key in source) {
|
|
10
|
+
if (source[key] === undefined) continue;
|
|
11
|
+
|
|
12
|
+
// Arrays: replace (don't merge arrays)
|
|
13
|
+
if (Array.isArray(source[key])) {
|
|
14
|
+
output[key] = source[key] as any;
|
|
15
|
+
}
|
|
16
|
+
// Objects: deep merge
|
|
17
|
+
else if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
18
|
+
const targetValue = output[key];
|
|
19
|
+
if (targetValue && typeof targetValue === 'object' && !Array.isArray(targetValue)) {
|
|
20
|
+
output[key] = deepMerge(targetValue, source[key] as any);
|
|
21
|
+
} else {
|
|
22
|
+
output[key] = source[key] as any;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Primitives: replace
|
|
26
|
+
else {
|
|
27
|
+
output[key] = source[key] as any;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return output;
|
|
32
|
+
}
|
|
33
|
+
|
package/src/navigation/index.ts
CHANGED
package/src/navigation/utils.ts
CHANGED
|
@@ -10,40 +10,24 @@ import type { RouteDefinition, RouteMetadata, MenuItem } from './types';
|
|
|
10
10
|
// Route Definition Helper
|
|
11
11
|
// ─────────────────────────────────────────────────────────────────────────
|
|
12
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
13
|
/**
|
|
21
|
-
*
|
|
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
|
|
14
|
+
* Define a route with metadata
|
|
32
15
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
16
|
+
* IMPORTANT: Next.js automatically handles basePath for <Link> components when
|
|
17
|
+
* basePath is set in next.config.ts. We should NOT add basePath manually here,
|
|
18
|
+
* as it would cause double-prefixing in static builds.
|
|
19
|
+
*
|
|
20
|
+
* @param path - Route path (e.g., '/dashboard', '/admin/users')
|
|
21
|
+
* @param metadata - Route metadata (label, icon, etc.)
|
|
35
22
|
*/
|
|
36
23
|
export function defineRoute(
|
|
37
24
|
path: string,
|
|
38
25
|
metadata: RouteMetadata,
|
|
39
|
-
config?: RouteConfig
|
|
40
26
|
): RouteDefinition {
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
const fullPath = manageBasePath(path, config);
|
|
44
|
-
|
|
27
|
+
// Always return path as-is. Next.js will handle basePath automatically
|
|
28
|
+
// for <Link> components when basePath is set in next.config.ts
|
|
45
29
|
return {
|
|
46
|
-
path
|
|
30
|
+
path,
|
|
47
31
|
metadata,
|
|
48
32
|
};
|
|
49
33
|
}
|
|
@@ -58,11 +42,11 @@ export function defineRoute(
|
|
|
58
42
|
export function getUnauthenticatedRedirect(
|
|
59
43
|
path: string,
|
|
60
44
|
authPath: string = '/auth',
|
|
61
|
-
config?: RouteConfig
|
|
62
45
|
): string | null {
|
|
63
46
|
if (path.startsWith('/private') || path.startsWith('/admin')) {
|
|
64
|
-
//
|
|
65
|
-
|
|
47
|
+
// Return path as-is. Next.js will handle basePath automatically for router.push()
|
|
48
|
+
// when basePath is set in next.config.ts
|
|
49
|
+
return authPath;
|
|
66
50
|
}
|
|
67
51
|
return null;
|
|
68
52
|
}
|
|
@@ -72,10 +56,10 @@ export function getUnauthenticatedRedirect(
|
|
|
72
56
|
*/
|
|
73
57
|
export function redirectToAuth(
|
|
74
58
|
authPath: string = '/auth',
|
|
75
|
-
config?: RouteConfig
|
|
76
59
|
): string {
|
|
77
|
-
//
|
|
78
|
-
|
|
60
|
+
// Return path as-is. Next.js will handle basePath automatically for router.push()
|
|
61
|
+
// when basePath is set in next.config.ts
|
|
62
|
+
return authPath;
|
|
79
63
|
}
|
|
80
64
|
|
|
81
65
|
// ─────────────────────────────────────────────────────────────────────────
|
package/src/og-image/route.tsx
CHANGED
|
@@ -105,7 +105,9 @@ export function createOgImageHandler(config: OgImageHandlerConfig) {
|
|
|
105
105
|
let description = defaultProps.description || subtitle;
|
|
106
106
|
|
|
107
107
|
// Support base64 data parameter (priority: base64 > query params > defaults)
|
|
108
|
+
// All template props can be encoded in base64, including styling params
|
|
108
109
|
const dataParam = searchParams.get('data');
|
|
110
|
+
let decodedParams: Record<string, any> = {};
|
|
109
111
|
|
|
110
112
|
if (dataParam) {
|
|
111
113
|
try {
|
|
@@ -115,17 +117,17 @@ export function createOgImageHandler(config: OgImageHandlerConfig) {
|
|
|
115
117
|
paramsObj[key] = value;
|
|
116
118
|
}
|
|
117
119
|
}
|
|
118
|
-
|
|
120
|
+
decodedParams = parseOgImageData(paramsObj);
|
|
119
121
|
|
|
120
122
|
// Base64 data takes precedence - apply all decoded values
|
|
121
|
-
if (
|
|
122
|
-
title =
|
|
123
|
+
if (decodedParams.title && typeof decodedParams.title === 'string' && decodedParams.title.trim() !== '') {
|
|
124
|
+
title = decodedParams.title.trim();
|
|
123
125
|
}
|
|
124
|
-
if (
|
|
125
|
-
subtitle =
|
|
126
|
+
if (decodedParams.subtitle && typeof decodedParams.subtitle === 'string' && decodedParams.subtitle.trim() !== '') {
|
|
127
|
+
subtitle = decodedParams.subtitle.trim();
|
|
126
128
|
}
|
|
127
|
-
if (
|
|
128
|
-
description =
|
|
129
|
+
if (decodedParams.description && typeof decodedParams.description === 'string' && decodedParams.description.trim() !== '') {
|
|
130
|
+
description = decodedParams.description.trim();
|
|
129
131
|
}
|
|
130
132
|
} catch (error) {
|
|
131
133
|
// Silently fall back to defaults
|
|
@@ -158,12 +160,56 @@ export function createOgImageHandler(config: OgImageHandlerConfig) {
|
|
|
158
160
|
fonts = await loadGoogleFonts(fontConfig);
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
//
|
|
163
|
+
// Helper function to parse numeric/boolean values from decoded params
|
|
164
|
+
const parseValue = (value: any, type: 'number' | 'boolean' | 'string' = 'string'): any => {
|
|
165
|
+
if (value === undefined || value === null || value === '') {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
if (type === 'number') {
|
|
169
|
+
const num = Number(value);
|
|
170
|
+
return isNaN(num) ? undefined : num;
|
|
171
|
+
}
|
|
172
|
+
if (type === 'boolean') {
|
|
173
|
+
if (typeof value === 'boolean') return value;
|
|
174
|
+
if (typeof value === 'string') {
|
|
175
|
+
return value.toLowerCase() === 'true' || value === '1';
|
|
176
|
+
}
|
|
177
|
+
return Boolean(value);
|
|
178
|
+
}
|
|
179
|
+
return String(value);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Merge props: decoded params from URL override defaultProps
|
|
162
183
|
const templateProps: OgImageTemplateProps = {
|
|
163
184
|
...defaultProps,
|
|
185
|
+
// Content
|
|
164
186
|
title,
|
|
165
187
|
subtitle,
|
|
166
188
|
description,
|
|
189
|
+
// Override with decoded params if present
|
|
190
|
+
siteName: decodedParams.siteName || defaultProps.siteName,
|
|
191
|
+
logo: decodedParams.logo || defaultProps.logo,
|
|
192
|
+
// Background
|
|
193
|
+
backgroundType: (decodedParams.backgroundType as 'gradient' | 'solid') || defaultProps.backgroundType,
|
|
194
|
+
gradientStart: decodedParams.gradientStart || defaultProps.gradientStart,
|
|
195
|
+
gradientEnd: decodedParams.gradientEnd || defaultProps.gradientEnd,
|
|
196
|
+
backgroundColor: decodedParams.backgroundColor || defaultProps.backgroundColor,
|
|
197
|
+
// Typography - Title
|
|
198
|
+
titleSize: parseValue(decodedParams.titleSize, 'number') ?? defaultProps.titleSize,
|
|
199
|
+
titleWeight: parseValue(decodedParams.titleWeight, 'number') ?? defaultProps.titleWeight,
|
|
200
|
+
titleColor: decodedParams.titleColor || defaultProps.titleColor,
|
|
201
|
+
// Typography - Description
|
|
202
|
+
descriptionSize: parseValue(decodedParams.descriptionSize, 'number') ?? defaultProps.descriptionSize,
|
|
203
|
+
descriptionColor: decodedParams.descriptionColor || defaultProps.descriptionColor,
|
|
204
|
+
// Typography - Site Name
|
|
205
|
+
siteNameSize: parseValue(decodedParams.siteNameSize, 'number') ?? defaultProps.siteNameSize,
|
|
206
|
+
siteNameColor: decodedParams.siteNameColor || defaultProps.siteNameColor,
|
|
207
|
+
// Layout
|
|
208
|
+
padding: parseValue(decodedParams.padding, 'number') ?? defaultProps.padding,
|
|
209
|
+
logoSize: parseValue(decodedParams.logoSize, 'number') ?? defaultProps.logoSize,
|
|
210
|
+
// Visibility flags
|
|
211
|
+
showLogo: parseValue(decodedParams.showLogo, 'boolean') ?? defaultProps.showLogo,
|
|
212
|
+
showSiteName: parseValue(decodedParams.showSiteName, 'boolean') ?? defaultProps.showSiteName,
|
|
167
213
|
};
|
|
168
214
|
|
|
169
215
|
|
|
@@ -221,7 +267,7 @@ export function createOgImageDynamicRoute(config: OgImageHandlerConfig) {
|
|
|
221
267
|
const handler = createOgImageHandler(config);
|
|
222
268
|
const isStaticBuild = typeof process !== 'undefined' && process.env.NEXT_PUBLIC_STATIC_BUILD === 'true';
|
|
223
269
|
|
|
224
|
-
|
|
270
|
+
async function GET(
|
|
225
271
|
request: NextRequest,
|
|
226
272
|
context: { params: Promise<{ data: string }> }
|
|
227
273
|
) {
|
|
@@ -249,5 +295,16 @@ export function createOgImageDynamicRoute(config: OgImageHandlerConfig) {
|
|
|
249
295
|
});
|
|
250
296
|
|
|
251
297
|
return handler.GET(modifiedRequest);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// For static export, provide generateStaticParams that returns empty array
|
|
301
|
+
// This allows the route to be excluded from static build
|
|
302
|
+
async function generateStaticParams(): Promise<Array<{ data: string }>> {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
GET,
|
|
308
|
+
generateStaticParams,
|
|
252
309
|
};
|
|
253
310
|
}
|
|
@@ -93,13 +93,8 @@ function getSiteUrl(): string {
|
|
|
93
93
|
return process.env.NEXT_PUBLIC_SITE_URL;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
// Fallback to VERCEL_URL if available
|
|
97
|
-
if (typeof process !== 'undefined' && process.env.VERCEL_URL) {
|
|
98
|
-
return `https://${process.env.VERCEL_URL}`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
96
|
// Development fallback
|
|
102
|
-
return '
|
|
97
|
+
return '';
|
|
103
98
|
}
|
|
104
99
|
|
|
105
100
|
export function generateOgImageMetadata(
|
|
@@ -108,7 +103,7 @@ export function generateOgImageMetadata(
|
|
|
108
103
|
options: OgImageMetadataOptions = {}
|
|
109
104
|
): Metadata {
|
|
110
105
|
const {
|
|
111
|
-
ogImageBaseUrl = '/api/og',
|
|
106
|
+
ogImageBaseUrl = 'https://djangocfg.com/api/og',
|
|
112
107
|
siteUrl: providedSiteUrl,
|
|
113
108
|
defaultParams = {},
|
|
114
109
|
useBase64 = true,
|
|
@@ -161,8 +156,8 @@ export function generateOgImageMetadata(
|
|
|
161
156
|
: [metadata.twitter.images]
|
|
162
157
|
: [];
|
|
163
158
|
|
|
164
|
-
//
|
|
165
|
-
|
|
159
|
+
// Build final metadata object
|
|
160
|
+
const finalMetadata: Metadata = {
|
|
166
161
|
...metadata,
|
|
167
162
|
openGraph: {
|
|
168
163
|
...metadata.openGraph,
|
|
@@ -188,6 +183,23 @@ export function generateOgImageMetadata(
|
|
|
188
183
|
],
|
|
189
184
|
},
|
|
190
185
|
};
|
|
186
|
+
|
|
187
|
+
// Automatically add metadataBase if siteUrl is an absolute URL
|
|
188
|
+
// metadataBase requires absolute URL - skip if siteUrl is relative path (like /cfg/admin)
|
|
189
|
+
// Only add if not already set in input metadata
|
|
190
|
+
if (!finalMetadata.metadataBase && siteUrl) {
|
|
191
|
+
// Check if siteUrl is an absolute URL (starts with http:// or https://)
|
|
192
|
+
if (siteUrl.startsWith('http://') || siteUrl.startsWith('https://')) {
|
|
193
|
+
try {
|
|
194
|
+
finalMetadata.metadataBase = new URL(siteUrl);
|
|
195
|
+
} catch (e) {
|
|
196
|
+
// If URL construction fails, skip metadataBase
|
|
197
|
+
// This shouldn't happen if we check for http/https, but just in case
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return finalMetadata;
|
|
191
203
|
}
|
|
192
204
|
|
|
193
205
|
/**
|
|
@@ -45,6 +45,7 @@ function decodeBase64(str: string): string {
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* OG Image URL parameters
|
|
48
|
+
* All parameters can be encoded in URL via base64
|
|
48
49
|
*/
|
|
49
50
|
export interface OgImageUrlParams {
|
|
50
51
|
/** Page title */
|
|
@@ -55,6 +56,36 @@ export interface OgImageUrlParams {
|
|
|
55
56
|
siteName?: string;
|
|
56
57
|
/** Logo URL (optional) */
|
|
57
58
|
logo?: string;
|
|
59
|
+
/** Background type: 'gradient' or 'solid' */
|
|
60
|
+
backgroundType?: 'gradient' | 'solid';
|
|
61
|
+
/** Gradient start color (hex) */
|
|
62
|
+
gradientStart?: string;
|
|
63
|
+
/** Gradient end color (hex) */
|
|
64
|
+
gradientEnd?: string;
|
|
65
|
+
/** Background color (for solid type) */
|
|
66
|
+
backgroundColor?: string;
|
|
67
|
+
/** Title font size (px) */
|
|
68
|
+
titleSize?: number;
|
|
69
|
+
/** Title font weight */
|
|
70
|
+
titleWeight?: number;
|
|
71
|
+
/** Title text color */
|
|
72
|
+
titleColor?: string;
|
|
73
|
+
/** Description font size (px) */
|
|
74
|
+
descriptionSize?: number;
|
|
75
|
+
/** Description text color */
|
|
76
|
+
descriptionColor?: string;
|
|
77
|
+
/** Site name font size (px) */
|
|
78
|
+
siteNameSize?: number;
|
|
79
|
+
/** Site name text color */
|
|
80
|
+
siteNameColor?: string;
|
|
81
|
+
/** Padding (px) */
|
|
82
|
+
padding?: number;
|
|
83
|
+
/** Logo size (px) */
|
|
84
|
+
logoSize?: number;
|
|
85
|
+
/** Show logo flag */
|
|
86
|
+
showLogo?: boolean;
|
|
87
|
+
/** Show site name flag */
|
|
88
|
+
showSiteName?: boolean;
|
|
58
89
|
/** Additional custom parameters */
|
|
59
90
|
[key: string]: string | number | boolean | undefined;
|
|
60
91
|
}
|
|
@@ -69,12 +100,26 @@ export interface OgImageUrlParams {
|
|
|
69
100
|
*
|
|
70
101
|
* @example
|
|
71
102
|
* ```typescript
|
|
72
|
-
* // Base64 encoding (safe, default)
|
|
103
|
+
* // Base64 encoding (safe, default) - all parameters can be encoded
|
|
73
104
|
* const url = generateOgImageUrl('/api/og', {
|
|
74
105
|
* title: 'My Page Title',
|
|
75
106
|
* description: 'Page description here',
|
|
107
|
+
* siteName: 'My Site',
|
|
108
|
+
* logo: '/logo.svg',
|
|
109
|
+
* backgroundType: 'gradient',
|
|
110
|
+
* gradientStart: '#0f172a',
|
|
111
|
+
* gradientEnd: '#334155',
|
|
112
|
+
* titleSize: 80,
|
|
113
|
+
* titleWeight: 800,
|
|
114
|
+
* titleColor: 'white',
|
|
115
|
+
* descriptionSize: 36,
|
|
116
|
+
* descriptionColor: 'rgba(226, 232, 240, 0.9)',
|
|
117
|
+
* siteNameSize: 32,
|
|
118
|
+
* siteNameColor: 'rgba(255, 255, 255, 0.95)',
|
|
119
|
+
* padding: 80,
|
|
120
|
+
* logoSize: 56,
|
|
76
121
|
* });
|
|
77
|
-
* // Result: /api/og
|
|
122
|
+
* // Result: /api/og/[base64-encoded-json]
|
|
78
123
|
*
|
|
79
124
|
* // Query params (legacy)
|
|
80
125
|
* const url = generateOgImageUrl('/api/og', { title: 'Hello' }, false);
|