@djangocfg/nextjs 2.1.55 → 2.1.57
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 +4 -4
- package/dist/config/index.mjs +1 -1
- package/dist/config/index.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +17 -7
- package/dist/index.mjs.map +1 -1
- package/dist/og-image/index.d.mts +1 -1
- package/dist/og-image/index.mjs +16 -6
- package/dist/og-image/index.mjs.map +1 -1
- package/dist/og-image/utils/index.d.mts +12 -6
- package/dist/og-image/utils/index.mjs +16 -6
- package/dist/og-image/utils/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/og-image/README.md +4 -4
- package/src/og-image/index.ts +3 -3
- package/src/og-image/utils/index.ts +3 -3
- package/src/og-image/utils/metadata.ts +33 -10
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/og-image/utils/fonts.ts","../../../src/og-image/utils/url.ts","../../../src/og-image/utils/metadata.ts"],"sourcesContent":["/**\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 OgImageMetadataOptions {\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}\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 { generateOgImageMetadata } from '@djangocfg/nextjs/og-image';\n * import { settings } from '@/core/settings';\n *\n * export const metadata = generateOgImageMetadata(\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 * 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 generateOgImageMetadata(\n metadata: Metadata,\n ogImageParams?: Partial<OgImageUrlParams>,\n options: OgImageMetadataOptions = {}\n): Metadata {\n const {\n ogImageBaseUrl = 'https://djangocfg.com/api/og',\n siteUrl: providedSiteUrl,\n defaultParams = {},\n useBase64 = true,\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 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 { createOgImageMetadataGenerator } from '@djangocfg/nextjs/og-image';\n * import { settings } from '@/core/settings';\n *\n * export const generateMetadata = createOgImageMetadataGenerator({\n * ogImageBaseUrl: '/api/og',\n * siteUrl: settings.app.siteUrl,\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 createOgImageMetadataGenerator(\n options: OgImageMetadataOptions\n) {\n return (\n metadata: Metadata,\n ogImageParams?: Partial<OgImageUrlParams>\n ): Metadata => {\n return generateOgImageMetadata(metadata, ogImageParams, options);\n };\n}\n\n"],"mappings":"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","names":["result"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/og-image/utils/fonts.ts","../../../src/og-image/utils/url.ts","../../../src/og-image/utils/metadata.ts"],"sourcesContent":["/**\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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/nextjs",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.57",
|
|
4
4
|
"description": "Next.js server utilities: sitemap, health, OG images, contact forms, navigation, config",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nextjs",
|
|
@@ -133,9 +133,9 @@
|
|
|
133
133
|
"web-push": "^3.6.7"
|
|
134
134
|
},
|
|
135
135
|
"devDependencies": {
|
|
136
|
-
"@djangocfg/imgai": "^2.1.
|
|
137
|
-
"@djangocfg/layouts": "^2.1.
|
|
138
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
136
|
+
"@djangocfg/imgai": "^2.1.57",
|
|
137
|
+
"@djangocfg/layouts": "^2.1.57",
|
|
138
|
+
"@djangocfg/typescript-config": "^2.1.57",
|
|
139
139
|
"@types/node": "^24.7.2",
|
|
140
140
|
"@types/react": "19.2.2",
|
|
141
141
|
"@types/react-dom": "19.2.1",
|
package/src/og-image/README.md
CHANGED
|
@@ -8,10 +8,10 @@ Automatically add `og:image` to Next.js metadata.
|
|
|
8
8
|
|
|
9
9
|
```typescript
|
|
10
10
|
import type { Metadata } from 'next';
|
|
11
|
-
import {
|
|
11
|
+
import { generateAppMetadata } from '@djangocfg/nextjs/og-image';
|
|
12
12
|
import { settings } from '@core/settings';
|
|
13
13
|
|
|
14
|
-
export const metadata: Metadata =
|
|
14
|
+
export const metadata: Metadata = generateAppMetadata(
|
|
15
15
|
{
|
|
16
16
|
title: 'My Page',
|
|
17
17
|
description: 'Page description',
|
|
@@ -35,10 +35,10 @@ export const metadata: Metadata = generateOgImageMetadata(
|
|
|
35
35
|
|
|
36
36
|
```typescript
|
|
37
37
|
// lib/metadata.ts
|
|
38
|
-
import {
|
|
38
|
+
import { createAppMetadataGenerator } from '@djangocfg/nextjs/og-image';
|
|
39
39
|
import { settings } from '@core/settings';
|
|
40
40
|
|
|
41
|
-
export const generateMetadata =
|
|
41
|
+
export const generateMetadata = createAppMetadataGenerator({
|
|
42
42
|
ogImageBaseUrl: '/api/og',
|
|
43
43
|
siteUrl: settings.app.siteUrl,
|
|
44
44
|
defaultParams: {
|
package/src/og-image/index.ts
CHANGED
|
@@ -19,9 +19,9 @@ export {
|
|
|
19
19
|
parseOgImageData,
|
|
20
20
|
encodeBase64,
|
|
21
21
|
decodeBase64,
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
generateAppMetadata,
|
|
23
|
+
createAppMetadataGenerator,
|
|
24
24
|
type FontConfig,
|
|
25
25
|
type OgImageUrlParams,
|
|
26
|
-
type
|
|
26
|
+
type AppMetadataOptions,
|
|
27
27
|
} from './utils';
|
|
@@ -10,7 +10,7 @@ import { generateOgImageUrl, getAbsoluteOgImageUrl, OgImageUrlParams} from './ur
|
|
|
10
10
|
/**
|
|
11
11
|
* Options for generating OG image metadata
|
|
12
12
|
*/
|
|
13
|
-
export interface
|
|
13
|
+
export interface AppMetadataOptions {
|
|
14
14
|
/** Base URL of the OG image API route (e.g., '/api/og') */
|
|
15
15
|
ogImageBaseUrl?: string;
|
|
16
16
|
/** Site URL for absolute URLs (e.g., 'https://example.com') */
|
|
@@ -19,6 +19,10 @@ export interface OgImageMetadataOptions {
|
|
|
19
19
|
defaultParams?: Partial<OgImageUrlParams>;
|
|
20
20
|
/** Whether to use base64 encoding (default: true) */
|
|
21
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;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
/**
|
|
@@ -63,10 +67,10 @@ function extractDescription(metadata: Metadata): string {
|
|
|
63
67
|
* @example
|
|
64
68
|
* ```typescript
|
|
65
69
|
* // In page.tsx or layout.tsx
|
|
66
|
-
* import {
|
|
70
|
+
* import { generateAppMetadata } from '@djangocfg/nextjs/og-image';
|
|
67
71
|
* import { settings } from '@/core/settings';
|
|
68
72
|
*
|
|
69
|
-
* export const metadata =
|
|
73
|
+
* export const metadata = generateAppMetadata(
|
|
70
74
|
* {
|
|
71
75
|
* title: 'My Page',
|
|
72
76
|
* description: 'Page description',
|
|
@@ -75,6 +79,8 @@ function extractDescription(metadata: Metadata): string {
|
|
|
75
79
|
* {
|
|
76
80
|
* ogImageBaseUrl: '/api/og',
|
|
77
81
|
* siteUrl: settings.app.siteUrl,
|
|
82
|
+
* favicon: settings.app.icons.favicon,
|
|
83
|
+
* appleIcon: settings.app.icons.logo192,
|
|
78
84
|
* defaultParams: {
|
|
79
85
|
* siteName: settings.app.name,
|
|
80
86
|
* logo: settings.app.icons.logoVector,
|
|
@@ -97,16 +103,18 @@ function getSiteUrl(): string {
|
|
|
97
103
|
return '';
|
|
98
104
|
}
|
|
99
105
|
|
|
100
|
-
export function
|
|
106
|
+
export function generateAppMetadata(
|
|
101
107
|
metadata: Metadata,
|
|
102
108
|
ogImageParams?: Partial<OgImageUrlParams>,
|
|
103
|
-
options:
|
|
109
|
+
options: AppMetadataOptions = {}
|
|
104
110
|
): Metadata {
|
|
105
111
|
const {
|
|
106
112
|
ogImageBaseUrl = 'https://djangocfg.com/api/og',
|
|
107
113
|
siteUrl: providedSiteUrl,
|
|
108
114
|
defaultParams = {},
|
|
109
115
|
useBase64 = true,
|
|
116
|
+
favicon,
|
|
117
|
+
appleIcon,
|
|
110
118
|
} = options;
|
|
111
119
|
|
|
112
120
|
// Automatically determine siteUrl if not provided or is undefined
|
|
@@ -198,6 +206,19 @@ export function generateOgImageMetadata(
|
|
|
198
206
|
}
|
|
199
207
|
}
|
|
200
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
|
+
|
|
201
222
|
return finalMetadata;
|
|
202
223
|
}
|
|
203
224
|
|
|
@@ -212,12 +233,14 @@ export function generateOgImageMetadata(
|
|
|
212
233
|
* @example
|
|
213
234
|
* ```typescript
|
|
214
235
|
* // In a shared file (e.g., lib/metadata.ts)
|
|
215
|
-
* import {
|
|
236
|
+
* import { createAppMetadataGenerator } from '@djangocfg/nextjs/og-image';
|
|
216
237
|
* import { settings } from '@/core/settings';
|
|
217
238
|
*
|
|
218
|
-
* export const generateMetadata =
|
|
239
|
+
* export const generateMetadata = createAppMetadataGenerator({
|
|
219
240
|
* ogImageBaseUrl: '/api/og',
|
|
220
241
|
* siteUrl: settings.app.siteUrl,
|
|
242
|
+
* favicon: settings.app.icons.favicon,
|
|
243
|
+
* appleIcon: settings.app.icons.logo192,
|
|
221
244
|
* defaultParams: {
|
|
222
245
|
* siteName: settings.app.name,
|
|
223
246
|
* logo: settings.app.icons.logoVector,
|
|
@@ -233,14 +256,14 @@ export function generateOgImageMetadata(
|
|
|
233
256
|
* });
|
|
234
257
|
* ```
|
|
235
258
|
*/
|
|
236
|
-
export function
|
|
237
|
-
options:
|
|
259
|
+
export function createAppMetadataGenerator(
|
|
260
|
+
options: AppMetadataOptions
|
|
238
261
|
) {
|
|
239
262
|
return (
|
|
240
263
|
metadata: Metadata,
|
|
241
264
|
ogImageParams?: Partial<OgImageUrlParams>
|
|
242
265
|
): Metadata => {
|
|
243
|
-
return
|
|
266
|
+
return generateAppMetadata(metadata, ogImageParams, options);
|
|
244
267
|
};
|
|
245
268
|
}
|
|
246
269
|
|