@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,253 @@
1
+ /**
2
+ * OG Image Route Handler
3
+ *
4
+ * Factory function to create OG Image route handler for Next.js App Router
5
+ *
6
+ * Usage:
7
+ * ```tsx
8
+ * // app/api/og/route.tsx
9
+ * import { createOgImageHandler } from '@djangocfg/nextjs/og-image';
10
+ * import { MyTemplate } from './templates';
11
+ *
12
+ * export const { GET, runtime } = createOgImageHandler({
13
+ * template: MyTemplate,
14
+ * defaultProps: {
15
+ * siteName: 'My Site',
16
+ * },
17
+ * fonts: [{ family: 'Manrope', weight: 700 }],
18
+ * });
19
+ * ```
20
+ */
21
+
22
+ import { ImageResponse } from 'next/og';
23
+ import { NextRequest } from 'next/server';
24
+ import type { ReactElement } from 'react';
25
+ import { loadGoogleFonts } from './utils';
26
+ import { parseOgImageData } from './utils/url';
27
+ import { DefaultTemplate } from './components/DefaultTemplate';
28
+ import type { OgImageTemplateProps } from './types';
29
+
30
+ export interface OgImageHandlerConfig {
31
+ /** Custom template component (optional, defaults to DefaultTemplate) */
32
+ template?: (props: OgImageTemplateProps) => ReactElement;
33
+ /** Default props to merge with query params */
34
+ defaultProps?: Partial<OgImageTemplateProps>;
35
+ /** Google Fonts to load */
36
+ fonts?: Array<{ family: string; weight: 400 | 500 | 600 | 700 | 800 | 900 }>;
37
+ /** Image size */
38
+ size?: { width: number; height: number };
39
+ /** Enable debug mode */
40
+ debug?: boolean;
41
+ }
42
+
43
+ /**
44
+ * Factory function to create OG Image route handler
45
+ *
46
+ * @example
47
+ * ```tsx
48
+ * // app/api/og/route.tsx
49
+ * import { createOgImageHandler } from '@djangocfg/nextjs/og-image';
50
+ * import { MyTemplate } from '@/components/MyTemplate';
51
+ *
52
+ * export const { GET, runtime } = createOgImageHandler({
53
+ * template: MyTemplate,
54
+ * defaultProps: { siteName: 'My Site' },
55
+ * fonts: [{ family: 'Inter', weight: 700 }],
56
+ * });
57
+ * ```
58
+ */
59
+ export function createOgImageHandler(config: OgImageHandlerConfig) {
60
+ const {
61
+ template: Template = DefaultTemplate,
62
+ defaultProps = {},
63
+ fonts: fontConfig = [],
64
+ size = { width: 1200, height: 630 },
65
+ debug = false,
66
+ } = config;
67
+
68
+ async function GET(req: NextRequest) {
69
+ let searchParams: URLSearchParams = new URLSearchParams();
70
+
71
+ // Try to get searchParams from multiple sources
72
+ if (req.nextUrl?.searchParams && req.nextUrl.searchParams.size > 0) {
73
+ searchParams = req.nextUrl.searchParams;
74
+ } else if (req.nextUrl?.search && req.nextUrl.search.length > 1) {
75
+ searchParams = new URLSearchParams(req.nextUrl.search);
76
+ } else {
77
+ try {
78
+ const url = new URL(req.url);
79
+ if (url.searchParams.size > 0) {
80
+ searchParams = url.searchParams;
81
+ }
82
+ } catch (error) {
83
+ // Ignore
84
+ }
85
+
86
+ if (searchParams.size === 0 && req.url) {
87
+ const queryIndex = req.url.indexOf('?');
88
+ if (queryIndex !== -1) {
89
+ const queryString = req.url.substring(queryIndex + 1);
90
+ searchParams = new URLSearchParams(queryString);
91
+ }
92
+ }
93
+
94
+ if (searchParams.size === 0) {
95
+ const customParams = req.headers.get('x-og-search-params');
96
+ if (customParams) {
97
+ searchParams = new URLSearchParams(customParams);
98
+ }
99
+ }
100
+ }
101
+
102
+ // Initialize with defaults
103
+ let title = defaultProps.title || 'Untitled';
104
+ let subtitle = defaultProps.subtitle || '';
105
+ let description = defaultProps.description || subtitle;
106
+
107
+ // Support base64 data parameter (priority: base64 > query params > defaults)
108
+ const dataParam = searchParams.get('data');
109
+
110
+ if (dataParam) {
111
+ try {
112
+ const paramsObj: Record<string, string> = { data: dataParam };
113
+ for (const [key, value] of searchParams.entries()) {
114
+ if (key !== 'data') {
115
+ paramsObj[key] = value;
116
+ }
117
+ }
118
+ const decodedData = parseOgImageData(paramsObj);
119
+
120
+ // 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
+ }
124
+ if (decodedData.subtitle && typeof decodedData.subtitle === 'string' && decodedData.subtitle.trim() !== '') {
125
+ subtitle = decodedData.subtitle.trim();
126
+ }
127
+ if (decodedData.description && typeof decodedData.description === 'string' && decodedData.description.trim() !== '') {
128
+ description = decodedData.description.trim();
129
+ }
130
+ } catch (error) {
131
+ // Silently fall back to defaults
132
+ }
133
+ }
134
+
135
+ // Fallback to query params if not set from base64
136
+ if (!title || title === 'Untitled') {
137
+ const titleParam = searchParams.get('title');
138
+ if (titleParam) {
139
+ title = titleParam;
140
+ }
141
+ }
142
+ if (!subtitle) {
143
+ const subtitleParam = searchParams.get('subtitle');
144
+ if (subtitleParam) {
145
+ subtitle = subtitleParam;
146
+ }
147
+ }
148
+ if (!description || description === subtitle) {
149
+ const descParam = searchParams.get('description');
150
+ if (descParam) {
151
+ description = descParam;
152
+ }
153
+ }
154
+
155
+ // Load fonts if configured
156
+ let fonts: any[] = [];
157
+ if (fontConfig.length > 0) {
158
+ fonts = await loadGoogleFonts(fontConfig);
159
+ }
160
+
161
+ // Merge props
162
+ const templateProps: OgImageTemplateProps = {
163
+ ...defaultProps,
164
+ title,
165
+ subtitle,
166
+ description,
167
+ };
168
+
169
+
170
+ return new ImageResponse(
171
+ <Template {...templateProps} />,
172
+ {
173
+ width: size.width,
174
+ height: size.height,
175
+ fonts,
176
+ debug: debug || process.env.NODE_ENV === 'development',
177
+ }
178
+ );
179
+ }
180
+
181
+ return {
182
+ GET,
183
+ runtime: 'edge' as const,
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Create OG Image route handler for dynamic route with path parameter
189
+ *
190
+ * This is a convenience wrapper for Next.js dynamic routes like `/api/og/[data]/route.tsx`.
191
+ * It extracts the `data` parameter from the path and passes it to the handler as a query parameter.
192
+ * Also handles static export mode automatically.
193
+ *
194
+ * @example
195
+ * ```tsx
196
+ * // app/api/og/[data]/route.tsx
197
+ * import { createOgImageDynamicRoute } from '@djangocfg/nextjs/og-image';
198
+ * import { OgImageTemplate } from '@/components/OgImageTemplate';
199
+ *
200
+ * export const runtime = 'nodejs';
201
+ * export const dynamic = 'force-static';
202
+ * export const revalidate = false;
203
+ *
204
+ * const handler = createOgImageDynamicRoute({
205
+ * template: OgImageTemplate,
206
+ * defaultProps: {
207
+ * siteName: 'My App',
208
+ * logo: '/logo.svg',
209
+ * },
210
+ * });
211
+ *
212
+ * export async function GET(
213
+ * request: NextRequest,
214
+ * { params }: { params: { data: string } }
215
+ ) {
216
+ * return handler(request, params);
217
+ * }
218
+ * ```
219
+ */
220
+ export function createOgImageDynamicRoute(config: OgImageHandlerConfig) {
221
+ const handler = createOgImageHandler(config);
222
+ const isStaticBuild = typeof process !== 'undefined' && process.env.NEXT_PUBLIC_STATIC_BUILD === 'true';
223
+
224
+ return async function GET(
225
+ request: NextRequest,
226
+ context: { params: Promise<{ data: string }> }
227
+ ) {
228
+ // In static export mode, return a simple error response
229
+ if (isStaticBuild) {
230
+ return new Response('OG Image generation is not available in static export mode', {
231
+ status: 404,
232
+ headers: { 'Content-Type': 'text/plain' },
233
+ });
234
+ }
235
+
236
+ // Await params (Next.js 15+ uses Promise)
237
+ const params = await context.params;
238
+
239
+ // Extract data from path parameter
240
+ const dataParam = params.data;
241
+
242
+ // Create a request with the data parameter as a query param for the handler
243
+ const url = new URL(request.url);
244
+ url.searchParams.set('data', dataParam);
245
+
246
+ const modifiedRequest = new NextRequest(url.toString(), {
247
+ method: request.method,
248
+ headers: request.headers,
249
+ });
250
+
251
+ return handler.GET(modifiedRequest);
252
+ };
253
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * OG Image Types
3
+ *
4
+ * Shared types for OG image templates and handlers
5
+ * This file is safe to import in client components
6
+ */
7
+
8
+ export interface OgImageTemplateProps {
9
+ // Content
10
+ title: string;
11
+ description?: string;
12
+ subtitle?: string;
13
+ siteName?: string;
14
+ logo?: string;
15
+
16
+ // Visibility flags
17
+ showLogo?: boolean;
18
+ showSiteName?: boolean;
19
+
20
+ // Background customization
21
+ backgroundType?: 'gradient' | 'solid';
22
+ gradientStart?: string;
23
+ gradientEnd?: string;
24
+ backgroundColor?: string;
25
+
26
+ // Typography - Title
27
+ titleSize?: number;
28
+ titleWeight?: number;
29
+ titleColor?: string;
30
+
31
+ // Typography - Description
32
+ descriptionSize?: number;
33
+ descriptionColor?: string;
34
+
35
+ // Typography - Site Name
36
+ siteNameSize?: number;
37
+ siteNameColor?: string;
38
+
39
+ // Layout
40
+ padding?: number;
41
+ logoSize?: number;
42
+
43
+ // Dev mode (for debugging)
44
+ devMode?: boolean;
45
+ }
46
+
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Font Utilities for OG Image Generation
3
+ *
4
+ * Provides dynamic font loading from Google Fonts without requiring files in public/
5
+ * Based on Vercel's official @vercel/og documentation
6
+ */
7
+
8
+ export interface FontConfig {
9
+ name: string;
10
+ weight?: 400 | 500 | 600 | 700 | 800 | 900;
11
+ style?: 'normal' | 'italic';
12
+ data: ArrayBuffer;
13
+ }
14
+
15
+ /**
16
+ * Load a Google Font dynamically
17
+ *
18
+ * @param font - Font family name (e.g., "Inter", "Roboto", "Manrope")
19
+ * @param text - Text to optimize font for (optional, reduces file size)
20
+ * @param weight - Font weight (default: 700)
21
+ * @returns ArrayBuffer of font data
22
+ *
23
+ * @example
24
+ * const fontData = await loadGoogleFont('Manrope', 'Hello World', 700);
25
+ */
26
+ export async function loadGoogleFont(
27
+ font: string,
28
+ text?: string,
29
+ weight: number = 700
30
+ ): Promise<ArrayBuffer> {
31
+ // Construct Google Fonts API URL
32
+ let url = `https://fonts.googleapis.com/css2?family=${font}:wght@${weight}`;
33
+
34
+ // Add text parameter to optimize font subset (reduces size)
35
+ if (text) {
36
+ url += `&text=${encodeURIComponent(text)}`;
37
+ }
38
+
39
+ try {
40
+ // Fetch CSS containing font URL
41
+ const css = await fetch(url, {
42
+ headers: {
43
+ // Required to get TTF format instead of WOFF2
44
+ 'User-Agent':
45
+ 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1',
46
+ },
47
+ }).then((res) => res.text());
48
+
49
+ // Extract font URL from CSS
50
+ const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/);
51
+
52
+ if (!resource || !resource[1]) {
53
+ throw new Error(`Failed to parse font URL from CSS for font: ${font}`);
54
+ }
55
+
56
+ // Fetch actual font file
57
+ const response = await fetch(resource[1]);
58
+
59
+ if (response.status !== 200) {
60
+ throw new Error(`Failed to fetch font data: HTTP ${response.status}`);
61
+ }
62
+
63
+ return await response.arrayBuffer();
64
+ } catch (error) {
65
+ console.error(`Error loading Google Font "${font}":`, error);
66
+ throw new Error(`Failed to load font "${font}": ${error instanceof Error ? error.message : 'Unknown error'}`);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Load multiple Google Fonts
72
+ *
73
+ * @param fonts - Array of font configurations to load
74
+ * @returns Array of FontConfig objects ready for ImageResponse
75
+ *
76
+ * @example
77
+ * const fonts = await loadGoogleFonts([
78
+ * { family: 'Manrope', weight: 700 },
79
+ * { family: 'Inter', weight: 400 }
80
+ * ]);
81
+ */
82
+ export async function loadGoogleFonts(
83
+ fonts: Array<{
84
+ family: string;
85
+ weight?: 400 | 500 | 600 | 700 | 800 | 900;
86
+ style?: 'normal' | 'italic';
87
+ text?: string;
88
+ }>
89
+ ): Promise<FontConfig[]> {
90
+ const fontConfigs = await Promise.all(
91
+ fonts.map(async ({ family, weight = 700, style = 'normal', text }) => {
92
+ const data = await loadGoogleFont(family, text, weight);
93
+ return {
94
+ name: family,
95
+ weight,
96
+ style,
97
+ data,
98
+ };
99
+ })
100
+ );
101
+
102
+ return fontConfigs;
103
+ }
104
+
105
+ /**
106
+ * Create a font loader with caching
107
+ *
108
+ * Useful for reusing font data across multiple OG image requests
109
+ *
110
+ * @example
111
+ * const fontLoader = createFontLoader();
112
+ * const font = await fontLoader.load('Manrope', 700);
113
+ */
114
+ export function createFontLoader() {
115
+ const cache = new Map<string, Promise<ArrayBuffer>>();
116
+
117
+ return {
118
+ /**
119
+ * Load a font with caching
120
+ */
121
+ async load(
122
+ family: string,
123
+ weight: number = 700,
124
+ text?: string
125
+ ): Promise<ArrayBuffer> {
126
+ const cacheKey = `${family}-${weight}-${text || 'all'}`;
127
+
128
+ if (!cache.has(cacheKey)) {
129
+ cache.set(cacheKey, loadGoogleFont(family, text, weight));
130
+ }
131
+
132
+ return cache.get(cacheKey)!;
133
+ },
134
+
135
+ /**
136
+ * Clear the cache
137
+ */
138
+ clear() {
139
+ cache.clear();
140
+ },
141
+
142
+ /**
143
+ * Get cache size
144
+ */
145
+ size() {
146
+ return cache.size;
147
+ },
148
+ };
149
+ }
150
+
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Utilities for OG Image Generation
3
+ */
4
+
5
+ export {
6
+ loadGoogleFont,
7
+ loadGoogleFonts,
8
+ createFontLoader,
9
+ type FontConfig,
10
+ } from './fonts';
11
+
12
+ export {
13
+ generateOgImageUrl,
14
+ getAbsoluteOgImageUrl,
15
+ createOgImageUrlBuilder,
16
+ parseOgImageUrl,
17
+ parseOgImageData,
18
+ encodeBase64,
19
+ decodeBase64,
20
+ type OgImageUrlParams,
21
+ } from './url';
22
+
23
+ export {
24
+ generateOgImageMetadata,
25
+ createOgImageMetadataGenerator,
26
+ type OgImageMetadataOptions,
27
+ } from './metadata';
28
+
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Metadata Utilities for OG Images
3
+ *
4
+ * Helpers to automatically add og:image to Next.js metadata
5
+ */
6
+
7
+ import type { Metadata } from 'next';
8
+ import { generateOgImageUrl, getAbsoluteOgImageUrl, type OgImageUrlParams } from './url';
9
+
10
+ /**
11
+ * Options for generating OG image metadata
12
+ */
13
+ export interface OgImageMetadataOptions {
14
+ /** Base URL of the OG image API route (e.g., '/api/og') */
15
+ ogImageBaseUrl?: string;
16
+ /** Site URL for absolute URLs (e.g., 'https://example.com') */
17
+ siteUrl?: string;
18
+ /** Default parameters to merge with page-specific params */
19
+ defaultParams?: Partial<OgImageUrlParams>;
20
+ /** Whether to use base64 encoding (default: true) */
21
+ useBase64?: boolean;
22
+ }
23
+
24
+ /**
25
+ * Extract title from metadata
26
+ */
27
+ function extractTitle(metadata: Metadata): string {
28
+ if (typeof metadata.title === 'string') {
29
+ return metadata.title;
30
+ }
31
+ if (metadata.title) {
32
+ if ('default' in metadata.title) {
33
+ return metadata.title.default;
34
+ }
35
+ if ('absolute' in metadata.title) {
36
+ return metadata.title.absolute;
37
+ }
38
+ }
39
+ return '';
40
+ }
41
+
42
+ /**
43
+ * Extract description from metadata
44
+ */
45
+ function extractDescription(metadata: Metadata): string {
46
+ if (typeof metadata.description === 'string') {
47
+ return metadata.description;
48
+ }
49
+ return '';
50
+ }
51
+
52
+ /**
53
+ * Generate Next.js metadata with OG image
54
+ *
55
+ * Automatically adds og:image, twitter:image, and other OG meta tags
56
+ * Automatically extracts title and description from metadata if not provided
57
+ *
58
+ * @param metadata - Base metadata object
59
+ * @param ogImageParams - Optional parameters for OG image generation (if not provided, extracted from metadata)
60
+ * @param options - Configuration options
61
+ * @returns Enhanced metadata with OG image
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * // In page.tsx or layout.tsx
66
+ * import { generateOgImageMetadata } from '@djangocfg/nextjs/og-image';
67
+ * import { settings } from '@/core/settings';
68
+ *
69
+ * export const metadata = generateOgImageMetadata(
70
+ * {
71
+ * title: 'My Page',
72
+ * description: 'Page description',
73
+ * },
74
+ * undefined, // Will auto-extract from metadata
75
+ * {
76
+ * ogImageBaseUrl: '/api/og',
77
+ * siteUrl: settings.app.siteUrl,
78
+ * defaultParams: {
79
+ * siteName: settings.app.name,
80
+ * logo: settings.app.icons.logoVector,
81
+ * },
82
+ * }
83
+ * );
84
+ * ```
85
+ */
86
+ /**
87
+ * Get site URL automatically from environment
88
+ * Priority: NEXT_PUBLIC_SITE_URL > VERCEL_URL > fallback
89
+ */
90
+ function getSiteUrl(): string {
91
+ // Try NEXT_PUBLIC_SITE_URL first (most reliable)
92
+ if (typeof process !== 'undefined' && process.env.NEXT_PUBLIC_SITE_URL) {
93
+ return process.env.NEXT_PUBLIC_SITE_URL;
94
+ }
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
+ // Development fallback
102
+ return 'http://localhost:3000';
103
+ }
104
+
105
+ export function generateOgImageMetadata(
106
+ metadata: Metadata,
107
+ ogImageParams?: Partial<OgImageUrlParams>,
108
+ options: OgImageMetadataOptions = {}
109
+ ): Metadata {
110
+ const {
111
+ ogImageBaseUrl = '/api/og',
112
+ siteUrl: providedSiteUrl,
113
+ defaultParams = {},
114
+ useBase64 = true,
115
+ } = options;
116
+
117
+ // Automatically determine siteUrl if not provided or is undefined
118
+ const siteUrl = providedSiteUrl && providedSiteUrl !== 'undefined'
119
+ ? providedSiteUrl
120
+ : getSiteUrl();
121
+
122
+ // Auto-extract title and description from metadata if not provided
123
+ const extractedTitle = extractTitle(metadata);
124
+ const extractedDescription = extractDescription(metadata);
125
+
126
+ // Merge with provided params (provided params take precedence)
127
+ const finalOgImageParams: OgImageUrlParams = {
128
+ ...defaultParams,
129
+ title: ogImageParams?.title || extractedTitle || defaultParams.title || '',
130
+ description: ogImageParams?.description || extractedDescription || defaultParams.description || '',
131
+ ...ogImageParams,
132
+ };
133
+
134
+ // Get alt text for image (title or siteName as fallback)
135
+ const imageAlt = finalOgImageParams.title || finalOgImageParams.siteName;
136
+
137
+ // Generate relative OG image URL
138
+ const relativeOgImageUrl = generateOgImageUrl(
139
+ ogImageBaseUrl,
140
+ finalOgImageParams,
141
+ useBase64
142
+ );
143
+
144
+ // CRITICAL: Use absolute URL to ensure query params are preserved
145
+ // Next.js might strip query params from relative URLs in some cases
146
+ // Absolute URLs ensure the full URL with params is used
147
+ const ogImageUrl = siteUrl
148
+ ? getAbsoluteOgImageUrl(relativeOgImageUrl, siteUrl)
149
+ : relativeOgImageUrl;
150
+
151
+ // Normalize existing images to arrays
152
+ const existingOgImages = metadata.openGraph?.images
153
+ ? Array.isArray(metadata.openGraph.images)
154
+ ? metadata.openGraph.images
155
+ : [metadata.openGraph.images]
156
+ : [];
157
+
158
+ const existingTwitterImages = metadata.twitter?.images
159
+ ? Array.isArray(metadata.twitter.images)
160
+ ? metadata.twitter.images
161
+ : [metadata.twitter.images]
162
+ : [];
163
+
164
+ // Merge with existing metadata
165
+ return {
166
+ ...metadata,
167
+ openGraph: {
168
+ ...metadata.openGraph,
169
+ images: [
170
+ ...existingOgImages,
171
+ {
172
+ url: ogImageUrl,
173
+ width: 1200,
174
+ height: 630,
175
+ alt: imageAlt,
176
+ },
177
+ ],
178
+ },
179
+ twitter: {
180
+ ...metadata.twitter,
181
+ card: 'summary_large_image',
182
+ images: [
183
+ ...existingTwitterImages,
184
+ {
185
+ url: ogImageUrl,
186
+ alt: imageAlt,
187
+ },
188
+ ],
189
+ },
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Create OG image metadata generator with preset configuration
195
+ *
196
+ * Useful when you want to reuse the same configuration across multiple pages
197
+ *
198
+ * @param options - Configuration options
199
+ * @returns Metadata generator function
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * // In a shared file (e.g., lib/metadata.ts)
204
+ * import { createOgImageMetadataGenerator } from '@djangocfg/nextjs/og-image';
205
+ * import { settings } from '@/core/settings';
206
+ *
207
+ * export const generateMetadata = createOgImageMetadataGenerator({
208
+ * ogImageBaseUrl: '/api/og',
209
+ * siteUrl: settings.app.siteUrl,
210
+ * defaultParams: {
211
+ * siteName: settings.app.name,
212
+ * logo: settings.app.icons.logoVector,
213
+ * },
214
+ * });
215
+ *
216
+ * // In page.tsx
217
+ * import { generateMetadata } from '@/lib/metadata';
218
+ *
219
+ * export const metadata = generateMetadata({
220
+ * title: 'My Page',
221
+ * description: 'Description',
222
+ * });
223
+ * ```
224
+ */
225
+ export function createOgImageMetadataGenerator(
226
+ options: OgImageMetadataOptions
227
+ ) {
228
+ return (
229
+ metadata: Metadata,
230
+ ogImageParams?: Partial<OgImageUrlParams>
231
+ ): Metadata => {
232
+ return generateOgImageMetadata(metadata, ogImageParams, options);
233
+ };
234
+ }
235
+