@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.
- package/README.md +49 -80
- package/dist/config/index.mjs +6 -17
- package/dist/config/index.mjs.map +1 -1
- package/dist/i18n/routing.d.mts +2 -2
- package/dist/index.d.mts +1 -6
- package/dist/index.mjs +9 -865
- package/dist/index.mjs.map +1 -1
- package/dist/og-image/index.d.mts +40 -100
- package/dist/og-image/index.mjs +46 -823
- package/dist/og-image/index.mjs.map +1 -1
- package/package.json +14 -25
- package/src/index.ts +0 -3
- package/src/og-image/README.md +142 -53
- package/src/og-image/index.ts +3 -27
- package/src/og-image/metadata.ts +67 -0
- package/src/og-image/types.ts +28 -44
- package/src/og-image/url.ts +58 -0
- package/dist/og-image/components/index.d.mts +0 -59
- package/dist/og-image/components/index.mjs +0 -338
- package/dist/og-image/components/index.mjs.map +0 -1
- package/dist/og-image/utils/index.d.mts +0 -308
- package/dist/og-image/utils/index.mjs +0 -327
- package/dist/og-image/utils/index.mjs.map +0 -1
- package/src/og-image/components/DefaultTemplate.tsx +0 -369
- package/src/og-image/components/index.ts +0 -9
- package/src/og-image/route.tsx +0 -312
- package/src/og-image/utils/fonts.ts +0 -150
- package/src/og-image/utils/index.ts +0 -28
- package/src/og-image/utils/metadata.ts +0 -269
- package/src/og-image/utils/url.ts +0 -386
|
@@ -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 };
|