@djangocfg/nextjs 2.1.225 → 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 +1 @@
1
- {"version":3,"sources":["../../src/og-image/route.tsx","../../src/og-image/components/DefaultTemplate.tsx","../../src/og-image/utils/fonts.ts","../../src/og-image/utils/url.ts","../../src/og-image/utils/metadata.ts"],"sourcesContent":["/**\n * OG Image Route Handler\n *\n * Factory function to create OG Image route handler for Next.js App Router\n *\n * Usage:\n * ```tsx\n * // app/api/og/route.tsx\n * import { createOgImageHandler } from '@djangocfg/nextjs/og-image';\n * import { MyTemplate } from './templates';\n *\n * export const { GET, runtime } = createOgImageHandler({\n * template: MyTemplate,\n * defaultProps: {\n * siteName: 'My Site',\n * },\n * fonts: [{ family: 'Manrope', weight: 700 }],\n * });\n * ```\n */\n\nimport { ImageResponse } from 'next/og';\nimport { NextRequest } from 'next/server';\n\nimport { DefaultTemplate } from './components/DefaultTemplate';\nimport { loadGoogleFonts } from './utils';\nimport { parseOgImageData } from './utils/url';\n\nimport type { ReactElement } from 'react';\nimport type { OgImageTemplateProps } from './types';\n\nexport interface OgImageHandlerConfig {\n /** Custom template component (optional, defaults to DefaultTemplate) */\n template?: (props: OgImageTemplateProps) => ReactElement;\n /** Default props to merge with query params */\n defaultProps?: Partial<OgImageTemplateProps>;\n /** Google Fonts to load */\n fonts?: Array<{ family: string; weight: 400 | 500 | 600 | 700 | 800 | 900 }>;\n /** Image size */\n size?: { width: number; height: number };\n /** Enable debug mode */\n debug?: boolean;\n}\n\n/**\n * Factory function to create OG Image route handler\n * \n * @example\n * ```tsx\n * // app/api/og/route.tsx\n * import { createOgImageHandler } from '@djangocfg/nextjs/og-image';\n * import { MyTemplate } from '@/components/MyTemplate';\n * \n * export const { GET, runtime } = createOgImageHandler({\n * template: MyTemplate,\n * defaultProps: { siteName: 'My Site' },\n * fonts: [{ family: 'Inter', weight: 700 }],\n * });\n * ```\n */\nexport function createOgImageHandler(config: OgImageHandlerConfig) {\n const {\n template: Template = DefaultTemplate,\n defaultProps = {},\n fonts: fontConfig = [],\n size = { width: 1200, height: 630 },\n debug = false,\n } = config;\n\n async function GET(req: NextRequest) {\n let searchParams: URLSearchParams = new URLSearchParams();\n \n // Try to get searchParams from multiple sources\n if (req.nextUrl?.searchParams && req.nextUrl.searchParams.size > 0) {\n searchParams = req.nextUrl.searchParams;\n } else if (req.nextUrl?.search && req.nextUrl.search.length > 1) {\n searchParams = new URLSearchParams(req.nextUrl.search);\n } else {\n try {\n const url = new URL(req.url);\n if (url.searchParams.size > 0) {\n searchParams = url.searchParams;\n }\n } catch (error) {\n // Ignore\n }\n \n if (searchParams.size === 0 && req.url) {\n const queryIndex = req.url.indexOf('?');\n if (queryIndex !== -1) {\n const queryString = req.url.substring(queryIndex + 1);\n searchParams = new URLSearchParams(queryString);\n }\n }\n \n if (searchParams.size === 0) {\n const customParams = req.headers.get('x-og-search-params');\n if (customParams) {\n searchParams = new URLSearchParams(customParams);\n }\n }\n }\n\n // Initialize with defaults\n let title = defaultProps.title || 'Untitled';\n let subtitle = defaultProps.subtitle || '';\n let description = defaultProps.description || subtitle;\n\n // Support base64 data parameter (priority: base64 > query params > defaults)\n // All template props can be encoded in base64, including styling params\n const dataParam = searchParams.get('data');\n let decodedParams: Record<string, any> = {};\n \n if (dataParam) {\n try {\n const paramsObj: Record<string, string> = { data: dataParam };\n for (const [key, value] of searchParams.entries()) {\n if (key !== 'data') {\n paramsObj[key] = value;\n }\n }\n decodedParams = parseOgImageData(paramsObj);\n \n // Base64 data takes precedence - apply all decoded values\n if (decodedParams.title && typeof decodedParams.title === 'string' && decodedParams.title.trim() !== '') {\n title = decodedParams.title.trim();\n }\n if (decodedParams.subtitle && typeof decodedParams.subtitle === 'string' && decodedParams.subtitle.trim() !== '') {\n subtitle = decodedParams.subtitle.trim();\n }\n if (decodedParams.description && typeof decodedParams.description === 'string' && decodedParams.description.trim() !== '') {\n description = decodedParams.description.trim();\n }\n } catch (error) {\n // Silently fall back to defaults\n }\n }\n\n // Fallback to query params if not set from base64\n if (!title || title === 'Untitled') {\n const titleParam = searchParams.get('title');\n if (titleParam) {\n title = titleParam;\n }\n }\n if (!subtitle) {\n const subtitleParam = searchParams.get('subtitle');\n if (subtitleParam) {\n subtitle = subtitleParam;\n }\n }\n if (!description || description === subtitle) {\n const descParam = searchParams.get('description');\n if (descParam) {\n description = descParam;\n }\n }\n\n // Load fonts if configured\n let fonts: any[] = [];\n if (fontConfig.length > 0) {\n fonts = await loadGoogleFonts(fontConfig);\n }\n\n // Helper function to parse numeric/boolean values from decoded params\n const parseValue = (value: any, type: 'number' | 'boolean' | 'string' = 'string'): any => {\n if (value === undefined || value === null || value === '') {\n return undefined;\n }\n if (type === 'number') {\n const num = Number(value);\n return isNaN(num) ? undefined : num;\n }\n if (type === 'boolean') {\n if (typeof value === 'boolean') return value;\n if (typeof value === 'string') {\n return value.toLowerCase() === 'true' || value === '1';\n }\n return Boolean(value);\n }\n return String(value);\n };\n\n // Merge props: decoded params from URL override defaultProps\n const templateProps: OgImageTemplateProps = {\n ...defaultProps,\n // Content\n title,\n subtitle,\n description,\n // Override with decoded params if present\n siteName: decodedParams.siteName || defaultProps.siteName,\n logo: decodedParams.logo || defaultProps.logo,\n // Background\n backgroundType: (decodedParams.backgroundType as 'gradient' | 'solid') || defaultProps.backgroundType,\n gradientStart: decodedParams.gradientStart || defaultProps.gradientStart,\n gradientEnd: decodedParams.gradientEnd || defaultProps.gradientEnd,\n backgroundColor: decodedParams.backgroundColor || defaultProps.backgroundColor,\n // Typography - Title\n titleSize: parseValue(decodedParams.titleSize, 'number') ?? defaultProps.titleSize,\n titleWeight: parseValue(decodedParams.titleWeight, 'number') ?? defaultProps.titleWeight,\n titleColor: decodedParams.titleColor || defaultProps.titleColor,\n // Typography - Description\n descriptionSize: parseValue(decodedParams.descriptionSize, 'number') ?? defaultProps.descriptionSize,\n descriptionColor: decodedParams.descriptionColor || defaultProps.descriptionColor,\n // Typography - Site Name\n siteNameSize: parseValue(decodedParams.siteNameSize, 'number') ?? defaultProps.siteNameSize,\n siteNameColor: decodedParams.siteNameColor || defaultProps.siteNameColor,\n // Layout\n padding: parseValue(decodedParams.padding, 'number') ?? defaultProps.padding,\n logoSize: parseValue(decodedParams.logoSize, 'number') ?? defaultProps.logoSize,\n // Visibility flags\n showLogo: parseValue(decodedParams.showLogo, 'boolean') ?? defaultProps.showLogo,\n showSiteName: parseValue(decodedParams.showSiteName, 'boolean') ?? defaultProps.showSiteName,\n };\n\n\n return new ImageResponse(\n <Template {...templateProps} />,\n {\n width: size.width,\n height: size.height,\n fonts,\n debug: debug || process.env.NODE_ENV === 'development',\n }\n );\n }\n\n return {\n GET,\n runtime: 'edge' as const,\n };\n}\n\n/**\n * Create OG Image route handler for dynamic route with path parameter\n * \n * This is a convenience wrapper for Next.js dynamic routes like `/api/og/[data]/route.tsx`.\n * It extracts the `data` parameter from the path and passes it to the handler as a query parameter.\n * Also handles static export mode automatically.\n * \n * @example\n * ```tsx\n * // app/api/og/[data]/route.tsx\n * import { createOgImageDynamicRoute } from '@djangocfg/nextjs/og-image';\n * import { OgImageTemplate } from '@/components/OgImageTemplate';\n * \n * export const runtime = 'nodejs';\n * export const dynamic = 'force-static';\n * export const revalidate = false;\n * \n * const handler = createOgImageDynamicRoute({\n * template: OgImageTemplate,\n * defaultProps: {\n * siteName: 'My App',\n * logo: '/logo.svg',\n * },\n * });\n * \n * export async function GET(\n * request: NextRequest,\n * { params }: { params: { data: string } }\n ) {\n * return handler(request, params);\n * }\n * ```\n */\nexport function createOgImageDynamicRoute(config: OgImageHandlerConfig) {\n const handler = createOgImageHandler(config);\n const isStaticBuild = typeof process !== 'undefined' && process.env.NEXT_PUBLIC_STATIC_BUILD === 'true';\n\n async function GET(\n request: NextRequest,\n context: { params: Promise<{ data: string }> }\n ) {\n // In static export mode, return a simple error response\n if (isStaticBuild) {\n return new Response('OG Image generation is not available in static export mode', {\n status: 404,\n headers: { 'Content-Type': 'text/plain' },\n });\n }\n\n // Await params (Next.js 15+ uses Promise)\n const params = await context.params;\n \n // Extract data from path parameter\n const dataParam = params.data;\n \n // Create a request with the data parameter as a query param for the handler\n const url = new URL(request.url);\n url.searchParams.set('data', dataParam);\n \n const modifiedRequest = new NextRequest(url.toString(), {\n method: request.method,\n headers: request.headers,\n });\n\n return handler.GET(modifiedRequest);\n }\n\n // For static export, provide generateStaticParams that returns empty array\n // This allows the route to be excluded from static build\n async function generateStaticParams(): Promise<Array<{ data: string }>> {\n return [];\n }\n\n return {\n GET,\n generateStaticParams,\n };\n}\n","/**\n * Default OG Image Template\n *\n * A modern, gradient-based template for OG images\n */\n\nimport type { ReactElement } from 'react';\nimport type { OgImageTemplateProps } from '../types';\n\n/**\n * Default OG Image Template Component\n *\n * Features:\n * - Modern gradient background\n * - Responsive text sizing\n * - Optional logo and site name\n * - Clean typography\n * - Full customization support\n *\n * @param props - Template props with title, description, siteName, logo and optional customization\n */\nexport function DefaultTemplate({\n title,\n description,\n siteName,\n logo,\n // Visibility flags\n showLogo = true,\n showSiteName = true,\n // Background customization\n backgroundType = 'gradient',\n gradientStart = '#667eea',\n gradientEnd = '#764ba2',\n backgroundColor = '#ffffff',\n // Typography - Title\n titleSize,\n titleWeight = 800,\n titleColor = 'white',\n // Typography - Description\n descriptionSize = 32,\n descriptionColor = 'rgba(255, 255, 255, 0.85)',\n // Typography - Site Name\n siteNameSize = 28,\n siteNameColor = 'rgba(255, 255, 255, 0.95)',\n // Layout\n padding = 80,\n logoSize = 48,\n // Dev mode\n devMode = false,\n}: OgImageTemplateProps): ReactElement {\n // Calculate title size if not provided (responsive based on title length)\n const calculatedTitleSize = titleSize || (title.length > 60 ? 56 : 72);\n \n // Determine background style\n const backgroundStyle =\n backgroundType === 'gradient'\n ? `linear-gradient(135deg, ${gradientStart} 0%, ${gradientEnd} 100%)`\n : backgroundColor;\n\n // Grid overlay for dev mode\n const gridOverlay = devMode ? (\n <div\n style={{\n position: 'absolute',\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n backgroundImage: `\n linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px),\n linear-gradient(90deg, rgba(0, 0, 0, 0.1) 1px, transparent 1px)\n `,\n backgroundSize: '20px 20px',\n pointerEvents: 'none',\n zIndex: 10,\n }}\n />\n ) : null;\n\n return (\n <div\n style={{\n height: '100%',\n width: '100%',\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'flex-start',\n justifyContent: 'space-between',\n background: backgroundStyle,\n padding: `${padding}px`,\n fontFamily: 'system-ui, -apple-system, sans-serif',\n position: 'relative',\n }}\n >\n {gridOverlay}\n \n {/* Header with logo and site name */}\n {((showLogo && logo) || (showSiteName && siteName)) && (\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: '16px',\n }}\n >\n {showLogo && logo && (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={logo}\n alt=\"Logo\"\n width={logoSize}\n height={logoSize}\n style={{\n borderRadius: '8px',\n }}\n />\n )}\n {showSiteName && siteName && (\n <div\n style={{\n fontSize: siteNameSize,\n fontWeight: 600,\n color: siteNameColor,\n letterSpacing: '-0.02em',\n }}\n >\n {siteName}\n </div>\n )}\n </div>\n )}\n\n {/* Main content */}\n <div\n style={{\n display: 'flex',\n flexDirection: 'column',\n gap: '24px',\n flex: 1,\n justifyContent: 'center',\n }}\n >\n {/* Title */}\n <div\n style={{\n fontSize: calculatedTitleSize,\n fontWeight: titleWeight,\n color: titleColor,\n lineHeight: 1.1,\n letterSpacing: '-0.03em',\n textShadow: backgroundType === 'gradient' ? '0 2px 20px rgba(0, 0, 0, 0.2)' : 'none',\n maxWidth: '100%',\n wordWrap: 'break-word',\n }}\n >\n {title}\n </div>\n\n {/* Description */}\n {description && (\n <div\n style={{\n fontSize: descriptionSize,\n fontWeight: 400,\n color: descriptionColor,\n lineHeight: 1.5,\n letterSpacing: '-0.01em',\n maxWidth: '90%',\n display: '-webkit-box',\n WebkitLineClamp: 2,\n WebkitBoxOrient: 'vertical',\n overflow: 'hidden',\n }}\n >\n {description}\n </div>\n )}\n </div>\n\n {/* Footer decoration */}\n <div\n style={{\n display: 'flex',\n width: '100%',\n height: '4px',\n background: backgroundType === 'gradient' \n ? `linear-gradient(90deg, ${gradientStart} 0%, ${gradientEnd} 100%)`\n : gradientStart,\n borderRadius: '2px',\n }}\n />\n </div>\n );\n}\n\n/**\n * Simple light template variant\n * \n * Light background variant with dark text\n */\nexport function LightTemplate({\n title,\n description,\n siteName,\n logo,\n // Visibility flags\n showLogo = true,\n showSiteName = true,\n // Background customization (defaults to light theme)\n backgroundType = 'solid',\n gradientStart = '#667eea',\n gradientEnd = '#764ba2',\n backgroundColor = '#ffffff',\n // Typography - Title\n titleSize,\n titleWeight = 800,\n titleColor = '#111',\n // Typography - Description\n descriptionSize = 32,\n descriptionColor = '#666',\n // Typography - Site Name\n siteNameSize = 28,\n siteNameColor = '#111',\n // Layout\n padding = 80,\n logoSize = 48,\n // Dev mode\n devMode = false,\n}: OgImageTemplateProps): ReactElement {\n // Calculate title size if not provided (responsive based on title length)\n const calculatedTitleSize = titleSize || (title.length > 60 ? 56 : 72);\n \n // Determine background style\n const backgroundStyle =\n backgroundType === 'gradient'\n ? `linear-gradient(135deg, ${gradientStart} 0%, ${gradientEnd} 100%)`\n : backgroundColor;\n\n // Grid overlay for dev mode\n const gridOverlay = devMode ? (\n <div\n style={{\n position: 'absolute',\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n backgroundImage: `\n linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px),\n linear-gradient(90deg, rgba(0, 0, 0, 0.1) 1px, transparent 1px)\n `,\n backgroundSize: '20px 20px',\n pointerEvents: 'none',\n zIndex: 10,\n }}\n />\n ) : null;\n\n return (\n <div\n style={{\n height: '100%',\n width: '100%',\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'flex-start',\n justifyContent: 'space-between',\n background: backgroundStyle,\n padding: `${padding}px`,\n fontFamily: 'system-ui, -apple-system, sans-serif',\n position: 'relative',\n }}\n >\n {gridOverlay}\n \n {/* Header with logo and site name */}\n {((showLogo && logo) || (showSiteName && siteName)) && (\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: '16px',\n }}\n >\n {showLogo && logo && (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={logo}\n alt=\"Logo\"\n width={logoSize}\n height={logoSize}\n style={{\n borderRadius: '8px',\n }}\n />\n )}\n {showSiteName && siteName && (\n <div\n style={{\n fontSize: siteNameSize,\n fontWeight: 600,\n color: siteNameColor,\n letterSpacing: '-0.02em',\n }}\n >\n {siteName}\n </div>\n )}\n </div>\n )}\n\n {/* Main content */}\n <div\n style={{\n display: 'flex',\n flexDirection: 'column',\n gap: '24px',\n flex: 1,\n justifyContent: 'center',\n }}\n >\n {/* Title */}\n <div\n style={{\n fontSize: calculatedTitleSize,\n fontWeight: titleWeight,\n color: titleColor,\n lineHeight: 1.1,\n letterSpacing: '-0.03em',\n maxWidth: '100%',\n wordWrap: 'break-word',\n }}\n >\n {title}\n </div>\n\n {/* Description */}\n {description && (\n <div\n style={{\n fontSize: descriptionSize,\n fontWeight: 400,\n color: descriptionColor,\n lineHeight: 1.5,\n letterSpacing: '-0.01em',\n maxWidth: '90%',\n }}\n >\n {description}\n </div>\n )}\n </div>\n\n {/* Footer decoration */}\n <div\n style={{\n display: 'flex',\n width: '100%',\n height: '4px',\n background: backgroundType === 'gradient' \n ? `linear-gradient(90deg, ${gradientStart} 0%, ${gradientEnd} 100%)`\n : gradientStart,\n borderRadius: '2px',\n }}\n />\n </div>\n );\n}\n\n","/**\n * Font Utilities for OG Image Generation\n *\n * Provides dynamic font loading from Google Fonts without requiring files in public/\n * Based on Vercel's official @vercel/og documentation\n */\n\nexport interface FontConfig {\n name: string;\n weight?: 400 | 500 | 600 | 700 | 800 | 900;\n style?: 'normal' | 'italic';\n data: ArrayBuffer;\n}\n\n/**\n * Load a Google Font dynamically\n *\n * @param font - Font family name (e.g., \"Inter\", \"Roboto\", \"Manrope\")\n * @param text - Text to optimize font for (optional, reduces file size)\n * @param weight - Font weight (default: 700)\n * @returns ArrayBuffer of font data\n *\n * @example\n * const fontData = await loadGoogleFont('Manrope', 'Hello World', 700);\n */\nexport async function loadGoogleFont(\n font: string,\n text?: string,\n weight: number = 700\n): Promise<ArrayBuffer> {\n // Construct Google Fonts API URL\n let url = `https://fonts.googleapis.com/css2?family=${font}:wght@${weight}`;\n\n // Add text parameter to optimize font subset (reduces size)\n if (text) {\n url += `&text=${encodeURIComponent(text)}`;\n }\n\n try {\n // Fetch CSS containing font URL\n const css = await fetch(url, {\n headers: {\n // Required to get TTF format instead of WOFF2\n 'User-Agent':\n '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',\n },\n }).then((res) => res.text());\n\n // Extract font URL from CSS\n const resource = css.match(/src: url\\((.+)\\) format\\('(opentype|truetype)'\\)/);\n\n if (!resource || !resource[1]) {\n throw new Error(`Failed to parse font URL from CSS for font: ${font}`);\n }\n\n // Fetch actual font file\n const response = await fetch(resource[1]);\n\n if (response.status !== 200) {\n throw new Error(`Failed to fetch font data: HTTP ${response.status}`);\n }\n\n return await response.arrayBuffer();\n } catch (error) {\n console.error(`Error loading Google Font \"${font}\":`, error);\n throw new Error(`Failed to load font \"${font}\": ${error instanceof Error ? error.message : 'Unknown error'}`);\n }\n}\n\n/**\n * Load multiple Google Fonts\n *\n * @param fonts - Array of font configurations to load\n * @returns Array of FontConfig objects ready for ImageResponse\n *\n * @example\n * const fonts = await loadGoogleFonts([\n * { family: 'Manrope', weight: 700 },\n * { family: 'Inter', weight: 400 }\n * ]);\n */\nexport async function loadGoogleFonts(\n fonts: Array<{\n family: string;\n weight?: 400 | 500 | 600 | 700 | 800 | 900;\n style?: 'normal' | 'italic';\n text?: string;\n }>\n): Promise<FontConfig[]> {\n const fontConfigs = await Promise.all(\n fonts.map(async ({ family, weight = 700, style = 'normal', text }) => {\n const data = await loadGoogleFont(family, text, weight);\n return {\n name: family,\n weight,\n style,\n data,\n };\n })\n );\n\n return fontConfigs;\n}\n\n/**\n * Create a font loader with caching\n *\n * Useful for reusing font data across multiple OG image requests\n *\n * @example\n * const fontLoader = createFontLoader();\n * const font = await fontLoader.load('Manrope', 700);\n */\nexport function createFontLoader() {\n const cache = new Map<string, Promise<ArrayBuffer>>();\n\n return {\n /**\n * Load a font with caching\n */\n async load(\n family: string,\n weight: number = 700,\n text?: string\n ): Promise<ArrayBuffer> {\n const cacheKey = `${family}-${weight}-${text || 'all'}`;\n\n if (!cache.has(cacheKey)) {\n cache.set(cacheKey, loadGoogleFont(family, text, weight));\n }\n\n return cache.get(cacheKey)!;\n },\n\n /**\n * Clear the cache\n */\n clear() {\n cache.clear();\n },\n\n /**\n * Get cache size\n */\n size() {\n return cache.size;\n },\n };\n}\n\n","/**\n * URL Generation Helpers for OG Images\n *\n * Utilities to generate OG image URLs with proper query parameters\n */\n\n/** Default OG Image API base URL */\nconst DEFAULT_OG_IMAGE_BASE_URL = 'https://djangocfg.com/api/og';\n\n/**\n * Encode string to base64 with Unicode support\n * Works in both browser and Node.js environments\n */\nfunction encodeBase64(str: string): string {\n // Node.js environment\n if (typeof Buffer !== 'undefined') {\n return Buffer.from(str, 'utf-8').toString('base64');\n }\n // Browser environment - handle Unicode via UTF-8 encoding\n return btoa(unescape(encodeURIComponent(str)));\n}\n\n/**\n * Decode base64 string with Unicode support\n * Works in both browser, Node.js, and Edge Runtime environments\n */\nfunction decodeBase64(str: string): string {\n // Node.js environment\n if (typeof Buffer !== 'undefined') {\n return Buffer.from(str, 'base64').toString('utf-8');\n }\n // Edge Runtime / Browser environment - handle Unicode via UTF-8 decoding\n // atob is available in Edge Runtime\n try {\n const binaryString = atob(str);\n // Convert binary string to UTF-8\n return decodeURIComponent(\n binaryString\n .split('')\n .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))\n .join('')\n );\n } catch (error) {\n // Fallback to simpler method if above fails\n return decodeURIComponent(escape(atob(str)));\n }\n}\n\n/**\n * OG Image URL parameters\n * All parameters can be encoded in URL via base64\n */\nexport interface OgImageUrlParams {\n /** Page title */\n title: string;\n /** Page description (optional) */\n description?: string;\n /** Site name (optional) */\n siteName?: string;\n /** Logo URL (optional) */\n logo?: string;\n /** Background type: 'gradient' or 'solid' */\n backgroundType?: 'gradient' | 'solid';\n /** Gradient start color (hex) */\n gradientStart?: string;\n /** Gradient end color (hex) */\n gradientEnd?: string;\n /** Background color (for solid type) */\n backgroundColor?: string;\n /** Title font size (px) */\n titleSize?: number;\n /** Title font weight */\n titleWeight?: number;\n /** Title text color */\n titleColor?: string;\n /** Description font size (px) */\n descriptionSize?: number;\n /** Description text color */\n descriptionColor?: string;\n /** Site name font size (px) */\n siteNameSize?: number;\n /** Site name text color */\n siteNameColor?: string;\n /** Padding (px) */\n padding?: number;\n /** Logo size (px) */\n logoSize?: number;\n /** Show logo flag */\n showLogo?: boolean;\n /** Show site name flag */\n showSiteName?: boolean;\n /** Additional custom parameters */\n [key: string]: string | number | boolean | undefined;\n}\n\n/**\n * Options for generating OG image URL\n */\nexport interface GenerateOgImageUrlOptions {\n /**\n * Base URL of the OG image API route\n * @default 'https://djangocfg.com/api/og'\n */\n baseUrl?: string;\n /**\n * If true, encode params as base64 for safer URLs\n * @default true\n */\n useBase64?: boolean;\n}\n\n/**\n * Generate OG image URL with query parameters or base64 encoding\n *\n * @param params - URL parameters for the OG image\n * @param options - Generation options (baseUrl, useBase64)\n * @returns Complete OG image URL with encoded parameters\n *\n * @example\n * ```typescript\n * // Using default baseUrl (https://djangocfg.com/api/og)\n * const url = generateOgImageUrl({\n * title: 'My Page Title',\n * description: 'Page description here',\n * });\n *\n * // With custom baseUrl\n * const url = generateOgImageUrl(\n * { title: 'My Page' },\n * { baseUrl: '/api/og' }\n * );\n * ```\n */\nexport function generateOgImageUrl(\n params: OgImageUrlParams,\n options: GenerateOgImageUrlOptions = {}\n): string {\n const {\n baseUrl = DEFAULT_OG_IMAGE_BASE_URL,\n useBase64 = true\n } = options;\n\n if (useBase64) {\n // Clean params - remove undefined/null/empty values\n const cleanParams: Record<string, string | number | boolean> = {};\n Object.entries(params).forEach(([key, value]) => {\n if (value !== undefined && value !== null && value !== '') {\n cleanParams[key] = value;\n }\n });\n\n // Encode as base64 (Unicode-safe)\n const jsonString = JSON.stringify(cleanParams);\n const base64Data = encodeBase64(jsonString);\n\n // CRITICAL: Use path parameter instead of query parameter\n // Next.js strips query params in internal requests for metadata generation\n // Using /api/og/[data] instead of /api/og?data=... preserves the data\n // IMPORTANT: Add trailing slash to avoid 308 redirects which cause timeouts in crawlers\n return `${baseUrl}/${base64Data}/`;\n } else {\n // Legacy query params mode\n const searchParams = new URLSearchParams();\n\n // Add all defined parameters\n Object.entries(params).forEach(([key, value]) => {\n if (value !== undefined && value !== null && value !== '') {\n searchParams.append(key, String(value));\n }\n });\n\n const query = searchParams.toString();\n return query ? `${baseUrl}?${query}` : baseUrl;\n }\n}\n\n/**\n * Get absolute OG image URL from relative path\n *\n * Useful for generating absolute URLs required by Open Graph meta tags\n *\n * @param relativePath - Relative OG image path (e.g., '/api/og?title=Hello')\n * @param siteUrl - Base site URL (e.g., 'https://example.com')\n * @returns Absolute URL\n *\n * @example\n * ```typescript\n * const absolute = getAbsoluteOgImageUrl(\n * '/api/og?title=Hello',\n * 'https://example.com'\n * );\n * // Result: https://example.com/api/og?title=Hello\n * ```\n */\nexport function getAbsoluteOgImageUrl(\n relativePath: string,\n siteUrl: string\n): string {\n // If path is already an absolute URL, return as-is\n if (relativePath.startsWith('http://') || relativePath.startsWith('https://')) {\n return relativePath;\n }\n\n // Remove trailing slash from site URL\n const cleanSiteUrl = siteUrl.replace(/\\/$/, '');\n\n // Ensure relative path starts with /\n const cleanPath = relativePath.startsWith('/')\n ? relativePath\n : `/${relativePath}`;\n\n return `${cleanSiteUrl}${cleanPath}`;\n}\n\n/**\n * Create OG image URL builder with preset configuration\n *\n * Useful when you want to reuse the same base URL and default parameters\n *\n * @param defaults - Default parameters to merge with each URL generation\n * @param options - Default options (baseUrl, useBase64)\n * @returns URL builder function\n *\n * @example\n * ```typescript\n * const buildOgUrl = createOgImageUrlBuilder(\n * { siteName: 'My Site', logo: '/logo.png' },\n * { baseUrl: '/api/og' }\n * );\n *\n * const url1 = buildOgUrl({ title: 'Page 1' });\n * const url2 = buildOgUrl({ title: 'Page 2', description: 'Custom desc' });\n * ```\n */\nexport function createOgImageUrlBuilder(\n defaults: Partial<OgImageUrlParams> = {},\n options: GenerateOgImageUrlOptions = {}\n) {\n return (params: OgImageUrlParams): string => {\n return generateOgImageUrl(\n { ...defaults, ...params },\n options\n );\n };\n}\n\n/**\n * Parse OG image URL parameters from a URL string (legacy query params)\n *\n * @param url - Full or relative URL with query parameters\n * @returns Parsed parameters object\n *\n * @example\n * ```typescript\n * const params = parseOgImageUrl('/api/og?title=Hello&description=World');\n * // Result: { title: 'Hello', description: 'World' }\n * ```\n */\nexport function parseOgImageUrl(url: string): Record<string, string> {\n try {\n const urlObj = new URL(url, 'http://dummy.com');\n const params: Record<string, string> = {};\n\n urlObj.searchParams.forEach((value, key) => {\n params[key] = value;\n });\n\n return params;\n } catch {\n return {};\n }\n}\n\n/**\n * Parse OG image data from base64-encoded query parameter\n *\n * Use this in your API route to decode the `data` parameter\n * Supports both base64 (new) and legacy query params format\n *\n * @param searchParams - URL search params or request object\n * @returns Parsed OG image parameters\n *\n * @example\n * ```typescript\n * // In Next.js API route (pages/api/og.ts)\n * export default function handler(req) {\n * const params = parseOgImageData(req.query);\n * // { title: 'Hello', description: 'World' }\n * }\n *\n * // In Next.js App Router (app/api/og/route.ts)\n * export async function GET(request: Request) {\n * const { searchParams } = new URL(request.url);\n * const params = parseOgImageData(Object.fromEntries(searchParams));\n * // { title: 'Hello', description: 'World' }\n * }\n * ```\n */\nexport function parseOgImageData(\n searchParams: Record<string, string | string[] | undefined> | URLSearchParams\n): Record<string, string> {\n try {\n // Handle URLSearchParams\n let params: Record<string, string | undefined>;\n\n if (searchParams instanceof URLSearchParams) {\n // Convert URLSearchParams to object\n params = {};\n for (const [key, value] of searchParams.entries()) {\n params[key] = value;\n }\n } else {\n params = searchParams as Record<string, string | undefined>;\n }\n\n // Debug logging\n if (process.env.NODE_ENV === 'development') {\n console.log('[parseOgImageData] Input params keys:', Object.keys(params));\n console.log('[parseOgImageData] Input params:', params);\n }\n\n // Check for base64-encoded data parameter\n const dataParam = params.data;\n if (dataParam && typeof dataParam === 'string' && dataParam.trim() !== '') {\n if (process.env.NODE_ENV === 'development') {\n console.log('[parseOgImageData] Found data param, length:', dataParam.length);\n }\n\n try {\n const decoded = decodeBase64(dataParam);\n if (process.env.NODE_ENV === 'development') {\n console.log('[parseOgImageData] Decoded string:', decoded.substring(0, 100));\n }\n\n const parsed = JSON.parse(decoded);\n if (process.env.NODE_ENV === 'development') {\n console.log('[parseOgImageData] Parsed JSON:', parsed);\n }\n\n // Ensure all values are strings\n const result: Record<string, string> = {};\n for (const [key, value] of Object.entries(parsed)) {\n if (value !== undefined && value !== null) {\n result[key] = String(value);\n }\n }\n\n if (process.env.NODE_ENV === 'development') {\n console.log('[parseOgImageData] Result:', result);\n }\n\n return result;\n } catch (decodeError) {\n console.error('[parseOgImageData] Error decoding/parsing data param:', decodeError);\n if (decodeError instanceof Error) {\n console.error('[parseOgImageData] Error message:', decodeError.message);\n }\n // Fall through to legacy query params\n }\n } else {\n if (process.env.NODE_ENV === 'development') {\n console.log('[parseOgImageData] No data param found or empty');\n }\n }\n\n // Fallback to legacy query params format\n const result: Record<string, string> = {};\n for (const [key, value] of Object.entries(params)) {\n if (key !== 'data' && value !== undefined && value !== null) {\n result[key] = Array.isArray(value) ? value[0] : String(value);\n }\n }\n\n if (process.env.NODE_ENV === 'development') {\n console.log('[parseOgImageData] Fallback result:', result);\n }\n\n return result;\n } catch (error) {\n console.error('[parseOgImageData] Unexpected error:', error);\n return {};\n }\n}\n\n// Export base64 utilities for advanced use cases\nexport { encodeBase64, decodeBase64 };\n","/**\n * Metadata Utilities for OG Images\n *\n * Helpers to automatically add og:image to Next.js metadata\n */\n\nimport type { Metadata } from 'next';\nimport { generateOgImageUrl, getAbsoluteOgImageUrl, OgImageUrlParams } from './url';\n\n/**\n * Options for generating OG image metadata\n */\nexport interface AppMetadataOptions {\n /** Base URL of the OG image API route (e.g., '/api/og') */\n ogImageBaseUrl?: string;\n /** Site URL for absolute URLs (e.g., 'https://example.com') */\n siteUrl?: string;\n /** Default parameters to merge with page-specific params */\n defaultParams?: Partial<OgImageUrlParams>;\n /** Whether to use base64 encoding (default: true) */\n useBase64?: boolean;\n /** Favicon URL (e.g., '/favicon.png') - automatically added to metadata.icons */\n favicon?: string;\n /** Apple touch icon URL (e.g., '/apple-icon.png') - automatically added to metadata.icons */\n appleIcon?: string;\n}\n\n/**\n * Extract title from metadata\n */\nfunction extractTitle(metadata: Metadata): string {\n if (typeof metadata.title === 'string') {\n return metadata.title;\n }\n if (metadata.title) {\n if ('default' in metadata.title) {\n return metadata.title.default;\n }\n if ('absolute' in metadata.title) {\n return metadata.title.absolute;\n }\n }\n return '';\n}\n\n/**\n * Extract description from metadata\n */\nfunction extractDescription(metadata: Metadata): string {\n if (typeof metadata.description === 'string') {\n return metadata.description;\n }\n return '';\n}\n\n/**\n * Generate Next.js metadata with OG image\n *\n * Automatically adds og:image, twitter:image, and other OG meta tags\n * Automatically extracts title and description from metadata if not provided\n *\n * @param metadata - Base metadata object\n * @param ogImageParams - Optional parameters for OG image generation (if not provided, extracted from metadata)\n * @param options - Configuration options\n * @returns Enhanced metadata with OG image\n *\n * @example\n * ```typescript\n * // In page.tsx or layout.tsx\n * import { generateAppMetadata } from '@djangocfg/nextjs/og-image';\n * import { settings } from '@/core/settings';\n *\n * export const metadata = generateAppMetadata(\n * {\n * title: 'My Page',\n * description: 'Page description',\n * },\n * undefined, // Will auto-extract from metadata\n * {\n * ogImageBaseUrl: '/api/og',\n * siteUrl: settings.app.siteUrl,\n * favicon: settings.app.icons.favicon,\n * appleIcon: settings.app.icons.logo192,\n * defaultParams: {\n * siteName: settings.app.name,\n * logo: settings.app.icons.logoVector,\n * },\n * }\n * );\n * ```\n */\n/**\n * Get site URL automatically from environment\n * Priority: NEXT_PUBLIC_SITE_URL > VERCEL_URL > fallback\n */\nfunction getSiteUrl(): string {\n // Try NEXT_PUBLIC_SITE_URL first (most reliable)\n if (typeof process !== 'undefined' && process.env.NEXT_PUBLIC_SITE_URL) {\n return process.env.NEXT_PUBLIC_SITE_URL;\n }\n \n // Development fallback\n return '';\n}\n\nexport function generateAppMetadata(\n metadata: Metadata,\n ogImageParams?: Partial<OgImageUrlParams>,\n options: AppMetadataOptions = {}\n): Metadata {\n const {\n ogImageBaseUrl = 'https://djangocfg.com/api/og',\n siteUrl: providedSiteUrl,\n defaultParams = {},\n useBase64 = true,\n favicon,\n appleIcon,\n } = options;\n\n // Automatically determine siteUrl if not provided or is undefined\n const siteUrl = providedSiteUrl && providedSiteUrl !== 'undefined' \n ? providedSiteUrl \n : getSiteUrl();\n\n // Auto-extract title and description from metadata if not provided\n const extractedTitle = extractTitle(metadata);\n const extractedDescription = extractDescription(metadata);\n\n // Merge with provided params (provided params take precedence)\n const finalOgImageParams: OgImageUrlParams = {\n ...defaultParams,\n title: ogImageParams?.title || extractedTitle || defaultParams.title || '',\n description: ogImageParams?.description || extractedDescription || defaultParams.description || '',\n ...ogImageParams,\n };\n\n // Get alt text for image (title or siteName as fallback)\n const imageAlt = finalOgImageParams.title || finalOgImageParams.siteName;\n\n // Generate relative OG image URL\n const relativeOgImageUrl = generateOgImageUrl(\n finalOgImageParams,\n { baseUrl: ogImageBaseUrl, useBase64 }\n );\n\n // CRITICAL: Use absolute URL to ensure query params are preserved\n // Next.js might strip query params from relative URLs in some cases\n // Absolute URLs ensure the full URL with params is used\n const ogImageUrl = siteUrl\n ? getAbsoluteOgImageUrl(relativeOgImageUrl, siteUrl)\n : relativeOgImageUrl;\n\n // Normalize existing images to arrays\n const existingOgImages = metadata.openGraph?.images\n ? Array.isArray(metadata.openGraph.images)\n ? metadata.openGraph.images\n : [metadata.openGraph.images]\n : [];\n\n const existingTwitterImages = metadata.twitter?.images\n ? Array.isArray(metadata.twitter.images)\n ? metadata.twitter.images\n : [metadata.twitter.images]\n : [];\n\n // Build final metadata object\n const finalMetadata: Metadata = {\n ...metadata,\n openGraph: {\n ...metadata.openGraph,\n images: [\n ...existingOgImages,\n {\n url: ogImageUrl,\n width: 1200,\n height: 630,\n alt: imageAlt,\n },\n ],\n },\n twitter: {\n ...metadata.twitter,\n card: 'summary_large_image',\n images: [\n ...existingTwitterImages,\n {\n url: ogImageUrl,\n alt: imageAlt,\n },\n ],\n },\n };\n\n // Automatically add metadataBase if siteUrl is an absolute URL\n // metadataBase requires absolute URL - skip if siteUrl is relative path (like /cfg/admin)\n // Only add if not already set in input metadata\n if (!finalMetadata.metadataBase && siteUrl) {\n // Check if siteUrl is an absolute URL (starts with http:// or https://)\n if (siteUrl.startsWith('http://') || siteUrl.startsWith('https://')) {\n try {\n finalMetadata.metadataBase = new URL(siteUrl);\n } catch (e) {\n // If URL construction fails, skip metadataBase\n // This shouldn't happen if we check for http/https, but just in case\n }\n }\n }\n\n // Add favicon and apple icon if provided\n if (favicon || appleIcon) {\n // metadata.icons can be string, array, or object - only spread if it's an object\n const existingIcons = metadata.icons && typeof metadata.icons === 'object' && !Array.isArray(metadata.icons)\n ? metadata.icons\n : {};\n finalMetadata.icons = {\n ...existingIcons,\n ...(favicon && { icon: favicon }),\n ...(appleIcon && { apple: appleIcon }),\n };\n }\n\n return finalMetadata;\n}\n\n/**\n * Create OG image metadata generator with preset configuration\n *\n * Useful when you want to reuse the same configuration across multiple pages\n *\n * @param options - Configuration options\n * @returns Metadata generator function\n *\n * @example\n * ```typescript\n * // In a shared file (e.g., lib/metadata.ts)\n * import { createAppMetadataGenerator } from '@djangocfg/nextjs/og-image';\n * import { settings } from '@/core/settings';\n *\n * export const generateMetadata = createAppMetadataGenerator({\n * ogImageBaseUrl: '/api/og',\n * siteUrl: settings.app.siteUrl,\n * favicon: settings.app.icons.favicon,\n * appleIcon: settings.app.icons.logo192,\n * defaultParams: {\n * siteName: settings.app.name,\n * logo: settings.app.icons.logoVector,\n * },\n * });\n *\n * // In page.tsx\n * import { generateMetadata } from '@/lib/metadata';\n *\n * export const metadata = generateMetadata({\n * title: 'My Page',\n * description: 'Description',\n * });\n * ```\n */\nexport function createAppMetadataGenerator(\n options: AppMetadataOptions\n) {\n return (\n metadata: Metadata,\n ogImageParams?: Partial<OgImageUrlParams>\n ): Metadata => {\n return generateAppMetadata(metadata, ogImageParams, options);\n };\n}\n\n"],"mappings":"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","names":["result","jsx"]}
1
+ {"version":3,"sources":["../../src/og-image/url.ts","../../src/og-image/metadata.ts"],"sourcesContent":["import type { OGImageParams } from './types'\n\n/**\n * Resolves the base URL for og:image meta tags.\n * Must be publicly accessible by crawlers (Twitter, Facebook, etc).\n *\n * Priority:\n * 1. NEXT_PUBLIC_MEDIA_URL — explicit media domain (nginx proxy or CDN)\n * 2. NEXT_PUBLIC_API_URL — direct Django API domain\n * 3. NEXT_PUBLIC_SITE_URL — site domain (same-domain/static build)\n * 4. '' — relative URL (last resort)\n *\n * Use cases:\n * - Same domain (static build): API_URL='' → relative /cfg/og/...\n * - Split domain (standalone): API_URL=https://api.x.com → absolute\n * - Media via nginx proxy: MEDIA_URL=https://x.com, API_URL=https://api.x.com\n * - CDN: MEDIA_URL=https://cdn.x.com\n */\nfunction resolveOgPublicBase(): string {\n if (typeof process === 'undefined') return ''\n return (\n process.env.NEXT_PUBLIC_MEDIA_URL?.replace(/\\/$/, '') ??\n process.env.NEXT_PUBLIC_API_URL?.replace(/\\/$/, '') ??\n process.env.NEXT_PUBLIC_SITE_URL?.replace(/\\/$/, '') ??\n ''\n )\n}\n\nfunction encodeBase64(str: string): string {\n if (typeof Buffer !== 'undefined') {\n return Buffer.from(str).toString('base64url')\n }\n // Browser fallback\n return btoa(unescape(encodeURIComponent(str)))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '')\n}\n\nfunction cleanParams(params: OGImageParams): Record<string, unknown> {\n return Object.fromEntries(\n Object.entries(params).filter(\n ([, v]) => v !== undefined && v !== null && v !== ''\n )\n )\n}\n\n/**\n * Builds an absolute (or relative) OG image URL pointing to Django's\n * django_ogimage endpoint: /cfg/og/<base64params>/\n *\n * The URL is suitable for use in og:image and twitter:image meta tags.\n */\nexport function buildOgUrl(params: OGImageParams): string {\n const b64 = encodeBase64(JSON.stringify(cleanParams(params)))\n const base = resolveOgPublicBase()\n return `${base}/cfg/og/${b64}/`\n}\n","import type { Metadata } from 'next'\nimport type { OGImageParams } from './types'\nimport { buildOgUrl } from './url'\n\n/**\n * Creates Next.js Metadata fragment with og:image and twitter:image\n * pointing to Django's django_ogimage renderer.\n *\n * Usage in generateMetadata():\n * return createOgMetadata({ title: 'Page', preset: 'DARK_BLUE' })\n */\nexport function createOgMetadata(params: OGImageParams): Metadata {\n const url = buildOgUrl(params)\n const image = { url, width: 1200, height: 630, alt: params.title }\n return {\n openGraph: {\n images: [image],\n },\n twitter: {\n card: 'summary_large_image',\n images: [url],\n },\n }\n}\n\n/**\n * Merges og:image metadata into an existing Metadata object.\n *\n * Usage:\n * return withOgImage(\n * { title: 'Page', description: '...' },\n * { preset: 'DARK_BLUE', layout: 'HERO' }\n * )\n */\nexport function withOgImage(\n metadata: Metadata,\n params: OGImageParams & { title?: string }\n): Metadata {\n // Auto-extract title from metadata if not in params\n const resolvedParams: OGImageParams = {\n title: extractTitle(metadata),\n ...params,\n }\n\n const ogMeta = createOgMetadata(resolvedParams)\n\n return {\n ...metadata,\n openGraph: {\n ...metadata.openGraph,\n ...ogMeta.openGraph,\n },\n twitter: {\n ...metadata.twitter,\n ...ogMeta.twitter,\n },\n }\n}\n\nfunction extractTitle(metadata: Metadata): string {\n const t = metadata.title\n if (!t) return ''\n if (typeof t === 'string') return t\n if (typeof t === 'object' && 'absolute' in t) return (t as { absolute: string }).absolute\n if (typeof t === 'object' && 'default' in t) return (t as { default: string }).default\n return ''\n}\n"],"mappings":";AAkBA,SAAS,sBAA8B;AACrC,MAAI,OAAO,YAAY,YAAa,QAAO;AAC3C,SACE,QAAQ,IAAI,uBAAuB,QAAQ,OAAO,EAAE,KACpD,QAAQ,IAAI,qBAAqB,QAAQ,OAAO,EAAE,KAClD,QAAQ,IAAI,sBAAsB,QAAQ,OAAO,EAAE,KACnD;AAEJ;AAEA,SAAS,aAAa,KAAqB;AACzC,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO,OAAO,KAAK,GAAG,EAAE,SAAS,WAAW;AAAA,EAC9C;AAEA,SAAO,KAAK,SAAS,mBAAmB,GAAG,CAAC,CAAC,EAC1C,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE;AACtB;AAEA,SAAS,YAAY,QAAgD;AACnE,SAAO,OAAO;AAAA,IACZ,OAAO,QAAQ,MAAM,EAAE;AAAA,MACrB,CAAC,CAAC,EAAE,CAAC,MAAM,MAAM,UAAa,MAAM,QAAQ,MAAM;AAAA,IACpD;AAAA,EACF;AACF;AAQO,SAAS,WAAW,QAA+B;AACxD,QAAM,MAAM,aAAa,KAAK,UAAU,YAAY,MAAM,CAAC,CAAC;AAC5D,QAAM,OAAO,oBAAoB;AACjC,SAAO,GAAG,IAAI,WAAW,GAAG;AAC9B;;;AC9CO,SAAS,iBAAiB,QAAiC;AAChE,QAAM,MAAM,WAAW,MAAM;AAC7B,QAAM,QAAQ,EAAE,KAAK,OAAO,MAAM,QAAQ,KAAK,KAAK,OAAO,MAAM;AACjE,SAAO;AAAA,IACL,WAAW;AAAA,MACT,QAAQ,CAAC,KAAK;AAAA,IAChB;AAAA,IACA,SAAS;AAAA,MACP,MAAM;AAAA,MACN,QAAQ,CAAC,GAAG;AAAA,IACd;AAAA,EACF;AACF;AAWO,SAAS,YACd,UACA,QACU;AAEV,QAAM,iBAAgC;AAAA,IACpC,OAAO,aAAa,QAAQ;AAAA,IAC5B,GAAG;AAAA,EACL;AAEA,QAAM,SAAS,iBAAiB,cAAc;AAE9C,SAAO;AAAA,IACL,GAAG;AAAA,IACH,WAAW;AAAA,MACT,GAAG,SAAS;AAAA,MACZ,GAAG,OAAO;AAAA,IACZ;AAAA,IACA,SAAS;AAAA,MACP,GAAG,SAAS;AAAA,MACZ,GAAG,OAAO;AAAA,IACZ;AAAA,EACF;AACF;AAEA,SAAS,aAAa,UAA4B;AAChD,QAAM,IAAI,SAAS;AACnB,MAAI,CAAC,EAAG,QAAO;AACf,MAAI,OAAO,MAAM,SAAU,QAAO;AAClC,MAAI,OAAO,MAAM,YAAY,cAAc,EAAG,QAAQ,EAA2B;AACjF,MAAI,OAAO,MAAM,YAAY,aAAa,EAAG,QAAQ,EAA0B;AAC/E,SAAO;AACT;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/nextjs",
3
- "version": "2.1.225",
3
+ "version": "2.1.226",
4
4
  "description": "Next.js server utilities: sitemap, health, OG images, contact forms, navigation, config",
5
5
  "keywords": [
6
6
  "nextjs",
@@ -44,21 +44,6 @@
44
44
  "import": "./dist/health/index.mjs",
45
45
  "default": "./dist/health/index.mjs"
46
46
  },
47
- "./og-image": {
48
- "types": "./dist/og-image/index.d.mts",
49
- "import": "./dist/og-image/index.mjs",
50
- "default": "./dist/og-image/index.mjs"
51
- },
52
- "./og-image/utils": {
53
- "types": "./dist/og-image/utils/index.d.mts",
54
- "import": "./dist/og-image/utils/index.mjs",
55
- "default": "./dist/og-image/utils/index.mjs"
56
- },
57
- "./og-image/components": {
58
- "types": "./dist/og-image/components/index.d.mts",
59
- "import": "./dist/og-image/components/index.mjs",
60
- "default": "./dist/og-image/components/index.mjs"
61
- },
62
47
  "./navigation": {
63
48
  "types": "./dist/navigation/index.d.mts",
64
49
  "import": "./dist/navigation/index.mjs",
@@ -89,6 +74,11 @@
89
74
  "import": "./dist/pwa/worker/index.mjs",
90
75
  "default": "./dist/pwa/worker/index.mjs"
91
76
  },
77
+ "./og-image": {
78
+ "types": "./dist/og-image/index.d.mts",
79
+ "import": "./dist/og-image/index.mjs",
80
+ "default": "./dist/og-image/index.mjs"
81
+ },
92
82
  "./monitor": {
93
83
  "types": "./dist/monitor/index.d.mts",
94
84
  "import": "./dist/monitor/index.mjs",
@@ -153,9 +143,9 @@
153
143
  "ai-docs": "tsx src/ai/cli.ts"
154
144
  },
155
145
  "peerDependencies": {
156
- "@djangocfg/i18n": "^2.1.225",
157
- "@djangocfg/monitor": "^2.1.225",
158
- "@djangocfg/ui-core": "^2.1.225",
146
+ "@djangocfg/i18n": "^2.1.226",
147
+ "@djangocfg/monitor": "^2.1.226",
148
+ "@djangocfg/ui-core": "^2.1.226",
159
149
  "next": "^16.0.10"
160
150
  },
161
151
  "peerDependenciesMeta": {
@@ -177,17 +167,16 @@
177
167
  "serwist": "^9.2.3"
178
168
  },
179
169
  "devDependencies": {
180
- "@djangocfg/i18n": "^2.1.225",
181
- "@djangocfg/monitor": "^2.1.225",
182
- "@djangocfg/ui-core": "^2.1.225",
183
- "@djangocfg/layouts": "^2.1.225",
184
- "@djangocfg/typescript-config": "^2.1.225",
170
+ "@djangocfg/i18n": "^2.1.226",
171
+ "@djangocfg/monitor": "^2.1.226",
172
+ "@djangocfg/ui-core": "^2.1.226",
173
+ "@djangocfg/layouts": "^2.1.226",
174
+ "@djangocfg/typescript-config": "^2.1.226",
185
175
  "@types/node": "^24.7.2",
186
176
  "@types/react": "^19.1.0",
187
177
  "@types/react-dom": "^19.1.0",
188
178
  "@types/semver": "^7.7.1",
189
179
  "@types/webpack": "^5.28.5",
190
- "@vercel/og": "^0.8.5",
191
180
  "eslint": "^9.37.0",
192
181
  "lucide-react": "^0.545.0",
193
182
  "tsup": "^8.0.1",
package/src/index.ts CHANGED
@@ -10,9 +10,6 @@ export * from './sitemap';
10
10
  // Health
11
11
  export * from './health';
12
12
 
13
- // OG Image
14
- export * from './og-image';
15
-
16
13
  // Navigation
17
14
  export * from './navigation';
18
15
 
@@ -1,66 +1,155 @@
1
- # OG Image Metadata Helper
1
+ # @djangocfg/nextjs/og-image
2
2
 
3
- Automatically add `og:image` to Next.js metadata.
3
+ Typed URL builder for Django's `django_ogimage` renderer.
4
4
 
5
- ## Usage
5
+ **No Edge Runtime. No `@vercel/og`. No JSX templates.**
6
+ Django renders and caches the PNG — Next.js only builds the URL and injects it into metadata.
6
7
 
7
- ### In layout.tsx or page.tsx
8
+ ---
9
+
10
+ ## How It Works
11
+
12
+ ```
13
+ generateMetadata() / withOgImage()
14
+
15
+ buildOgUrl(params) — base64-encodes OGImageParams
16
+
17
+ resolveOgPublicBase() — picks the right base URL from env vars
18
+
19
+ <meta og:image> = "{base}/cfg/og/{b64}/"
20
+
21
+ Crawler/browser hits that URL
22
+
23
+ Django OGImageRenderView — decodes params, renders PNG via PicTex
24
+
25
+ FileResponse (cached in MEDIA_ROOT/ogimage/<sharded>/<key>.png)
26
+ ```
27
+
28
+ ---
29
+
30
+ ## URL Resolution
31
+
32
+ The public base URL (used in `og:image` meta tags) is resolved in priority order:
33
+
34
+ | Priority | Variable | Use case |
35
+ |---|---|---|
36
+ | 1 | `NEXT_PUBLIC_MEDIA_URL` | nginx proxies `/media/` from `SITE_URL` to `API_URL`; or CDN |
37
+ | 2 | `NEXT_PUBLIC_API_URL` | direct split-domain (Next.js ≠ Django domain) |
38
+ | 3 | `NEXT_PUBLIC_SITE_URL` | same-domain / static build |
39
+ | 4 | `''` (empty) | relative URLs (last resort) |
40
+
41
+ **Why not always use `API_URL`?**
42
+ Crawlers (Twitter, Facebook, Slack) must reach the OG image URL publicly. When nginx proxies `/media/` through the frontend domain, `API_URL` is internal — the crawler can't reach it. `MEDIA_URL` solves this explicitly.
43
+
44
+ ### Deployment examples
45
+
46
+ ```bash
47
+ # Same-domain static build (Django serves everything)
48
+ NEXT_PUBLIC_SITE_URL=https://example.com
49
+ NEXT_PUBLIC_API_URL= # empty → relative /cfg/og/...
50
+
51
+ # Split-domain standalone build
52
+ NEXT_PUBLIC_SITE_URL=https://example.com
53
+ NEXT_PUBLIC_API_URL=https://api.example.com
54
+ # → og:image = https://api.example.com/cfg/og/<b64>/
55
+
56
+ # Media proxied through site domain (nginx /media/ → api/media/)
57
+ NEXT_PUBLIC_SITE_URL=https://example.com
58
+ NEXT_PUBLIC_API_URL=https://api.example.com
59
+ NEXT_PUBLIC_MEDIA_URL=https://example.com # ← crawler hits example.com/cfg/og/...
60
+
61
+ # CDN
62
+ NEXT_PUBLIC_MEDIA_URL=https://cdn.example.com
63
+ ```
64
+
65
+ ---
66
+
67
+ ## API
68
+
69
+ ### `buildOgUrl(params)`
70
+
71
+ Builds the raw OG image URL string. Useful when you need the URL directly.
72
+
73
+ ```typescript
74
+ import { buildOgUrl } from '@djangocfg/nextjs/og-image'
75
+
76
+ const url = buildOgUrl({ title: 'Hello', preset: 'DARK_BLUE' })
77
+ // → "https://api.example.com/cfg/og/eyJ0aXRsZSI6Ikhlb.../"
78
+ ```
79
+
80
+ ### `createOgMetadata(params)`
81
+
82
+ Returns a Next.js `Metadata` fragment with `openGraph.images` and `twitter.images`.
8
83
 
9
84
  ```typescript
10
- import type { Metadata } from 'next';
11
- import { generateAppMetadata } from '@djangocfg/nextjs/og-image';
12
- import { settings } from '@core/settings';
13
-
14
- export const metadata: Metadata = generateAppMetadata(
15
- {
16
- title: 'My Page',
17
- description: 'Page description',
18
- },
19
- {
20
- title: 'My Page',
21
- description: 'Page description',
22
- },
23
- {
24
- ogImageBaseUrl: '/api/og',
25
- siteUrl: settings.app.siteUrl,
26
- defaultParams: {
27
- siteName: settings.app.name,
28
- logo: settings.app.icons.logoVector,
29
- },
30
- }
31
- );
85
+ import { createOgMetadata } from '@djangocfg/nextjs/og-image'
86
+
87
+ export async function generateMetadata() {
88
+ return createOgMetadata({ title: 'Page', preset: 'DARK_BLUE' })
89
+ }
90
+ // → { openGraph: { images: [{ url, width: 1200, height: 630 }] }, twitter: { ... } }
32
91
  ```
33
92
 
34
- ### With Generator (Recommended)
93
+ ### `withOgImage(metadata, params)`
94
+
95
+ Merges OG image into an existing `Metadata` object. Auto-extracts `title` from metadata if not in params.
35
96
 
36
97
  ```typescript
37
- // lib/metadata.ts
38
- import { createAppMetadataGenerator } from '@djangocfg/nextjs/og-image';
39
- import { settings } from '@core/settings';
40
-
41
- export const generateMetadata = createAppMetadataGenerator({
42
- ogImageBaseUrl: '/api/og',
43
- siteUrl: settings.app.siteUrl,
44
- defaultParams: {
45
- siteName: settings.app.name,
46
- logo: settings.app.icons.logoVector,
47
- },
48
- });
49
-
50
- // page.tsx
51
- import { generateMetadata } from '@/lib/metadata';
52
-
53
- export const metadata = generateMetadata(
54
- { title: 'My Page', description: 'Description' },
55
- { title: 'My Page', description: 'Description' }
56
- );
98
+ import { withOgImage } from '@djangocfg/nextjs/og-image'
99
+
100
+ export async function generateMetadata(): Promise<Metadata> {
101
+ return withOgImage(
102
+ { title: 'Product Page', description: 'Buy now' },
103
+ { preset: 'DARK_BLUE', layout: 'HERO' }
104
+ )
105
+ }
57
106
  ```
58
107
 
59
- ## What It Does
108
+ ---
109
+
110
+ ## OGImageParams
111
+
112
+ Mirrors Django's `OGImageParams` exactly:
113
+
114
+ ```typescript
115
+ interface OGImageParams {
116
+ title: string // required
117
+ description?: string
118
+ locale?: string // for CJK/RTL font selection
119
+ style?: 'dark' | 'light'
120
+
121
+ // Color overrides (hex, e.g. "#1a1a2e")
122
+ bg_color?: string
123
+ bg_color2?: string // gradient second color
124
+ text_color?: string
125
+ accent_color?: string
126
+
127
+ size?: string // "1200x630" (default)
128
+ preset?: OGPreset // see below
129
+ layout?: OGLayout // see below
130
+ page_url?: string // excluded from Django's cache key
131
+ }
132
+ ```
133
+
134
+ ### Presets (`OGPreset`)
135
+
136
+ Dark: `DARK` `DARK_BLUE` `DARK_PURPLE` `DARK_GREEN` `DARK_ROSE`
137
+ Light: `LIGHT` `LIGHT_GRAY` `LIGHT_WARM` `LIGHT_GREEN`
138
+
139
+ ### Layouts (`OGLayout`)
140
+
141
+ | Value | Description |
142
+ |---|---|
143
+ | `DEFAULT` | Accent bar top, brand bottom-left, content centered |
144
+ | `HERO` | Accent bar top, brand top-left, content bottom — larger fonts |
145
+ | `ARTICLE` | No accent, brand top-left, content centered |
146
+ | `MINIMAL` | No accent, no brand, pure content |
147
+
148
+ ---
149
+
150
+ ## Django side
60
151
 
61
- Automatically adds to HTML:
62
- - `<meta property="og:image" content="...">`
63
- - `<meta name="twitter:image" content="...">`
64
- - `<meta name="twitter:card" content="summary_large_image">`
152
+ The endpoint is registered by `django_ogimage` at `/cfg/og/<b64params>/`.
153
+ Enable it by including the URLs in your Django project (handled automatically by `django-cfg`).
65
154
 
66
- The URL is generated based on parameters and automatically added to metadata.
155
+ Cache lives at `MEDIA_ROOT/ogimage/<key[:2]>/<key[2:4]>/<key>.png` sharded, `Cache-Control: public, max-age=86400`.
@@ -1,27 +1,3 @@
1
- /**
2
- * OG Image exports
3
- */
4
-
5
- export { createOgImageHandler, createOgImageDynamicRoute } from './route';
6
- export type { OgImageHandlerConfig } from './route';
7
- export type { OgImageTemplateProps } from './types';
8
-
9
- export { DefaultTemplate, LightTemplate } from './components';
10
-
11
- export {
12
- loadGoogleFont,
13
- loadGoogleFonts,
14
- createFontLoader,
15
- generateOgImageUrl,
16
- getAbsoluteOgImageUrl,
17
- createOgImageUrlBuilder,
18
- parseOgImageUrl,
19
- parseOgImageData,
20
- encodeBase64,
21
- decodeBase64,
22
- generateAppMetadata,
23
- createAppMetadataGenerator,
24
- type FontConfig,
25
- type OgImageUrlParams,
26
- type AppMetadataOptions,
27
- } from './utils';
1
+ export type { OGImageParams, OGPreset, OGLayout } from './types'
2
+ export { buildOgUrl } from './url'
3
+ export { createOgMetadata, withOgImage } from './metadata'
@@ -0,0 +1,67 @@
1
+ import type { Metadata } from 'next'
2
+ import type { OGImageParams } from './types'
3
+ import { buildOgUrl } from './url'
4
+
5
+ /**
6
+ * Creates Next.js Metadata fragment with og:image and twitter:image
7
+ * pointing to Django's django_ogimage renderer.
8
+ *
9
+ * Usage in generateMetadata():
10
+ * return createOgMetadata({ title: 'Page', preset: 'DARK_BLUE' })
11
+ */
12
+ export function createOgMetadata(params: OGImageParams): Metadata {
13
+ const url = buildOgUrl(params)
14
+ const image = { url, width: 1200, height: 630, alt: params.title }
15
+ return {
16
+ openGraph: {
17
+ images: [image],
18
+ },
19
+ twitter: {
20
+ card: 'summary_large_image',
21
+ images: [url],
22
+ },
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Merges og:image metadata into an existing Metadata object.
28
+ *
29
+ * Usage:
30
+ * return withOgImage(
31
+ * { title: 'Page', description: '...' },
32
+ * { preset: 'DARK_BLUE', layout: 'HERO' }
33
+ * )
34
+ */
35
+ export function withOgImage(
36
+ metadata: Metadata,
37
+ params: OGImageParams & { title?: string }
38
+ ): Metadata {
39
+ // Auto-extract title from metadata if not in params
40
+ const resolvedParams: OGImageParams = {
41
+ title: extractTitle(metadata),
42
+ ...params,
43
+ }
44
+
45
+ const ogMeta = createOgMetadata(resolvedParams)
46
+
47
+ return {
48
+ ...metadata,
49
+ openGraph: {
50
+ ...metadata.openGraph,
51
+ ...ogMeta.openGraph,
52
+ },
53
+ twitter: {
54
+ ...metadata.twitter,
55
+ ...ogMeta.twitter,
56
+ },
57
+ }
58
+ }
59
+
60
+ function extractTitle(metadata: Metadata): string {
61
+ const t = metadata.title
62
+ if (!t) return ''
63
+ if (typeof t === 'string') return t
64
+ if (typeof t === 'object' && 'absolute' in t) return (t as { absolute: string }).absolute
65
+ if (typeof t === 'object' && 'default' in t) return (t as { default: string }).default
66
+ return ''
67
+ }
@@ -1,46 +1,30 @@
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
- */
1
+ export type OGPreset =
2
+ | 'DARK'
3
+ | 'DARK_BLUE'
4
+ | 'DARK_PURPLE'
5
+ | 'DARK_GREEN'
6
+ | 'DARK_ROSE'
7
+ | 'LIGHT'
8
+ | 'LIGHT_GRAY'
9
+ | 'LIGHT_WARM'
10
+ | 'LIGHT_GREEN'
7
11
 
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
- }
12
+ export type OGLayout = 'DEFAULT' | 'HERO' | 'ARTICLE' | 'MINIMAL'
46
13
 
14
+ /** Mirrors Django OGImageParams exactly */
15
+ export interface OGImageParams {
16
+ title: string
17
+ description?: string
18
+ locale?: string
19
+ style?: 'dark' | 'light'
20
+ bg_color?: string
21
+ bg_color2?: string
22
+ text_color?: string
23
+ accent_color?: string
24
+ /** e.g. "1200x630" */
25
+ size?: string
26
+ preset?: OGPreset
27
+ layout?: OGLayout
28
+ /** Excluded from cache key on Django side */
29
+ page_url?: string
30
+ }