@djangocfg/nextjs 2.1.224 → 2.1.226

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.
@@ -1,269 +0,0 @@
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, OgImageUrlParams } from './url';
9
-
10
- /**
11
- * Options for generating OG image metadata
12
- */
13
- export interface AppMetadataOptions {
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
- /** Favicon URL (e.g., '/favicon.png') - automatically added to metadata.icons */
23
- favicon?: string;
24
- /** Apple touch icon URL (e.g., '/apple-icon.png') - automatically added to metadata.icons */
25
- appleIcon?: string;
26
- }
27
-
28
- /**
29
- * Extract title from metadata
30
- */
31
- function extractTitle(metadata: Metadata): string {
32
- if (typeof metadata.title === 'string') {
33
- return metadata.title;
34
- }
35
- if (metadata.title) {
36
- if ('default' in metadata.title) {
37
- return metadata.title.default;
38
- }
39
- if ('absolute' in metadata.title) {
40
- return metadata.title.absolute;
41
- }
42
- }
43
- return '';
44
- }
45
-
46
- /**
47
- * Extract description from metadata
48
- */
49
- function extractDescription(metadata: Metadata): string {
50
- if (typeof metadata.description === 'string') {
51
- return metadata.description;
52
- }
53
- return '';
54
- }
55
-
56
- /**
57
- * Generate Next.js metadata with OG image
58
- *
59
- * Automatically adds og:image, twitter:image, and other OG meta tags
60
- * Automatically extracts title and description from metadata if not provided
61
- *
62
- * @param metadata - Base metadata object
63
- * @param ogImageParams - Optional parameters for OG image generation (if not provided, extracted from metadata)
64
- * @param options - Configuration options
65
- * @returns Enhanced metadata with OG image
66
- *
67
- * @example
68
- * ```typescript
69
- * // In page.tsx or layout.tsx
70
- * import { generateAppMetadata } from '@djangocfg/nextjs/og-image';
71
- * import { settings } from '@/core/settings';
72
- *
73
- * export const metadata = generateAppMetadata(
74
- * {
75
- * title: 'My Page',
76
- * description: 'Page description',
77
- * },
78
- * undefined, // Will auto-extract from metadata
79
- * {
80
- * ogImageBaseUrl: '/api/og',
81
- * siteUrl: settings.app.siteUrl,
82
- * favicon: settings.app.icons.favicon,
83
- * appleIcon: settings.app.icons.logo192,
84
- * defaultParams: {
85
- * siteName: settings.app.name,
86
- * logo: settings.app.icons.logoVector,
87
- * },
88
- * }
89
- * );
90
- * ```
91
- */
92
- /**
93
- * Get site URL automatically from environment
94
- * Priority: NEXT_PUBLIC_SITE_URL > VERCEL_URL > fallback
95
- */
96
- function getSiteUrl(): string {
97
- // Try NEXT_PUBLIC_SITE_URL first (most reliable)
98
- if (typeof process !== 'undefined' && process.env.NEXT_PUBLIC_SITE_URL) {
99
- return process.env.NEXT_PUBLIC_SITE_URL;
100
- }
101
-
102
- // Development fallback
103
- return '';
104
- }
105
-
106
- export function generateAppMetadata(
107
- metadata: Metadata,
108
- ogImageParams?: Partial<OgImageUrlParams>,
109
- options: AppMetadataOptions = {}
110
- ): Metadata {
111
- const {
112
- ogImageBaseUrl = 'https://djangocfg.com/api/og',
113
- siteUrl: providedSiteUrl,
114
- defaultParams = {},
115
- useBase64 = true,
116
- favicon,
117
- appleIcon,
118
- } = options;
119
-
120
- // Automatically determine siteUrl if not provided or is undefined
121
- const siteUrl = providedSiteUrl && providedSiteUrl !== 'undefined'
122
- ? providedSiteUrl
123
- : getSiteUrl();
124
-
125
- // Auto-extract title and description from metadata if not provided
126
- const extractedTitle = extractTitle(metadata);
127
- const extractedDescription = extractDescription(metadata);
128
-
129
- // Merge with provided params (provided params take precedence)
130
- const finalOgImageParams: OgImageUrlParams = {
131
- ...defaultParams,
132
- title: ogImageParams?.title || extractedTitle || defaultParams.title || '',
133
- description: ogImageParams?.description || extractedDescription || defaultParams.description || '',
134
- ...ogImageParams,
135
- };
136
-
137
- // Get alt text for image (title or siteName as fallback)
138
- const imageAlt = finalOgImageParams.title || finalOgImageParams.siteName;
139
-
140
- // Generate relative OG image URL
141
- const relativeOgImageUrl = generateOgImageUrl(
142
- finalOgImageParams,
143
- { baseUrl: ogImageBaseUrl, useBase64 }
144
- );
145
-
146
- // CRITICAL: Use absolute URL to ensure query params are preserved
147
- // Next.js might strip query params from relative URLs in some cases
148
- // Absolute URLs ensure the full URL with params is used
149
- const ogImageUrl = siteUrl
150
- ? getAbsoluteOgImageUrl(relativeOgImageUrl, siteUrl)
151
- : relativeOgImageUrl;
152
-
153
- // Normalize existing images to arrays
154
- const existingOgImages = metadata.openGraph?.images
155
- ? Array.isArray(metadata.openGraph.images)
156
- ? metadata.openGraph.images
157
- : [metadata.openGraph.images]
158
- : [];
159
-
160
- const existingTwitterImages = metadata.twitter?.images
161
- ? Array.isArray(metadata.twitter.images)
162
- ? metadata.twitter.images
163
- : [metadata.twitter.images]
164
- : [];
165
-
166
- // Build final metadata object
167
- const finalMetadata: Metadata = {
168
- ...metadata,
169
- openGraph: {
170
- ...metadata.openGraph,
171
- images: [
172
- ...existingOgImages,
173
- {
174
- url: ogImageUrl,
175
- width: 1200,
176
- height: 630,
177
- alt: imageAlt,
178
- },
179
- ],
180
- },
181
- twitter: {
182
- ...metadata.twitter,
183
- card: 'summary_large_image',
184
- images: [
185
- ...existingTwitterImages,
186
- {
187
- url: ogImageUrl,
188
- alt: imageAlt,
189
- },
190
- ],
191
- },
192
- };
193
-
194
- // Automatically add metadataBase if siteUrl is an absolute URL
195
- // metadataBase requires absolute URL - skip if siteUrl is relative path (like /cfg/admin)
196
- // Only add if not already set in input metadata
197
- if (!finalMetadata.metadataBase && siteUrl) {
198
- // Check if siteUrl is an absolute URL (starts with http:// or https://)
199
- if (siteUrl.startsWith('http://') || siteUrl.startsWith('https://')) {
200
- try {
201
- finalMetadata.metadataBase = new URL(siteUrl);
202
- } catch (e) {
203
- // If URL construction fails, skip metadataBase
204
- // This shouldn't happen if we check for http/https, but just in case
205
- }
206
- }
207
- }
208
-
209
- // Add favicon and apple icon if provided
210
- if (favicon || appleIcon) {
211
- // metadata.icons can be string, array, or object - only spread if it's an object
212
- const existingIcons = metadata.icons && typeof metadata.icons === 'object' && !Array.isArray(metadata.icons)
213
- ? metadata.icons
214
- : {};
215
- finalMetadata.icons = {
216
- ...existingIcons,
217
- ...(favicon && { icon: favicon }),
218
- ...(appleIcon && { apple: appleIcon }),
219
- };
220
- }
221
-
222
- return finalMetadata;
223
- }
224
-
225
- /**
226
- * Create OG image metadata generator with preset configuration
227
- *
228
- * Useful when you want to reuse the same configuration across multiple pages
229
- *
230
- * @param options - Configuration options
231
- * @returns Metadata generator function
232
- *
233
- * @example
234
- * ```typescript
235
- * // In a shared file (e.g., lib/metadata.ts)
236
- * import { createAppMetadataGenerator } from '@djangocfg/nextjs/og-image';
237
- * import { settings } from '@/core/settings';
238
- *
239
- * export const generateMetadata = createAppMetadataGenerator({
240
- * ogImageBaseUrl: '/api/og',
241
- * siteUrl: settings.app.siteUrl,
242
- * favicon: settings.app.icons.favicon,
243
- * appleIcon: settings.app.icons.logo192,
244
- * defaultParams: {
245
- * siteName: settings.app.name,
246
- * logo: settings.app.icons.logoVector,
247
- * },
248
- * });
249
- *
250
- * // In page.tsx
251
- * import { generateMetadata } from '@/lib/metadata';
252
- *
253
- * export const metadata = generateMetadata({
254
- * title: 'My Page',
255
- * description: 'Description',
256
- * });
257
- * ```
258
- */
259
- export function createAppMetadataGenerator(
260
- options: AppMetadataOptions
261
- ) {
262
- return (
263
- metadata: Metadata,
264
- ogImageParams?: Partial<OgImageUrlParams>
265
- ): Metadata => {
266
- return generateAppMetadata(metadata, ogImageParams, options);
267
- };
268
- }
269
-
@@ -1,386 +0,0 @@
1
- /**
2
- * URL Generation Helpers for OG Images
3
- *
4
- * Utilities to generate OG image URLs with proper query parameters
5
- */
6
-
7
- /** Default OG Image API base URL */
8
- const DEFAULT_OG_IMAGE_BASE_URL = 'https://djangocfg.com/api/og';
9
-
10
- /**
11
- * Encode string to base64 with Unicode support
12
- * Works in both browser and Node.js environments
13
- */
14
- function encodeBase64(str: string): string {
15
- // Node.js environment
16
- if (typeof Buffer !== 'undefined') {
17
- return Buffer.from(str, 'utf-8').toString('base64');
18
- }
19
- // Browser environment - handle Unicode via UTF-8 encoding
20
- return btoa(unescape(encodeURIComponent(str)));
21
- }
22
-
23
- /**
24
- * Decode base64 string with Unicode support
25
- * Works in both browser, Node.js, and Edge Runtime environments
26
- */
27
- function decodeBase64(str: string): string {
28
- // Node.js environment
29
- if (typeof Buffer !== 'undefined') {
30
- return Buffer.from(str, 'base64').toString('utf-8');
31
- }
32
- // Edge Runtime / Browser environment - handle Unicode via UTF-8 decoding
33
- // atob is available in Edge Runtime
34
- try {
35
- const binaryString = atob(str);
36
- // Convert binary string to UTF-8
37
- return decodeURIComponent(
38
- binaryString
39
- .split('')
40
- .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
41
- .join('')
42
- );
43
- } catch (error) {
44
- // Fallback to simpler method if above fails
45
- return decodeURIComponent(escape(atob(str)));
46
- }
47
- }
48
-
49
- /**
50
- * OG Image URL parameters
51
- * All parameters can be encoded in URL via base64
52
- */
53
- export interface OgImageUrlParams {
54
- /** Page title */
55
- title: string;
56
- /** Page description (optional) */
57
- description?: string;
58
- /** Site name (optional) */
59
- siteName?: string;
60
- /** Logo URL (optional) */
61
- logo?: string;
62
- /** Background type: 'gradient' or 'solid' */
63
- backgroundType?: 'gradient' | 'solid';
64
- /** Gradient start color (hex) */
65
- gradientStart?: string;
66
- /** Gradient end color (hex) */
67
- gradientEnd?: string;
68
- /** Background color (for solid type) */
69
- backgroundColor?: string;
70
- /** Title font size (px) */
71
- titleSize?: number;
72
- /** Title font weight */
73
- titleWeight?: number;
74
- /** Title text color */
75
- titleColor?: string;
76
- /** Description font size (px) */
77
- descriptionSize?: number;
78
- /** Description text color */
79
- descriptionColor?: string;
80
- /** Site name font size (px) */
81
- siteNameSize?: number;
82
- /** Site name text color */
83
- siteNameColor?: string;
84
- /** Padding (px) */
85
- padding?: number;
86
- /** Logo size (px) */
87
- logoSize?: number;
88
- /** Show logo flag */
89
- showLogo?: boolean;
90
- /** Show site name flag */
91
- showSiteName?: boolean;
92
- /** Additional custom parameters */
93
- [key: string]: string | number | boolean | undefined;
94
- }
95
-
96
- /**
97
- * Options for generating OG image URL
98
- */
99
- export interface GenerateOgImageUrlOptions {
100
- /**
101
- * Base URL of the OG image API route
102
- * @default 'https://djangocfg.com/api/og'
103
- */
104
- baseUrl?: string;
105
- /**
106
- * If true, encode params as base64 for safer URLs
107
- * @default true
108
- */
109
- useBase64?: boolean;
110
- }
111
-
112
- /**
113
- * Generate OG image URL with query parameters or base64 encoding
114
- *
115
- * @param params - URL parameters for the OG image
116
- * @param options - Generation options (baseUrl, useBase64)
117
- * @returns Complete OG image URL with encoded parameters
118
- *
119
- * @example
120
- * ```typescript
121
- * // Using default baseUrl (https://djangocfg.com/api/og)
122
- * const url = generateOgImageUrl({
123
- * title: 'My Page Title',
124
- * description: 'Page description here',
125
- * });
126
- *
127
- * // With custom baseUrl
128
- * const url = generateOgImageUrl(
129
- * { title: 'My Page' },
130
- * { baseUrl: '/api/og' }
131
- * );
132
- * ```
133
- */
134
- export function generateOgImageUrl(
135
- params: OgImageUrlParams,
136
- options: GenerateOgImageUrlOptions = {}
137
- ): string {
138
- const {
139
- baseUrl = DEFAULT_OG_IMAGE_BASE_URL,
140
- useBase64 = true
141
- } = options;
142
-
143
- if (useBase64) {
144
- // Clean params - remove undefined/null/empty values
145
- const cleanParams: Record<string, string | number | boolean> = {};
146
- Object.entries(params).forEach(([key, value]) => {
147
- if (value !== undefined && value !== null && value !== '') {
148
- cleanParams[key] = value;
149
- }
150
- });
151
-
152
- // Encode as base64 (Unicode-safe)
153
- const jsonString = JSON.stringify(cleanParams);
154
- const base64Data = encodeBase64(jsonString);
155
-
156
- // CRITICAL: Use path parameter instead of query parameter
157
- // Next.js strips query params in internal requests for metadata generation
158
- // Using /api/og/[data] instead of /api/og?data=... preserves the data
159
- // IMPORTANT: Add trailing slash to avoid 308 redirects which cause timeouts in crawlers
160
- return `${baseUrl}/${base64Data}/`;
161
- } else {
162
- // Legacy query params mode
163
- const searchParams = new URLSearchParams();
164
-
165
- // Add all defined parameters
166
- Object.entries(params).forEach(([key, value]) => {
167
- if (value !== undefined && value !== null && value !== '') {
168
- searchParams.append(key, String(value));
169
- }
170
- });
171
-
172
- const query = searchParams.toString();
173
- return query ? `${baseUrl}?${query}` : baseUrl;
174
- }
175
- }
176
-
177
- /**
178
- * Get absolute OG image URL from relative path
179
- *
180
- * Useful for generating absolute URLs required by Open Graph meta tags
181
- *
182
- * @param relativePath - Relative OG image path (e.g., '/api/og?title=Hello')
183
- * @param siteUrl - Base site URL (e.g., 'https://example.com')
184
- * @returns Absolute URL
185
- *
186
- * @example
187
- * ```typescript
188
- * const absolute = getAbsoluteOgImageUrl(
189
- * '/api/og?title=Hello',
190
- * 'https://example.com'
191
- * );
192
- * // Result: https://example.com/api/og?title=Hello
193
- * ```
194
- */
195
- export function getAbsoluteOgImageUrl(
196
- relativePath: string,
197
- siteUrl: string
198
- ): string {
199
- // If path is already an absolute URL, return as-is
200
- if (relativePath.startsWith('http://') || relativePath.startsWith('https://')) {
201
- return relativePath;
202
- }
203
-
204
- // Remove trailing slash from site URL
205
- const cleanSiteUrl = siteUrl.replace(/\/$/, '');
206
-
207
- // Ensure relative path starts with /
208
- const cleanPath = relativePath.startsWith('/')
209
- ? relativePath
210
- : `/${relativePath}`;
211
-
212
- return `${cleanSiteUrl}${cleanPath}`;
213
- }
214
-
215
- /**
216
- * Create OG image URL builder with preset configuration
217
- *
218
- * Useful when you want to reuse the same base URL and default parameters
219
- *
220
- * @param defaults - Default parameters to merge with each URL generation
221
- * @param options - Default options (baseUrl, useBase64)
222
- * @returns URL builder function
223
- *
224
- * @example
225
- * ```typescript
226
- * const buildOgUrl = createOgImageUrlBuilder(
227
- * { siteName: 'My Site', logo: '/logo.png' },
228
- * { baseUrl: '/api/og' }
229
- * );
230
- *
231
- * const url1 = buildOgUrl({ title: 'Page 1' });
232
- * const url2 = buildOgUrl({ title: 'Page 2', description: 'Custom desc' });
233
- * ```
234
- */
235
- export function createOgImageUrlBuilder(
236
- defaults: Partial<OgImageUrlParams> = {},
237
- options: GenerateOgImageUrlOptions = {}
238
- ) {
239
- return (params: OgImageUrlParams): string => {
240
- return generateOgImageUrl(
241
- { ...defaults, ...params },
242
- options
243
- );
244
- };
245
- }
246
-
247
- /**
248
- * Parse OG image URL parameters from a URL string (legacy query params)
249
- *
250
- * @param url - Full or relative URL with query parameters
251
- * @returns Parsed parameters object
252
- *
253
- * @example
254
- * ```typescript
255
- * const params = parseOgImageUrl('/api/og?title=Hello&description=World');
256
- * // Result: { title: 'Hello', description: 'World' }
257
- * ```
258
- */
259
- export function parseOgImageUrl(url: string): Record<string, string> {
260
- try {
261
- const urlObj = new URL(url, 'http://dummy.com');
262
- const params: Record<string, string> = {};
263
-
264
- urlObj.searchParams.forEach((value, key) => {
265
- params[key] = value;
266
- });
267
-
268
- return params;
269
- } catch {
270
- return {};
271
- }
272
- }
273
-
274
- /**
275
- * Parse OG image data from base64-encoded query parameter
276
- *
277
- * Use this in your API route to decode the `data` parameter
278
- * Supports both base64 (new) and legacy query params format
279
- *
280
- * @param searchParams - URL search params or request object
281
- * @returns Parsed OG image parameters
282
- *
283
- * @example
284
- * ```typescript
285
- * // In Next.js API route (pages/api/og.ts)
286
- * export default function handler(req) {
287
- * const params = parseOgImageData(req.query);
288
- * // { title: 'Hello', description: 'World' }
289
- * }
290
- *
291
- * // In Next.js App Router (app/api/og/route.ts)
292
- * export async function GET(request: Request) {
293
- * const { searchParams } = new URL(request.url);
294
- * const params = parseOgImageData(Object.fromEntries(searchParams));
295
- * // { title: 'Hello', description: 'World' }
296
- * }
297
- * ```
298
- */
299
- export function parseOgImageData(
300
- searchParams: Record<string, string | string[] | undefined> | URLSearchParams
301
- ): Record<string, string> {
302
- try {
303
- // Handle URLSearchParams
304
- let params: Record<string, string | undefined>;
305
-
306
- if (searchParams instanceof URLSearchParams) {
307
- // Convert URLSearchParams to object
308
- params = {};
309
- for (const [key, value] of searchParams.entries()) {
310
- params[key] = value;
311
- }
312
- } else {
313
- params = searchParams as Record<string, string | undefined>;
314
- }
315
-
316
- // Debug logging
317
- if (process.env.NODE_ENV === 'development') {
318
- console.log('[parseOgImageData] Input params keys:', Object.keys(params));
319
- console.log('[parseOgImageData] Input params:', params);
320
- }
321
-
322
- // Check for base64-encoded data parameter
323
- const dataParam = params.data;
324
- if (dataParam && typeof dataParam === 'string' && dataParam.trim() !== '') {
325
- if (process.env.NODE_ENV === 'development') {
326
- console.log('[parseOgImageData] Found data param, length:', dataParam.length);
327
- }
328
-
329
- try {
330
- const decoded = decodeBase64(dataParam);
331
- if (process.env.NODE_ENV === 'development') {
332
- console.log('[parseOgImageData] Decoded string:', decoded.substring(0, 100));
333
- }
334
-
335
- const parsed = JSON.parse(decoded);
336
- if (process.env.NODE_ENV === 'development') {
337
- console.log('[parseOgImageData] Parsed JSON:', parsed);
338
- }
339
-
340
- // Ensure all values are strings
341
- const result: Record<string, string> = {};
342
- for (const [key, value] of Object.entries(parsed)) {
343
- if (value !== undefined && value !== null) {
344
- result[key] = String(value);
345
- }
346
- }
347
-
348
- if (process.env.NODE_ENV === 'development') {
349
- console.log('[parseOgImageData] Result:', result);
350
- }
351
-
352
- return result;
353
- } catch (decodeError) {
354
- console.error('[parseOgImageData] Error decoding/parsing data param:', decodeError);
355
- if (decodeError instanceof Error) {
356
- console.error('[parseOgImageData] Error message:', decodeError.message);
357
- }
358
- // Fall through to legacy query params
359
- }
360
- } else {
361
- if (process.env.NODE_ENV === 'development') {
362
- console.log('[parseOgImageData] No data param found or empty');
363
- }
364
- }
365
-
366
- // Fallback to legacy query params format
367
- const result: Record<string, string> = {};
368
- for (const [key, value] of Object.entries(params)) {
369
- if (key !== 'data' && value !== undefined && value !== null) {
370
- result[key] = Array.isArray(value) ? value[0] : String(value);
371
- }
372
- }
373
-
374
- if (process.env.NODE_ENV === 'development') {
375
- console.log('[parseOgImageData] Fallback result:', result);
376
- }
377
-
378
- return result;
379
- } catch (error) {
380
- console.error('[parseOgImageData] Unexpected error:', error);
381
- return {};
382
- }
383
- }
384
-
385
- // Export base64 utilities for advanced use cases
386
- export { encodeBase64, decodeBase64 };