@djangocfg/nextjs 2.1.225 → 2.1.227

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,312 +0,0 @@
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
-
25
- import { DefaultTemplate } from './components/DefaultTemplate';
26
- import { loadGoogleFonts } from './utils';
27
- import { parseOgImageData } from './utils/url';
28
-
29
- import type { ReactElement } from 'react';
30
- import type { OgImageTemplateProps } from './types';
31
-
32
- export interface OgImageHandlerConfig {
33
- /** Custom template component (optional, defaults to DefaultTemplate) */
34
- template?: (props: OgImageTemplateProps) => ReactElement;
35
- /** Default props to merge with query params */
36
- defaultProps?: Partial<OgImageTemplateProps>;
37
- /** Google Fonts to load */
38
- fonts?: Array<{ family: string; weight: 400 | 500 | 600 | 700 | 800 | 900 }>;
39
- /** Image size */
40
- size?: { width: number; height: number };
41
- /** Enable debug mode */
42
- debug?: boolean;
43
- }
44
-
45
- /**
46
- * Factory function to create OG Image route handler
47
- *
48
- * @example
49
- * ```tsx
50
- * // app/api/og/route.tsx
51
- * import { createOgImageHandler } from '@djangocfg/nextjs/og-image';
52
- * import { MyTemplate } from '@/components/MyTemplate';
53
- *
54
- * export const { GET, runtime } = createOgImageHandler({
55
- * template: MyTemplate,
56
- * defaultProps: { siteName: 'My Site' },
57
- * fonts: [{ family: 'Inter', weight: 700 }],
58
- * });
59
- * ```
60
- */
61
- export function createOgImageHandler(config: OgImageHandlerConfig) {
62
- const {
63
- template: Template = DefaultTemplate,
64
- defaultProps = {},
65
- fonts: fontConfig = [],
66
- size = { width: 1200, height: 630 },
67
- debug = false,
68
- } = config;
69
-
70
- async function GET(req: NextRequest) {
71
- let searchParams: URLSearchParams = new URLSearchParams();
72
-
73
- // Try to get searchParams from multiple sources
74
- if (req.nextUrl?.searchParams && req.nextUrl.searchParams.size > 0) {
75
- searchParams = req.nextUrl.searchParams;
76
- } else if (req.nextUrl?.search && req.nextUrl.search.length > 1) {
77
- searchParams = new URLSearchParams(req.nextUrl.search);
78
- } else {
79
- try {
80
- const url = new URL(req.url);
81
- if (url.searchParams.size > 0) {
82
- searchParams = url.searchParams;
83
- }
84
- } catch (error) {
85
- // Ignore
86
- }
87
-
88
- if (searchParams.size === 0 && req.url) {
89
- const queryIndex = req.url.indexOf('?');
90
- if (queryIndex !== -1) {
91
- const queryString = req.url.substring(queryIndex + 1);
92
- searchParams = new URLSearchParams(queryString);
93
- }
94
- }
95
-
96
- if (searchParams.size === 0) {
97
- const customParams = req.headers.get('x-og-search-params');
98
- if (customParams) {
99
- searchParams = new URLSearchParams(customParams);
100
- }
101
- }
102
- }
103
-
104
- // Initialize with defaults
105
- let title = defaultProps.title || 'Untitled';
106
- let subtitle = defaultProps.subtitle || '';
107
- let description = defaultProps.description || subtitle;
108
-
109
- // Support base64 data parameter (priority: base64 > query params > defaults)
110
- // All template props can be encoded in base64, including styling params
111
- const dataParam = searchParams.get('data');
112
- let decodedParams: Record<string, any> = {};
113
-
114
- if (dataParam) {
115
- try {
116
- const paramsObj: Record<string, string> = { data: dataParam };
117
- for (const [key, value] of searchParams.entries()) {
118
- if (key !== 'data') {
119
- paramsObj[key] = value;
120
- }
121
- }
122
- decodedParams = parseOgImageData(paramsObj);
123
-
124
- // Base64 data takes precedence - apply all decoded values
125
- if (decodedParams.title && typeof decodedParams.title === 'string' && decodedParams.title.trim() !== '') {
126
- title = decodedParams.title.trim();
127
- }
128
- if (decodedParams.subtitle && typeof decodedParams.subtitle === 'string' && decodedParams.subtitle.trim() !== '') {
129
- subtitle = decodedParams.subtitle.trim();
130
- }
131
- if (decodedParams.description && typeof decodedParams.description === 'string' && decodedParams.description.trim() !== '') {
132
- description = decodedParams.description.trim();
133
- }
134
- } catch (error) {
135
- // Silently fall back to defaults
136
- }
137
- }
138
-
139
- // Fallback to query params if not set from base64
140
- if (!title || title === 'Untitled') {
141
- const titleParam = searchParams.get('title');
142
- if (titleParam) {
143
- title = titleParam;
144
- }
145
- }
146
- if (!subtitle) {
147
- const subtitleParam = searchParams.get('subtitle');
148
- if (subtitleParam) {
149
- subtitle = subtitleParam;
150
- }
151
- }
152
- if (!description || description === subtitle) {
153
- const descParam = searchParams.get('description');
154
- if (descParam) {
155
- description = descParam;
156
- }
157
- }
158
-
159
- // Load fonts if configured
160
- let fonts: any[] = [];
161
- if (fontConfig.length > 0) {
162
- fonts = await loadGoogleFonts(fontConfig);
163
- }
164
-
165
- // Helper function to parse numeric/boolean values from decoded params
166
- const parseValue = (value: any, type: 'number' | 'boolean' | 'string' = 'string'): any => {
167
- if (value === undefined || value === null || value === '') {
168
- return undefined;
169
- }
170
- if (type === 'number') {
171
- const num = Number(value);
172
- return isNaN(num) ? undefined : num;
173
- }
174
- if (type === 'boolean') {
175
- if (typeof value === 'boolean') return value;
176
- if (typeof value === 'string') {
177
- return value.toLowerCase() === 'true' || value === '1';
178
- }
179
- return Boolean(value);
180
- }
181
- return String(value);
182
- };
183
-
184
- // Merge props: decoded params from URL override defaultProps
185
- const templateProps: OgImageTemplateProps = {
186
- ...defaultProps,
187
- // Content
188
- title,
189
- subtitle,
190
- description,
191
- // Override with decoded params if present
192
- siteName: decodedParams.siteName || defaultProps.siteName,
193
- logo: decodedParams.logo || defaultProps.logo,
194
- // Background
195
- backgroundType: (decodedParams.backgroundType as 'gradient' | 'solid') || defaultProps.backgroundType,
196
- gradientStart: decodedParams.gradientStart || defaultProps.gradientStart,
197
- gradientEnd: decodedParams.gradientEnd || defaultProps.gradientEnd,
198
- backgroundColor: decodedParams.backgroundColor || defaultProps.backgroundColor,
199
- // Typography - Title
200
- titleSize: parseValue(decodedParams.titleSize, 'number') ?? defaultProps.titleSize,
201
- titleWeight: parseValue(decodedParams.titleWeight, 'number') ?? defaultProps.titleWeight,
202
- titleColor: decodedParams.titleColor || defaultProps.titleColor,
203
- // Typography - Description
204
- descriptionSize: parseValue(decodedParams.descriptionSize, 'number') ?? defaultProps.descriptionSize,
205
- descriptionColor: decodedParams.descriptionColor || defaultProps.descriptionColor,
206
- // Typography - Site Name
207
- siteNameSize: parseValue(decodedParams.siteNameSize, 'number') ?? defaultProps.siteNameSize,
208
- siteNameColor: decodedParams.siteNameColor || defaultProps.siteNameColor,
209
- // Layout
210
- padding: parseValue(decodedParams.padding, 'number') ?? defaultProps.padding,
211
- logoSize: parseValue(decodedParams.logoSize, 'number') ?? defaultProps.logoSize,
212
- // Visibility flags
213
- showLogo: parseValue(decodedParams.showLogo, 'boolean') ?? defaultProps.showLogo,
214
- showSiteName: parseValue(decodedParams.showSiteName, 'boolean') ?? defaultProps.showSiteName,
215
- };
216
-
217
-
218
- return new ImageResponse(
219
- <Template {...templateProps} />,
220
- {
221
- width: size.width,
222
- height: size.height,
223
- fonts,
224
- debug: debug || process.env.NODE_ENV === 'development',
225
- }
226
- );
227
- }
228
-
229
- return {
230
- GET,
231
- runtime: 'edge' as const,
232
- };
233
- }
234
-
235
- /**
236
- * Create OG Image route handler for dynamic route with path parameter
237
- *
238
- * This is a convenience wrapper for Next.js dynamic routes like `/api/og/[data]/route.tsx`.
239
- * It extracts the `data` parameter from the path and passes it to the handler as a query parameter.
240
- * Also handles static export mode automatically.
241
- *
242
- * @example
243
- * ```tsx
244
- * // app/api/og/[data]/route.tsx
245
- * import { createOgImageDynamicRoute } from '@djangocfg/nextjs/og-image';
246
- * import { OgImageTemplate } from '@/components/OgImageTemplate';
247
- *
248
- * export const runtime = 'nodejs';
249
- * export const dynamic = 'force-static';
250
- * export const revalidate = false;
251
- *
252
- * const handler = createOgImageDynamicRoute({
253
- * template: OgImageTemplate,
254
- * defaultProps: {
255
- * siteName: 'My App',
256
- * logo: '/logo.svg',
257
- * },
258
- * });
259
- *
260
- * export async function GET(
261
- * request: NextRequest,
262
- * { params }: { params: { data: string } }
263
- ) {
264
- * return handler(request, params);
265
- * }
266
- * ```
267
- */
268
- export function createOgImageDynamicRoute(config: OgImageHandlerConfig) {
269
- const handler = createOgImageHandler(config);
270
- const isStaticBuild = typeof process !== 'undefined' && process.env.NEXT_PUBLIC_STATIC_BUILD === 'true';
271
-
272
- async function GET(
273
- request: NextRequest,
274
- context: { params: Promise<{ data: string }> }
275
- ) {
276
- // In static export mode, return a simple error response
277
- if (isStaticBuild) {
278
- return new Response('OG Image generation is not available in static export mode', {
279
- status: 404,
280
- headers: { 'Content-Type': 'text/plain' },
281
- });
282
- }
283
-
284
- // Await params (Next.js 15+ uses Promise)
285
- const params = await context.params;
286
-
287
- // Extract data from path parameter
288
- const dataParam = params.data;
289
-
290
- // Create a request with the data parameter as a query param for the handler
291
- const url = new URL(request.url);
292
- url.searchParams.set('data', dataParam);
293
-
294
- const modifiedRequest = new NextRequest(url.toString(), {
295
- method: request.method,
296
- headers: request.headers,
297
- });
298
-
299
- return handler.GET(modifiedRequest);
300
- }
301
-
302
- // For static export, provide generateStaticParams that returns empty array
303
- // This allows the route to be excluded from static build
304
- async function generateStaticParams(): Promise<Array<{ data: string }>> {
305
- return [];
306
- }
307
-
308
- return {
309
- GET,
310
- generateStaticParams,
311
- };
312
- }
@@ -1,150 +0,0 @@
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
-
@@ -1,28 +0,0 @@
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
- generateAppMetadata,
25
- createAppMetadataGenerator,
26
- type AppMetadataOptions,
27
- } from './metadata';
28
-