@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/nextjs",
3
- "version": "1.0.1",
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.31",
113
- "@djangocfg/layouts": "^2.0.1",
114
- "@djangocfg/api": "^1.4.31",
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.31",
119
- "@djangocfg/layouts": "^2.0.1",
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
- /** Base path for static builds (default: '/cfg/admin') */
40
- basePath?: string;
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; [key: string]: any }
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
- // Determine basePath: staticBuildPath takes precedence for static builds, then basePath, then default
106
- const basePath = isStaticBuild
107
- ? (options.staticBuildPath || options.basePath)
108
- : (options.basePath || '');
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: isStaticBuild ? '' : process.env.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; [key: string]: any }) => {
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
+
@@ -6,4 +6,3 @@
6
6
 
7
7
  export * from './types';
8
8
  export * from './utils';
9
-
@@ -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
- * 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
14
+ * Define a route with metadata
32
15
  *
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.
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
- // 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
-
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: fullPath,
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
- // In static builds, Next.js handles basePath automatically
65
- return manageBasePath(authPath, config);
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
- // In static builds, Next.js handles basePath automatically
78
- return manageBasePath(authPath, config);
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
  // ─────────────────────────────────────────────────────────────────────────
@@ -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
- const decodedData = parseOgImageData(paramsObj);
120
+ decodedParams = parseOgImageData(paramsObj);
119
121
 
120
122
  // Base64 data takes precedence - apply all decoded values
121
- if (decodedData.title && typeof decodedData.title === 'string' && decodedData.title.trim() !== '') {
122
- title = decodedData.title.trim();
123
+ if (decodedParams.title && typeof decodedParams.title === 'string' && decodedParams.title.trim() !== '') {
124
+ title = decodedParams.title.trim();
123
125
  }
124
- if (decodedData.subtitle && typeof decodedData.subtitle === 'string' && decodedData.subtitle.trim() !== '') {
125
- subtitle = decodedData.subtitle.trim();
126
+ if (decodedParams.subtitle && typeof decodedParams.subtitle === 'string' && decodedParams.subtitle.trim() !== '') {
127
+ subtitle = decodedParams.subtitle.trim();
126
128
  }
127
- if (decodedData.description && typeof decodedData.description === 'string' && decodedData.description.trim() !== '') {
128
- description = decodedData.description.trim();
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
- // Merge props
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
- return async function GET(
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 'http://localhost:3000';
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
- // Merge with existing metadata
165
- return {
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?data=eyJ0aXRsZSI6Ik15IFBhZ2UgVGl0bGUiLCJkZXNjcmlwdGlvbiI6IlBhZ2UgZGVzY3JpcHRpb24gaGVyZSJ9
122
+ * // Result: /api/og/[base64-encoded-json]
78
123
  *
79
124
  * // Query params (legacy)
80
125
  * const url = generateOgImageUrl('/api/og', { title: 'Hello' }, false);