@ewanc26/og 0.1.3 → 0.1.5
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/dist/chunk-2I73D34T.js +368 -0
- package/dist/chunk-2I73D34T.js.map +1 -0
- package/dist/index.cjs +253 -111
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +69 -33
- package/dist/index.js.map +1 -1
- package/dist/templates/index.cjs +179 -77
- package/dist/templates/index.cjs.map +1 -1
- package/dist/templates/index.d.cts +208 -85
- package/dist/templates/index.d.ts +208 -85
- package/dist/templates/index.js +1 -1
- package/package.json +62 -62
- package/src/endpoint.ts +6 -3
- package/src/fonts-data.ts +61 -0
- package/src/fonts.ts +21 -36
- package/src/templates/blog.ts +70 -36
- package/src/templates/default.ts +67 -33
- package/src/templates/profile.ts +40 -6
- package/dist/chunk-EPPJ2HBS.js +0 -258
- package/dist/chunk-EPPJ2HBS.js.map +0 -1
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/generate.ts","../src/fonts.ts","../src/noise.ts","../src/png-encoder.ts","../src/templates/blog.ts","../src/templates/profile.ts","../src/templates/default.ts","../src/templates/index.ts","../src/types.ts","../src/endpoint.ts","../src/svg.ts"],"sourcesContent":["/**\n * @ewanc26/og\n *\n * Dynamic OpenGraph image generator with noise backgrounds, bold typography,\n * and Satori-based rendering. Works in SvelteKit endpoints, edge runtimes,\n * and build scripts.\n *\n * @example\n * ```ts\n * import { generateOgImage, createOgEndpoint } from '@ewanc26/og';\n * import { blogTemplate } from '@ewanc26/og/templates';\n *\n * // Generate PNG\n * const png = await generateOgImage({\n * title: 'My Blog Post',\n * description: 'A description',\n * siteName: 'ewancroft.uk',\n * });\n *\n * // SvelteKit endpoint\n * export const GET = createOgEndpoint({ siteName: 'ewancroft.uk' });\n * ```\n */\n\n// Core generation\nexport { generateOgImage, generateOgImageDataUrl, generateOgResponse, OG_WIDTH, OG_HEIGHT } from './generate.js'\n\n// Types\nexport type {\n\tOgColorConfig,\n\tOgFontConfig,\n\tOgNoiseConfig,\n\tOgTemplateProps,\n\tOgTemplate,\n\tOgGenerateOptions,\n\tOgEndpointOptions,\n} from './types.js'\nexport { defaultColors } from './types.js'\n\n// Noise (for advanced customization)\nexport { generateNoiseDataUrl, generateCircleNoiseDataUrl } from './noise.js'\n\n// Fonts (for advanced customization)\nexport { loadFonts, createSatoriFonts, BUNDLED_FONTS } from './fonts.js'\n\n// Endpoint helpers\nexport { createOgEndpoint } from './endpoint.js'\n\n// SVG to PNG conversion\nexport { svgToPng, svgToPngDataUrl, svgToPngResponse } from './svg.js'\nexport type { SvgToPngOptions } from './svg.js'\n","/**\n * Core OG image generation.\n * Uses satori for JSX-to-SVG and resvg-js for SVG-to-PNG.\n */\n\nimport satori from 'satori'\nimport { Resvg } from '@resvg/resvg-js'\nimport { loadFonts, createSatoriFonts } from './fonts.js'\nimport { generateNoiseDataUrl, generateCircleNoiseDataUrl } from './noise.js'\nimport { getTemplate } from './templates/index.js'\nimport { defaultColors } from './types.js'\nimport type {\n\tOgGenerateOptions,\n\tOgColorConfig,\n\tOgTemplateProps,\n} from './types.js'\n\n// Standard OG image dimensions\nexport const OG_WIDTH = 1200\nexport const OG_HEIGHT = 630\n\n/**\n * Generate an OG image as PNG Buffer.\n */\nexport async function generateOgImage(options: OgGenerateOptions): Promise<Buffer> {\n\tconst {\n\t\ttitle,\n\t\tdescription,\n\t\tsiteName,\n\t\timage,\n\t\ttemplate = 'blog',\n\t\tcolors: colorOverrides,\n\t\tfonts: fontConfig,\n\t\tnoise: noiseConfig,\n\t\tnoiseSeed,\n\t\twidth = OG_WIDTH,\n\t\theight = OG_HEIGHT,\n\t\tdebugSvg = false,\n\t} = options\n\n\t// Merge colours\n\tconst colors: OgColorConfig = {\n\t\t...defaultColors,\n\t\t...colorOverrides,\n\t}\n\n\t// Load fonts\n\tconst fonts = await loadFonts(fontConfig)\n\tconst satoriFonts = createSatoriFonts(fonts)\n\n\t// Generate noise background\n\tconst noiseEnabled = noiseConfig?.enabled !== false\n\tconst noiseSeedValue = noiseSeed || noiseConfig?.seed || title\n\tconst noiseDataUrl = noiseEnabled\n\t\t? generateNoiseDataUrl({\n\t\t\t\tseed: noiseSeedValue,\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t\topacity: noiseConfig?.opacity ?? 0.4,\n\t\t\t\tcolorMode: noiseConfig?.colorMode ?? 'grayscale',\n\t\t\t})\n\t\t: undefined\n\n\t// Generate circular noise decoration\n\tconst circleNoiseDataUrl = noiseEnabled\n\t\t? generateCircleNoiseDataUrl({\n\t\t\t\tseed: `${noiseSeedValue}-circle`,\n\t\t\t\tsize: 200,\n\t\t\t\topacity: noiseConfig?.opacity ?? 0.15,\n\t\t\t\tcolorMode: noiseConfig?.colorMode ?? 'grayscale',\n\t\t\t})\n\t\t: undefined\n\n\t// Get template function\n\tconst templateFn = getTemplate(template as Parameters<typeof getTemplate>[0])\n\n\t// Build template props\n\tconst props: OgTemplateProps = {\n\t\ttitle,\n\t\tdescription,\n\t\tsiteName,\n\t\timage,\n\t\tcolors,\n\t\tnoiseDataUrl,\n\t\tcircleNoiseDataUrl,\n\t\twidth,\n\t\theight,\n\t}\n\n\t// Render template to Satori-compatible structure\n\tconst element = templateFn(props)\n\n\t// Generate SVG with satori\n\tconst svg = await satori(element as Parameters<typeof satori>[0], {\n\t\twidth,\n\t\theight,\n\t\tfonts: satoriFonts,\n\t})\n\n\t// Debug: return SVG string\n\tif (debugSvg) {\n\t\treturn Buffer.from(svg)\n\t}\n\n\t// Convert SVG to PNG with resvg-js\n\tconst resvg = new Resvg(svg, {\n\t\tfitTo: {\n\t\t\tmode: 'width',\n\t\t\tvalue: width,\n\t\t},\n\t})\n\tconst pngData = resvg.render()\n\n\treturn Buffer.from(pngData.asPng())\n}\n\n/**\n * Generate OG image and return as base64 data URL.\n */\nexport async function generateOgImageDataUrl(options: OgGenerateOptions): Promise<string> {\n\tconst png = await generateOgImage(options)\n\treturn `data:image/png;base64,${png.toString('base64')}`\n}\n\n/**\n * Generate OG image and return as Response (for SvelteKit endpoints).\n */\nexport async function generateOgResponse(options: OgGenerateOptions, cacheMaxAge = 3600): Promise<Response> {\n\tconst png = await generateOgImage(options)\n\n\treturn new Response(png, {\n\t\theaders: {\n\t\t\t'Content-Type': 'image/png',\n\t\t\t'Cache-Control': `public, max-age=${cacheMaxAge}`,\n\t\t},\n\t})\n}\n","/**\n * @ewanc26/og fonts\n *\n * Font loading utilities. Bundles Inter font by default.\n */\n\nimport { readFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { OgFontConfig } from './types.js'\n\n// Declare __dirname for CJS contexts (injected by bundlers)\ndeclare const __dirname: string | undefined\n\n// ─── Paths ────────────────────────────────────────────────────────────────────\n\n/**\n * Get the directory of the current module.\n * Works in both ESM and bundled contexts.\n */\nfunction getModuleDir(): string {\n\t// ESM context\n\tif (typeof import.meta !== 'undefined' && import.meta.url) {\n\t\treturn dirname(fileURLToPath(import.meta.url))\n\t}\n\t// Bundled CJS context - __dirname is injected by bundlers\n\tif (typeof __dirname !== 'undefined') {\n\t\treturn __dirname\n\t}\n\t// Fallback\n\treturn resolve(process.cwd(), 'node_modules/@ewanc26/og/dist')\n}\n\n/**\n * Resolve the fonts directory relative to the installed package.\n * Tries multiple possible locations for serverless compatibility.\n */\nfunction getFontsDir(): string {\n\tconst candidates = [\n\t\t// Standard: fonts next to dist\n\t\tresolve(getModuleDir(), '../fonts'),\n\t\t// Vercel serverless: fonts inside dist\n\t\tresolve(getModuleDir(), 'fonts'),\n\t\t// Fallback: node_modules path\n\t\tresolve(process.cwd(), 'node_modules/@ewanc26/og/fonts'),\n\t]\n\n\tfor (const dir of candidates) {\n\t\tif (existsSync(dir)) {\n\t\t\treturn dir\n\t\t}\n\t}\n\n\t// Return first candidate as fallback (will fail gracefully)\n\treturn candidates[0]\n}\n\n/**\n * Resolve bundled font paths. Uses getters to defer resolution until runtime.\n */\nexport const BUNDLED_FONTS = {\n\tget heading() {\n\t\treturn resolve(getFontsDir(), 'Inter-Bold.ttf')\n\t},\n\tget body() {\n\t\treturn resolve(getFontsDir(), 'Inter-Regular.ttf')\n\t},\n} as const\n\n// Google Fonts CDN fallback URLs\nconst FONT_FALLBACKS = {\n\theading: 'https://github.com/rsms/inter/raw/refs/heads/main/docs/font-files/Inter-Bold.ttf',\n\tbody: 'https://github.com/rsms/inter/raw/refs/heads/main/docs/font-files/Inter-Regular.ttf',\n}\n\n// ─── Font Loading ──────────────────────────────────────────────────────────────\n\nexport interface LoadedFonts {\n\theading: ArrayBuffer\n\tbody: ArrayBuffer\n}\n\n/**\n * Load fonts from config, falling back to bundled fonts,\n * with CDN fallback for serverless environments.\n */\nexport async function loadFonts(config?: OgFontConfig): Promise<LoadedFonts> {\n\tconst headingPath = config?.heading ?? BUNDLED_FONTS.heading\n\tconst bodyPath = config?.body ?? BUNDLED_FONTS.body\n\n\tconst [heading, body] = await Promise.all([\n\t\tloadFontFileWithFallback(headingPath, FONT_FALLBACKS.heading),\n\t\tloadFontFileWithFallback(bodyPath, FONT_FALLBACKS.body),\n\t])\n\n\treturn { heading, body }\n}\n\n/**\n * Load a font file, falling back to CDN if local file fails.\n */\nasync function loadFontFileWithFallback(path: string, fallbackUrl: string): Promise<ArrayBuffer> {\n\ttry {\n\t\treturn await loadFontFile(path)\n\t} catch (error) {\n\t\tconsole.warn(`Failed to load local font at ${path}, trying CDN fallback:`, error)\n\t\ttry {\n\t\t\treturn await loadFontFile(fallbackUrl)\n\t\t} catch (fallbackError) {\n\t\t\tthrow new Error(`Failed to load font from both local path (${path}) and CDN (${fallbackUrl}): ${fallbackError}`)\n\t\t}\n\t}\n}\n\n/**\n * Load a font from file path or URL.\n */\nasync function loadFontFile(source: string): Promise<ArrayBuffer> {\n\t// Handle URLs\n\tif (source.startsWith('http://') || source.startsWith('https://')) {\n\t\tconst response = await fetch(source)\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`Failed to load font from URL: ${source}`)\n\t\t}\n\t\treturn response.arrayBuffer()\n\t}\n\n\t// Handle file paths\n\tconst buffer = await readFile(source)\n\treturn buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)\n}\n\n// ─── Font Registration for Satori ─────────────────────────────────────────────\n\nexport type SatoriFontConfig = {\n\tname: string\n\tdata: ArrayBuffer\n\tweight: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900\n\tstyle: 'normal' | 'italic'\n}\n\n/**\n * Create Satori-compatible font config from loaded fonts.\n * Uses Inter font family with weight 700 for headings and 400 for body.\n */\nexport function createSatoriFonts(fonts: LoadedFonts): SatoriFontConfig[] {\n\treturn [\n\t\t{\n\t\t\tname: 'Inter',\n\t\t\tdata: fonts.heading,\n\t\t\tweight: 700,\n\t\t\tstyle: 'normal',\n\t\t},\n\t\t{\n\t\t\tname: 'Inter',\n\t\t\tdata: fonts.body,\n\t\t\tweight: 400,\n\t\t\tstyle: 'normal',\n\t\t},\n\t]\n}\n","/**\n * @ewanc26/og noise\n */\n\nimport { generateNoisePixels } from '@ewanc26/noise'\nimport { PNGEncoder } from './png-encoder.js'\nimport type { OgNoiseConfig } from './types.js'\n\nexport interface NoiseOptions {\n\tseed: string\n\twidth: number\n\theight: number\n\topacity?: number\n\tcolorMode?: OgNoiseConfig['colorMode']\n}\n\nexport interface CircleNoiseOptions {\n\tseed: string\n\tsize: number\n\topacity?: number\n\tcolorMode?: OgNoiseConfig['colorMode']\n}\n\n/**\n * Generate a noise PNG as a data URL.\n */\nexport function generateNoiseDataUrl(options: NoiseOptions): string {\n\tconst { seed, width, height, opacity = 0.4, colorMode = 'grayscale' } = options\n\n\tconst pixels = generateNoisePixels(width, height, seed, {\n\t\tgridSize: 4,\n\t\toctaves: 3,\n\t\tcolorMode: colorMode === 'grayscale'\n\t\t\t? { type: 'grayscale', range: [20, 60] }\n\t\t\t: { type: 'hsl', hueRange: 40, saturationRange: [30, 50], lightnessRange: [30, 50] }\n\t})\n\n\tif (opacity < 1) {\n\t\tfor (let i = 3; i < pixels.length; i += 4) {\n\t\t\tpixels[i] = Math.round(pixels[i] * opacity)\n\t\t}\n\t}\n\n\tconst pngBuffer = PNGEncoder.encode(pixels, width, height)\n\treturn `data:image/png;base64,${pngBuffer.toString('base64')}`\n}\n\n/**\n * Generate a circular noise PNG as a data URL.\n * Creates a square image with circular transparency mask.\n */\nexport function generateCircleNoiseDataUrl(options: CircleNoiseOptions): string {\n\tconst { seed, size, opacity = 0.15, colorMode = 'grayscale' } = options\n\n\tconst pixels = generateNoisePixels(size, size, seed, {\n\t\tgridSize: 4,\n\t\toctaves: 3,\n\t\tcolorMode: colorMode === 'grayscale'\n\t\t\t? { type: 'grayscale', range: [30, 70] }\n\t\t\t: { type: 'hsl', hueRange: 40, saturationRange: [30, 50], lightnessRange: [30, 50] }\n\t})\n\n\tconst center = size / 2\n\tconst radius = size / 2\n\n\t// Apply circular mask\n\tfor (let y = 0; y < size; y++) {\n\t\tfor (let x = 0; x < size; x++) {\n\t\t\tconst idx = (y * size + x) * 4\n\t\t\tconst dx = x - center + 0.5\n\t\t\tconst dy = y - center + 0.5\n\t\t\tconst dist = Math.sqrt(dx * dx + dy * dy)\n\n\t\t\tif (dist > radius) {\n\t\t\t\t// Outside circle - fully transparent\n\t\t\t\tpixels[idx + 3] = 0\n\t\t\t} else if (dist > radius - 2) {\n\t\t\t\t// Anti-alias edge\n\t\t\t\tconst edgeOpacity = (radius - dist) / 2\n\t\t\t\tpixels[idx + 3] = Math.round(255 * edgeOpacity * opacity)\n\t\t\t} else {\n\t\t\t\t// Inside circle - apply opacity\n\t\t\t\tpixels[idx + 3] = Math.round(255 * opacity)\n\t\t\t}\n\t\t}\n\t}\n\n\tconst pngBuffer = PNGEncoder.encode(pixels, size, size)\n\treturn `data:image/png;base64,${pngBuffer.toString('base64')}`\n}\n","/**\n * Minimal PNG encoder for noise backgrounds.\n * Uses node:zlib for deflate compression.\n */\n\nimport { deflateSync } from 'node:zlib'\n\nconst PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])\n\nfunction crc32(data: Buffer): number {\n\tlet crc = 0xffffffff\n\tconst table: number[] = []\n\n\t// Build CRC table\n\tfor (let n = 0; n < 256; n++) {\n\t\tlet c = n\n\t\tfor (let k = 0; k < 8; k++) {\n\t\t\tc = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1\n\t\t}\n\t\ttable[n] = c\n\t}\n\n\t// Calculate CRC\n\tfor (let i = 0; i < data.length; i++) {\n\t\tcrc = table[(crc ^ data[i]) & 0xff] ^ (crc >>> 8)\n\t}\n\n\treturn (crc ^ 0xffffffff) >>> 0\n}\n\nfunction createChunk(type: string, data: Buffer): Buffer {\n\tconst length = Buffer.alloc(4)\n\tlength.writeUInt32BE(data.length, 0)\n\n\tconst typeBuffer = Buffer.from(type, 'ascii')\n\tconst crcData = Buffer.concat([typeBuffer, data])\n\tconst crc = Buffer.alloc(4)\n\tcrc.writeUInt32BE(crc32(crcData), 0)\n\n\treturn Buffer.concat([length, typeBuffer, data, crc])\n}\n\nfunction createIHDR(width: number, height: number): Buffer {\n\tconst data = Buffer.alloc(13)\n\tdata.writeUInt32BE(width, 0) // Width\n\tdata.writeUInt32BE(height, 4) // Height\n\tdata.writeUInt8(8, 8) // Bit depth: 8 bits\n\tdata.writeUInt8(2, 9) // Colour type: 2 (RGB)\n\tdata.writeUInt8(0, 10) // Compression method\n\tdata.writeUInt8(0, 11) // Filter method\n\tdata.writeUInt8(0, 12) // Interlace method\n\n\treturn createChunk('IHDR', data)\n}\n\nfunction createIDAT(pixels: Uint8ClampedArray, width: number, height: number): Buffer {\n\t// Apply filter (none filter = 0) per row\n\tconst rawData = Buffer.alloc(height * (width * 3 + 1))\n\n\tlet srcOffset = 0\n\tlet dstOffset = 0\n\n\tfor (let y = 0; y < height; y++) {\n\t\trawData[dstOffset++] = 0 // Filter type: none\n\t\tfor (let x = 0; x < width; x++) {\n\t\t\tconst r = pixels[srcOffset++]\n\t\t\tconst g = pixels[srcOffset++]\n\t\t\tconst b = pixels[srcOffset++]\n\t\t\tsrcOffset++ // Skip alpha\n\t\t\trawData[dstOffset++] = r\n\t\t\trawData[dstOffset++] = g\n\t\t\trawData[dstOffset++] = b\n\t\t}\n\t}\n\n\tconst compressed = deflateSync(rawData)\n\treturn createChunk('IDAT', compressed)\n}\n\nfunction createIEND(): Buffer {\n\treturn createChunk('IEND', Buffer.alloc(0))\n}\n\n/**\n * Encode raw RGBA pixel data as a PNG Buffer.\n */\nexport function encodePNG(pixels: Uint8ClampedArray, width: number, height: number): Buffer {\n\treturn Buffer.concat([\n\t\tPNG_SIGNATURE,\n\t\tcreateIHDR(width, height),\n\t\tcreateIDAT(pixels, width, height),\n\t\tcreateIEND(),\n\t])\n}\n\n/**\n * PNGEncoder namespace for cleaner imports.\n */\nexport const PNGEncoder = {\n\tencode: encodePNG,\n}\n","/**\n * Blog OG template.\n * Clean centered layout.\n */\n\nimport type { OgTemplateProps } from '../types.js'\n\nexport function blogTemplate({\n\ttitle,\n\tdescription,\n\tsiteName,\n\tcolors,\n\twidth,\n\theight,\n}: OgTemplateProps) {\n\treturn {\n\t\ttype: 'div',\n\t\tprops: {\n\t\t\tstyle: {\n\t\t\t\tdisplay: 'flex',\n\t\t\t\tflexDirection: 'column',\n\t\t\t\talignItems: 'center',\n\t\t\t\tjustifyContent: 'center',\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t\tbackgroundColor: colors.background,\n\t\t\t},\n\t\t\tchildren: [\n\t\t\t\t{\n\t\t\t\t\ttype: 'h1',\n\t\t\t\t\tprops: {\n\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\tfontSize: 64,\n\t\t\t\t\t\t\tfontWeight: 700,\n\t\t\t\t\t\t\tcolor: colors.text,\n\t\t\t\t\t\t\tletterSpacing: '-0.02em',\n\t\t\t\t\t\t\tmargin: 0,\n\t\t\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\t\t\tlineHeight: 1.1,\n\t\t\t\t\t\t\tmaxWidth: 1000,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tchildren: title,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdescription ? {\n\t\t\t\t\ttype: 'p',\n\t\t\t\t\tprops: {\n\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\tfontSize: 28,\n\t\t\t\t\t\t\tfontWeight: 400,\n\t\t\t\t\t\t\tcolor: colors.accent,\n\t\t\t\t\t\t\tmarginTop: 28,\n\t\t\t\t\t\t\tmarginBottom: 0,\n\t\t\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\t\t\tlineHeight: 1.4,\n\t\t\t\t\t\t\tmaxWidth: 900,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tchildren: description,\n\t\t\t\t\t},\n\t\t\t\t} : null,\n\t\t\t\t{\n\t\t\t\t\ttype: 'p',\n\t\t\t\t\tprops: {\n\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\tfontSize: 24,\n\t\t\t\t\t\t\tfontWeight: 400,\n\t\t\t\t\t\t\tcolor: colors.accent,\n\t\t\t\t\t\t\tmarginTop: 56,\n\t\t\t\t\t\t\tmarginBottom: 0,\n\t\t\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\t\t\topacity: 0.7,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tchildren: siteName,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t].filter(Boolean),\n\t\t},\n\t}\n}\n","/**\n * Profile OG template.\n * Centered layout.\n */\n\nimport type { OgTemplateProps } from '../types.js'\n\nexport function profileTemplate({\n\ttitle,\n\tdescription,\n\tsiteName,\n\timage,\n\tcolors,\n\twidth,\n\theight,\n}: OgTemplateProps) {\n\tconst children: unknown[] = []\n\n\tif (image) {\n\t\tchildren.push({\n\t\t\ttype: 'img',\n\t\t\tprops: {\n\t\t\t\tsrc: image,\n\t\t\t\twidth: 120,\n\t\t\t\theight: 120,\n\t\t\t\tstyle: {\n\t\t\t\t\tborderRadius: '50%',\n\t\t\t\t\tmarginBottom: 32,\n\t\t\t\t\tobjectFit: 'cover',\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t}\n\n\tchildren.push({\n\t\ttype: 'h1',\n\t\tprops: {\n\t\t\tstyle: {\n\t\t\t\tfontSize: 56,\n\t\t\t\tfontWeight: 700,\n\t\t\t\tcolor: colors.text,\n\t\t\t\tletterSpacing: '-0.02em',\n\t\t\t\tmargin: 0,\n\t\t\t\ttextAlign: 'center',\n\t\t\t\tlineHeight: 1.1,\n\t\t\t\tmaxWidth: 900,\n\t\t\t},\n\t\t\tchildren: title,\n\t\t},\n\t})\n\n\tif (description) {\n\t\tchildren.push({\n\t\t\ttype: 'p',\n\t\t\tprops: {\n\t\t\t\tstyle: {\n\t\t\t\t\tfontSize: 26,\n\t\t\t\t\tfontWeight: 400,\n\t\t\t\t\tcolor: colors.accent,\n\t\t\t\t\tmarginTop: 20,\n\t\t\t\t\tmarginBottom: 0,\n\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\tlineHeight: 1.4,\n\t\t\t\t\tmaxWidth: 700,\n\t\t\t\t},\n\t\t\t\tchildren: description,\n\t\t\t},\n\t\t})\n\t}\n\n\tchildren.push({\n\t\ttype: 'p',\n\t\tprops: {\n\t\t\tstyle: {\n\t\t\t\tfontSize: 24,\n\t\t\t\tfontWeight: 400,\n\t\t\t\tcolor: colors.accent,\n\t\t\t\tmarginTop: 48,\n\t\t\t\tmarginBottom: 0,\n\t\t\t\ttextAlign: 'center',\n\t\t\t\topacity: 0.7,\n\t\t\t},\n\t\t\tchildren: siteName,\n\t\t},\n\t})\n\n\treturn {\n\t\ttype: 'div',\n\t\tprops: {\n\t\t\tstyle: {\n\t\t\t\tdisplay: 'flex',\n\t\t\t\tflexDirection: 'column',\n\t\t\t\talignItems: 'center',\n\t\t\t\tjustifyContent: 'center',\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t\tbackgroundColor: colors.background,\n\t\t\t},\n\t\t\tchildren,\n\t\t},\n\t}\n}\n","/**\n * Default OG template.\n * Clean, centered layout.\n */\n\nimport type { OgTemplateProps } from '../types.js'\n\nexport function defaultTemplate({\n\ttitle,\n\tdescription,\n\tsiteName,\n\tcolors,\n\twidth,\n\theight,\n}: OgTemplateProps) {\n\treturn {\n\t\ttype: 'div',\n\t\tprops: {\n\t\t\tstyle: {\n\t\t\t\tdisplay: 'flex',\n\t\t\t\tflexDirection: 'column',\n\t\t\t\talignItems: 'center',\n\t\t\t\tjustifyContent: 'center',\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t\tbackgroundColor: colors.background,\n\t\t\t},\n\t\t\tchildren: [\n\t\t\t\t{\n\t\t\t\t\ttype: 'h1',\n\t\t\t\t\tprops: {\n\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\tfontSize: 72,\n\t\t\t\t\t\t\tfontWeight: 700,\n\t\t\t\t\t\t\tcolor: colors.text,\n\t\t\t\t\t\t\tletterSpacing: '-0.02em',\n\t\t\t\t\t\t\tmargin: 0,\n\t\t\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\t\t},\n\t\t\t\t\t\tchildren: title,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdescription ? {\n\t\t\t\t\ttype: 'p',\n\t\t\t\t\tprops: {\n\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\tfontSize: 32,\n\t\t\t\t\t\t\tfontWeight: 400,\n\t\t\t\t\t\t\tcolor: colors.accent,\n\t\t\t\t\t\t\tmarginTop: 24,\n\t\t\t\t\t\t\tmarginBottom: 0,\n\t\t\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\t\t\tmaxWidth: 900,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tchildren: description,\n\t\t\t\t\t},\n\t\t\t\t} : null,\n\t\t\t\t{\n\t\t\t\t\ttype: 'p',\n\t\t\t\t\tprops: {\n\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\tfontSize: 28,\n\t\t\t\t\t\t\tfontWeight: 400,\n\t\t\t\t\t\t\tcolor: colors.accent,\n\t\t\t\t\t\t\tmarginTop: 64,\n\t\t\t\t\t\t\tmarginBottom: 0,\n\t\t\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\t\t\topacity: 0.7,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tchildren: siteName,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t].filter(Boolean),\n\t\t},\n\t}\n}\n","/**\n * Built-in OG templates.\n */\n\nexport { blogTemplate } from './blog.js'\nexport { profileTemplate } from './profile.js'\nexport { defaultTemplate } from './default.js'\n\nimport { blogTemplate } from './blog.js'\nimport { profileTemplate } from './profile.js'\nimport { defaultTemplate } from './default.js'\nimport type { OgTemplate } from '../types.js'\n\nexport const templates = {\n\tblog: blogTemplate,\n\tprofile: profileTemplate,\n\tdefault: defaultTemplate,\n} as const\n\nexport type TemplateName = keyof typeof templates\n\nexport function getTemplate(name: TemplateName | OgTemplate): OgTemplate {\n\tif (typeof name === 'function') {\n\t\treturn name\n\t}\n\treturn templates[name]\n}\n","/**\n * @ewanc26/og types\n */\n\n// ─── Colour Configuration ─────────────────────────────────────────────────────\n\nexport interface OgColorConfig {\n\t/** Background color (very dark). @default '#0f1a15' */\n\tbackground: string\n\t/** Primary text color. @default '#e8f5e9' */\n\ttext: string\n\t/** Secondary/accent text (mint). @default '#86efac' */\n\taccent: string\n}\n\nexport const defaultColors: OgColorConfig = {\n\tbackground: '#0f1a15',\n\ttext: '#e8f5e9',\n\taccent: '#86efac',\n}\n\n// ─── Font Configuration ───────────────────────────────────────────────────────\n\nexport interface OgFontConfig {\n\theading?: string\n\tbody?: string\n}\n\n// ─── Noise Configuration ──────────────────────────────────────────────────────\n\nexport interface OgNoiseConfig {\n\tenabled?: boolean\n\tseed?: string\n\topacity?: number\n\tcolorMode?: 'grayscale' | 'hsl'\n}\n\n// ─── Template Props ────────────────────────────────────────────────────────────\n\nexport interface OgTemplateProps {\n\ttitle: string\n\tdescription?: string\n\tsiteName: string\n\timage?: string\n\tcolors: OgColorConfig\n\tnoiseDataUrl?: string\n\tcircleNoiseDataUrl?: string\n\twidth: number\n\theight: number\n}\n\nexport type OgTemplate = (props: OgTemplateProps) => unknown\n\n// ─── Generation Options ───────────────────────────────────────────────────────\n\nexport interface OgGenerateOptions {\n\ttitle: string\n\tdescription?: string\n\tsiteName: string\n\timage?: string\n\ttemplate?: 'blog' | 'profile' | 'default' | OgTemplate\n\tcolors?: Partial<OgColorConfig>\n\tfonts?: OgFontConfig\n\tnoise?: OgNoiseConfig\n\tnoiseSeed?: string\n\twidth?: number\n\theight?: number\n\tdebugSvg?: boolean\n}\n\n// ─── SvelteKit Endpoint Options ───────────────────────────────────────────────\n\nexport interface OgEndpointOptions {\n\tsiteName: string\n\tdefaultTemplate?: 'blog' | 'profile' | 'default' | OgTemplate\n\tcolors?: Partial<OgColorConfig>\n\tfonts?: OgFontConfig\n\tnoise?: OgNoiseConfig\n\tcacheMaxAge?: number\n\twidth?: number\n\theight?: number\n}\n\n// ─── Internal Types ────────────────────────────────────────────────────────────\n\nexport interface InternalGenerateContext {\n\twidth: number\n\theight: number\n\tfonts: { heading: ArrayBuffer; body: ArrayBuffer }\n\tcolors: OgColorConfig\n\tnoiseDataUrl?: string\n}\n","/**\n * SvelteKit endpoint helpers.\n */\n\nimport { generateOgResponse } from './generate.js'\nimport type { OgEndpointOptions, OgTemplate } from './types.js'\n\n/**\n * Create a SvelteKit GET handler for OG image generation.\n *\n * @example\n * ```ts\n * // src/routes/og/[title]/+server.ts\n * import { createOgEndpoint } from '@ewanc26/og';\n *\n * export const GET = createOgEndpoint({\n * siteName: 'ewancroft.uk',\n * defaultTemplate: 'blog',\n * });\n * ```\n *\n * The endpoint expects query parameters:\n * - `title` (required): Page title\n * - `description`: Optional description\n * - `image`: Optional avatar/logo URL\n * - `seed`: Optional noise seed\n */\nexport function createOgEndpoint(options: OgEndpointOptions) {\n\tconst {\n\t\tsiteName,\n\t\tdefaultTemplate: template = 'default',\n\t\tcolors,\n\t\tfonts,\n\t\tnoise,\n\t\tcacheMaxAge = 3600,\n\t\twidth,\n\t\theight,\n\t} = options\n\n\treturn async ({ url }: { url: URL }) => {\n\t\tconst title = url.searchParams.get('title')\n\t\tconst description = url.searchParams.get('description') ?? undefined\n\t\tconst image = url.searchParams.get('image') ?? undefined\n\t\tconst noiseSeed = url.searchParams.get('seed') ?? undefined\n\n\t\tif (!title) {\n\t\t\treturn new Response('Missing title parameter', { status: 400 })\n\t\t}\n\n\t\ttry {\n\t\t\treturn await generateOgResponse(\n\t\t\t\t{\n\t\t\t\t\ttitle,\n\t\t\t\t\tdescription,\n\t\t\t\t\tsiteName,\n\t\t\t\t\timage,\n\t\t\t\t\ttemplate: template as OgTemplate,\n\t\t\t\t\tcolors,\n\t\t\t\t\tfonts,\n\t\t\t\t\tnoise,\n\t\t\t\t\tnoiseSeed,\n\t\t\t\t\twidth,\n\t\t\t\t\theight,\n\t\t\t\t},\n\t\t\t\tcacheMaxAge\n\t\t\t)\n\t\t} catch (error) {\n\t\t\tconsole.error('Failed to generate OG image:', error)\n\t\t\treturn new Response('Failed to generate image', { status: 500 })\n\t\t}\n\t}\n}\n\n/**\n * Create a typed OG image URL for use in meta tags.\n */\nexport function createOgImageUrl(\n\tbaseUrl: string,\n\tparams: {\n\t\ttitle: string\n\t\tdescription?: string\n\t\timage?: string\n\t\tseed?: string\n\t}\n): string {\n\tconst url = new URL(baseUrl)\n\turl.searchParams.set('title', params.title)\n\tif (params.description) url.searchParams.set('description', params.description)\n\tif (params.image) url.searchParams.set('image', params.image)\n\tif (params.seed) url.searchParams.set('seed', params.seed)\n\treturn url.toString()\n}\n","/**\n * SVG to PNG conversion using @resvg/resvg-js.\n */\n\nimport { Resvg } from '@resvg/resvg-js'\n\nexport interface SvgToPngOptions {\n\t/** Scale to fit width in pixels */\n\tfitWidth?: number\n\t/** Background colour for transparent areas */\n\tbackgroundColor?: string\n}\n\n/**\n * Convert an SVG string to PNG Buffer.\n */\nexport function svgToPng(svg: string, options: SvgToPngOptions = {}): Buffer {\n\tconst opts = {\n\t\tfitTo: options.fitWidth\n\t\t\t? { mode: 'width' as const, value: options.fitWidth }\n\t\t\t: undefined,\n\t\tbackground: options.backgroundColor,\n\t}\n\n\tconst resvg = new Resvg(svg, opts)\n\tconst rendered = resvg.render()\n\n\treturn Buffer.from(rendered.asPng())\n}\n\n/**\n * Convert an SVG string to PNG data URL.\n */\nexport function svgToPngDataUrl(svg: string, options: SvgToPngOptions = {}): string {\n\tconst png = svgToPng(svg, options)\n\treturn `data:image/png;base64,${png.toString('base64')}`\n}\n\n/**\n * Convert an SVG string to PNG Response (for SvelteKit endpoints).\n */\nexport function svgToPngResponse(svg: string, options: SvgToPngOptions = {}, cacheMaxAge = 3600): Response {\n\tconst png = svgToPng(svg, options)\n\n\treturn new Response(png, {\n\t\theaders: {\n\t\t\t'Content-Type': 'image/png',\n\t\t\t'Cache-Control': `public, max-age=${cacheMaxAge}`,\n\t\t},\n\t})\n}\n"],"mappings":"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","names":["satori","import_resvg_js"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","../src/generate.ts","../src/fonts.ts","../src/fonts-data.ts","../src/noise.ts","../src/png-encoder.ts","../src/templates/blog.ts","../src/templates/profile.ts","../src/templates/default.ts","../src/templates/index.ts","../src/types.ts","../src/endpoint.ts","../src/svg.ts"],"sourcesContent":["/**\n * @ewanc26/og\n *\n * Dynamic OpenGraph image generator with noise backgrounds, bold typography,\n * and Satori-based rendering. Works in SvelteKit endpoints, edge runtimes,\n * and build scripts.\n *\n * @example\n * ```ts\n * import { generateOgImage, createOgEndpoint } from '@ewanc26/og';\n * import { blogTemplate } from '@ewanc26/og/templates';\n *\n * // Generate PNG\n * const png = await generateOgImage({\n * title: 'My Blog Post',\n * description: 'A description',\n * siteName: 'ewancroft.uk',\n * });\n *\n * // SvelteKit endpoint\n * export const GET = createOgEndpoint({ siteName: 'ewancroft.uk' });\n * ```\n */\n\n// Core generation\nexport { generateOgImage, generateOgImageDataUrl, generateOgResponse, OG_WIDTH, OG_HEIGHT } from './generate.js'\n\n// Types\nexport type {\n\tOgColorConfig,\n\tOgFontConfig,\n\tOgNoiseConfig,\n\tOgTemplateProps,\n\tOgTemplate,\n\tOgGenerateOptions,\n\tOgEndpointOptions,\n} from './types.js'\nexport { defaultColors } from './types.js'\n\n// Noise (for advanced customization)\nexport { generateNoiseDataUrl, generateCircleNoiseDataUrl } from './noise.js'\n\n// Fonts (for advanced customization)\nexport { loadFonts, createSatoriFonts, BUNDLED_FONTS } from './fonts.js'\n\n// Endpoint helpers\nexport { createOgEndpoint } from './endpoint.js'\n\n// SVG to PNG conversion\nexport { svgToPng, svgToPngDataUrl, svgToPngResponse } from './svg.js'\nexport type { SvgToPngOptions } from './svg.js'\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","/**\n * Core OG image generation.\n * Uses satori for JSX-to-SVG and resvg-js for SVG-to-PNG.\n */\n\nimport satori from 'satori'\nimport { Resvg } from '@resvg/resvg-js'\nimport { loadFonts, createSatoriFonts } from './fonts.js'\nimport { generateNoiseDataUrl, generateCircleNoiseDataUrl } from './noise.js'\nimport { getTemplate } from './templates/index.js'\nimport { defaultColors } from './types.js'\nimport type {\n\tOgGenerateOptions,\n\tOgColorConfig,\n\tOgTemplateProps,\n} from './types.js'\n\n// Standard OG image dimensions\nexport const OG_WIDTH = 1200\nexport const OG_HEIGHT = 630\n\n/**\n * Generate an OG image as PNG Buffer.\n */\nexport async function generateOgImage(options: OgGenerateOptions): Promise<Buffer> {\n\tconst {\n\t\ttitle,\n\t\tdescription,\n\t\tsiteName,\n\t\timage,\n\t\ttemplate = 'blog',\n\t\tcolors: colorOverrides,\n\t\tfonts: fontConfig,\n\t\tnoise: noiseConfig,\n\t\tnoiseSeed,\n\t\twidth = OG_WIDTH,\n\t\theight = OG_HEIGHT,\n\t\tdebugSvg = false,\n\t} = options\n\n\t// Merge colours\n\tconst colors: OgColorConfig = {\n\t\t...defaultColors,\n\t\t...colorOverrides,\n\t}\n\n\t// Load fonts\n\tconst fonts = await loadFonts(fontConfig)\n\tconst satoriFonts = createSatoriFonts(fonts)\n\n\t// Generate noise background\n\tconst noiseEnabled = noiseConfig?.enabled !== false\n\tconst noiseSeedValue = noiseSeed || noiseConfig?.seed || title\n\tconst noiseDataUrl = noiseEnabled\n\t\t? generateNoiseDataUrl({\n\t\t\t\tseed: noiseSeedValue,\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t\topacity: noiseConfig?.opacity ?? 0.4,\n\t\t\t\tcolorMode: noiseConfig?.colorMode ?? 'grayscale',\n\t\t\t})\n\t\t: undefined\n\n\t// Generate circular noise decoration\n\tconst circleNoiseDataUrl = noiseEnabled\n\t\t? generateCircleNoiseDataUrl({\n\t\t\t\tseed: `${noiseSeedValue}-circle`,\n\t\t\t\tsize: 200,\n\t\t\t\topacity: noiseConfig?.opacity ?? 0.15,\n\t\t\t\tcolorMode: noiseConfig?.colorMode ?? 'grayscale',\n\t\t\t})\n\t\t: undefined\n\n\t// Get template function\n\tconst templateFn = getTemplate(template as Parameters<typeof getTemplate>[0])\n\n\t// Build template props\n\tconst props: OgTemplateProps = {\n\t\ttitle,\n\t\tdescription,\n\t\tsiteName,\n\t\timage,\n\t\tcolors,\n\t\tnoiseDataUrl,\n\t\tcircleNoiseDataUrl,\n\t\twidth,\n\t\theight,\n\t}\n\n\t// Render template to Satori-compatible structure\n\tconst element = templateFn(props)\n\n\t// Generate SVG with satori\n\tconst svg = await satori(element as Parameters<typeof satori>[0], {\n\t\twidth,\n\t\theight,\n\t\tfonts: satoriFonts,\n\t})\n\n\t// Debug: return SVG string\n\tif (debugSvg) {\n\t\treturn Buffer.from(svg)\n\t}\n\n\t// Convert SVG to PNG with resvg-js\n\tconst resvg = new Resvg(svg, {\n\t\tfitTo: {\n\t\t\tmode: 'width',\n\t\t\tvalue: width,\n\t\t},\n\t})\n\tconst pngData = resvg.render()\n\n\treturn Buffer.from(pngData.asPng())\n}\n\n/**\n * Generate OG image and return as base64 data URL.\n */\nexport async function generateOgImageDataUrl(options: OgGenerateOptions): Promise<string> {\n\tconst png = await generateOgImage(options)\n\treturn `data:image/png;base64,${png.toString('base64')}`\n}\n\n/**\n * Generate OG image and return as Response (for SvelteKit endpoints).\n */\nexport async function generateOgResponse(options: OgGenerateOptions, cacheMaxAge = 3600): Promise<Response> {\n\tconst png = await generateOgImage(options)\n\n\treturn new Response(png, {\n\t\theaders: {\n\t\t\t'Content-Type': 'image/png',\n\t\t\t'Cache-Control': `public, max-age=${cacheMaxAge}`,\n\t\t},\n\t})\n}\n","/**\n * @ewanc26/og fonts\n *\n * Font loading utilities. Bundles Inter font by default.\n */\n\nimport { readFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { OgFontConfig } from './types.js'\nimport { loadEmbeddedFonts } from './fonts-data.js'\n\n// Declare __dirname for CJS contexts (injected by bundlers)\ndeclare const __dirname: string | undefined\n\n// ─── Paths ────────────────────────────────────────────────────────────────────\n\n/**\n * Get the directory of the current module.\n * Works in both ESM and bundled contexts.\n */\nfunction getModuleDir(): string {\n\t// ESM context\n\tif (typeof import.meta !== 'undefined' && import.meta.url) {\n\t\treturn dirname(fileURLToPath(import.meta.url))\n\t}\n\t// Bundled CJS context - __dirname is injected by bundlers\n\tif (typeof __dirname !== 'undefined') {\n\t\treturn __dirname\n\t}\n\t// Fallback\n\treturn resolve(process.cwd(), 'node_modules/@ewanc26/og/dist')\n}\n\n/**\n * Resolve the fonts directory relative to the installed package.\n * Tries multiple possible locations for serverless compatibility.\n */\nfunction getFontsDir(): string {\n\tconst candidates = [\n\t\t// Standard: fonts next to dist\n\t\tresolve(getModuleDir(), '../fonts'),\n\t\t// Vercel serverless: fonts inside dist\n\t\tresolve(getModuleDir(), 'fonts'),\n\t\t// Fallback: node_modules path\n\t\tresolve(process.cwd(), 'node_modules/@ewanc26/og/fonts'),\n\t]\n\n\tfor (const dir of candidates) {\n\t\tif (existsSync(dir)) {\n\t\t\treturn dir\n\t\t}\n\t}\n\n\t// Return first candidate as fallback (will fail gracefully)\n\treturn candidates[0]\n}\n\n/**\n * Resolve bundled font paths. Uses getters to defer resolution until runtime.\n */\nexport const BUNDLED_FONTS = {\n\tget heading() {\n\t\treturn resolve(getFontsDir(), 'Inter-Bold.ttf')\n\t},\n\tget body() {\n\t\treturn resolve(getFontsDir(), 'Inter-Regular.ttf')\n\t},\n} as const\n\n// ─── Font Loading ──────────────────────────────────────────────────────────────\n\nexport interface LoadedFonts {\n\theading: ArrayBuffer\n\tbody: ArrayBuffer\n}\n\n/**\n * Helper to convert Buffer to ArrayBuffer\n */\nfunction toArrayBuffer(buf: Buffer): ArrayBuffer {\n\treturn buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer\n}\n\n/**\n * Load fonts from config, falling back to bundled fonts.\n */\nexport async function loadFonts(config?: OgFontConfig): Promise<LoadedFonts> {\n\tconst headingPath = config?.heading ?? BUNDLED_FONTS.heading\n\tconst bodyPath = config?.body ?? BUNDLED_FONTS.body\n\n\tconst [heading, body] = await Promise.all([\n\t\tloadFontFile(headingPath),\n\t\tloadFontFile(bodyPath),\n\t])\n\n\treturn { heading, body }\n}\n\n/**\n * Load a font from file path.\n * Falls back to alternative locations if local file not found.\n */\nasync function loadFontFile(source: string): Promise<ArrayBuffer> {\n\ttry {\n\t\tconst buffer = await readFile(source)\n\t\treturn toArrayBuffer(buffer)\n\t} catch (error) {\n\t\t// Try embedded fonts (loaded from alternative locations)\n\t\tconst embedded = await loadEmbeddedFonts()\n\t\tif (embedded) {\n\t\t\treturn source.includes('Bold') ? embedded.heading : embedded.body\n\t\t}\n\t\tthrow new Error(`Failed to load font from ${source}`)\n\t}\n}\n\n// ─── Font Registration for Satori ─────────────────────────────────────────────\n\nexport type SatoriFontConfig = {\n\tname: string\n\tdata: ArrayBuffer\n\tweight: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900\n\tstyle: 'normal' | 'italic'\n}\n\n/**\n * Create Satori-compatible font config from loaded fonts.\n * Uses Inter font family with weight 700 for headings and 400 for body.\n */\nexport function createSatoriFonts(fonts: LoadedFonts): SatoriFontConfig[] {\n\treturn [\n\t\t{\n\t\t\tname: 'Inter',\n\t\t\tdata: fonts.heading,\n\t\t\tweight: 700,\n\t\t\tstyle: 'normal',\n\t\t},\n\t\t{\n\t\t\tname: 'Inter',\n\t\t\tdata: fonts.body,\n\t\t\tweight: 400,\n\t\t\tstyle: 'normal',\n\t\t},\n\t]\n}\n","/**\n * Font loading helper for serverless environments\n */\n\nimport { readFile } from 'node:fs/promises'\nimport { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n// Declare __dirname for CJS contexts (injected by bundlers)\ndeclare const __dirname: string | undefined\n\nfunction getModuleDir(): string {\n\tif (typeof import.meta !== 'undefined' && import.meta.url) {\n\t\treturn dirname(fileURLToPath(import.meta.url))\n\t}\n\tif (typeof __dirname !== 'undefined') {\n\t\treturn __dirname\n\t}\n\treturn resolve(process.cwd(), 'node_modules/@ewanc26/og/dist')\n}\n\nexport interface FontData {\n\theading: ArrayBuffer\n\tbody: ArrayBuffer\n}\n\n/**\n * Try loading fonts from dist/fonts directory\n */\nexport async function loadEmbeddedFonts(): Promise<FontData | null> {\n\tconst moduleDir = getModuleDir()\n\n\tconst paths = [\n\t\t{\n\t\t\theading: resolve(moduleDir, 'fonts/Inter-Bold.ttf'),\n\t\t\tbody: resolve(moduleDir, 'fonts/Inter-Regular.ttf'),\n\t\t},\n\t\t{\n\t\t\theading: resolve(moduleDir, '../fonts/Inter-Bold.ttf'),\n\t\t\tbody: resolve(moduleDir, '../fonts/Inter-Regular.ttf'),\n\t\t},\n\t]\n\n\tfor (const p of paths) {\n\t\ttry {\n\t\t\tconst [headingBuf, bodyBuf] = await Promise.all([\n\t\t\t\treadFile(p.heading),\n\t\t\t\treadFile(p.body),\n\t\t\t])\n\t\t\t// Convert Buffer to ArrayBuffer\n\t\t\treturn {\n\t\t\t\theading: headingBuf.buffer.slice(headingBuf.byteOffset, headingBuf.byteOffset + headingBuf.byteLength),\n\t\t\t\tbody: bodyBuf.buffer.slice(bodyBuf.byteOffset, bodyBuf.byteOffset + bodyBuf.byteLength),\n\t\t\t}\n\t\t} catch {\n\t\t\tcontinue\n\t\t}\n\t}\n\n\treturn null\n}\n","/**\n * @ewanc26/og noise\n */\n\nimport { generateNoisePixels } from '@ewanc26/noise'\nimport { PNGEncoder } from './png-encoder.js'\nimport type { OgNoiseConfig } from './types.js'\n\nexport interface NoiseOptions {\n\tseed: string\n\twidth: number\n\theight: number\n\topacity?: number\n\tcolorMode?: OgNoiseConfig['colorMode']\n}\n\nexport interface CircleNoiseOptions {\n\tseed: string\n\tsize: number\n\topacity?: number\n\tcolorMode?: OgNoiseConfig['colorMode']\n}\n\n/**\n * Generate a noise PNG as a data URL.\n */\nexport function generateNoiseDataUrl(options: NoiseOptions): string {\n\tconst { seed, width, height, opacity = 0.4, colorMode = 'grayscale' } = options\n\n\tconst pixels = generateNoisePixels(width, height, seed, {\n\t\tgridSize: 4,\n\t\toctaves: 3,\n\t\tcolorMode: colorMode === 'grayscale'\n\t\t\t? { type: 'grayscale', range: [20, 60] }\n\t\t\t: { type: 'hsl', hueRange: 40, saturationRange: [30, 50], lightnessRange: [30, 50] }\n\t})\n\n\tif (opacity < 1) {\n\t\tfor (let i = 3; i < pixels.length; i += 4) {\n\t\t\tpixels[i] = Math.round(pixels[i] * opacity)\n\t\t}\n\t}\n\n\tconst pngBuffer = PNGEncoder.encode(pixels, width, height)\n\treturn `data:image/png;base64,${pngBuffer.toString('base64')}`\n}\n\n/**\n * Generate a circular noise PNG as a data URL.\n * Creates a square image with circular transparency mask.\n */\nexport function generateCircleNoiseDataUrl(options: CircleNoiseOptions): string {\n\tconst { seed, size, opacity = 0.15, colorMode = 'grayscale' } = options\n\n\tconst pixels = generateNoisePixels(size, size, seed, {\n\t\tgridSize: 4,\n\t\toctaves: 3,\n\t\tcolorMode: colorMode === 'grayscale'\n\t\t\t? { type: 'grayscale', range: [30, 70] }\n\t\t\t: { type: 'hsl', hueRange: 40, saturationRange: [30, 50], lightnessRange: [30, 50] }\n\t})\n\n\tconst center = size / 2\n\tconst radius = size / 2\n\n\t// Apply circular mask\n\tfor (let y = 0; y < size; y++) {\n\t\tfor (let x = 0; x < size; x++) {\n\t\t\tconst idx = (y * size + x) * 4\n\t\t\tconst dx = x - center + 0.5\n\t\t\tconst dy = y - center + 0.5\n\t\t\tconst dist = Math.sqrt(dx * dx + dy * dy)\n\n\t\t\tif (dist > radius) {\n\t\t\t\t// Outside circle - fully transparent\n\t\t\t\tpixels[idx + 3] = 0\n\t\t\t} else if (dist > radius - 2) {\n\t\t\t\t// Anti-alias edge\n\t\t\t\tconst edgeOpacity = (radius - dist) / 2\n\t\t\t\tpixels[idx + 3] = Math.round(255 * edgeOpacity * opacity)\n\t\t\t} else {\n\t\t\t\t// Inside circle - apply opacity\n\t\t\t\tpixels[idx + 3] = Math.round(255 * opacity)\n\t\t\t}\n\t\t}\n\t}\n\n\tconst pngBuffer = PNGEncoder.encode(pixels, size, size)\n\treturn `data:image/png;base64,${pngBuffer.toString('base64')}`\n}\n","/**\n * Minimal PNG encoder for noise backgrounds.\n * Uses node:zlib for deflate compression.\n */\n\nimport { deflateSync } from 'node:zlib'\n\nconst PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])\n\nfunction crc32(data: Buffer): number {\n\tlet crc = 0xffffffff\n\tconst table: number[] = []\n\n\t// Build CRC table\n\tfor (let n = 0; n < 256; n++) {\n\t\tlet c = n\n\t\tfor (let k = 0; k < 8; k++) {\n\t\t\tc = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1\n\t\t}\n\t\ttable[n] = c\n\t}\n\n\t// Calculate CRC\n\tfor (let i = 0; i < data.length; i++) {\n\t\tcrc = table[(crc ^ data[i]) & 0xff] ^ (crc >>> 8)\n\t}\n\n\treturn (crc ^ 0xffffffff) >>> 0\n}\n\nfunction createChunk(type: string, data: Buffer): Buffer {\n\tconst length = Buffer.alloc(4)\n\tlength.writeUInt32BE(data.length, 0)\n\n\tconst typeBuffer = Buffer.from(type, 'ascii')\n\tconst crcData = Buffer.concat([typeBuffer, data])\n\tconst crc = Buffer.alloc(4)\n\tcrc.writeUInt32BE(crc32(crcData), 0)\n\n\treturn Buffer.concat([length, typeBuffer, data, crc])\n}\n\nfunction createIHDR(width: number, height: number): Buffer {\n\tconst data = Buffer.alloc(13)\n\tdata.writeUInt32BE(width, 0) // Width\n\tdata.writeUInt32BE(height, 4) // Height\n\tdata.writeUInt8(8, 8) // Bit depth: 8 bits\n\tdata.writeUInt8(2, 9) // Colour type: 2 (RGB)\n\tdata.writeUInt8(0, 10) // Compression method\n\tdata.writeUInt8(0, 11) // Filter method\n\tdata.writeUInt8(0, 12) // Interlace method\n\n\treturn createChunk('IHDR', data)\n}\n\nfunction createIDAT(pixels: Uint8ClampedArray, width: number, height: number): Buffer {\n\t// Apply filter (none filter = 0) per row\n\tconst rawData = Buffer.alloc(height * (width * 3 + 1))\n\n\tlet srcOffset = 0\n\tlet dstOffset = 0\n\n\tfor (let y = 0; y < height; y++) {\n\t\trawData[dstOffset++] = 0 // Filter type: none\n\t\tfor (let x = 0; x < width; x++) {\n\t\t\tconst r = pixels[srcOffset++]\n\t\t\tconst g = pixels[srcOffset++]\n\t\t\tconst b = pixels[srcOffset++]\n\t\t\tsrcOffset++ // Skip alpha\n\t\t\trawData[dstOffset++] = r\n\t\t\trawData[dstOffset++] = g\n\t\t\trawData[dstOffset++] = b\n\t\t}\n\t}\n\n\tconst compressed = deflateSync(rawData)\n\treturn createChunk('IDAT', compressed)\n}\n\nfunction createIEND(): Buffer {\n\treturn createChunk('IEND', Buffer.alloc(0))\n}\n\n/**\n * Encode raw RGBA pixel data as a PNG Buffer.\n */\nexport function encodePNG(pixels: Uint8ClampedArray, width: number, height: number): Buffer {\n\treturn Buffer.concat([\n\t\tPNG_SIGNATURE,\n\t\tcreateIHDR(width, height),\n\t\tcreateIDAT(pixels, width, height),\n\t\tcreateIEND(),\n\t])\n}\n\n/**\n * PNGEncoder namespace for cleaner imports.\n */\nexport const PNGEncoder = {\n\tencode: encodePNG,\n}\n","/**\n * Blog OG template.\n * Clean centered layout.\n */\n\nimport type { OgTemplateProps } from '../types.js'\n\nexport function blogTemplate({\n\ttitle,\n\tdescription,\n\tsiteName,\n\tcolors,\n\tnoiseDataUrl,\n\twidth,\n\theight,\n}: OgTemplateProps) {\n\treturn {\n\t\ttype: 'div',\n\t\tprops: {\n\t\t\tstyle: {\n\t\t\t\tposition: 'relative',\n\t\t\t\tdisplay: 'flex',\n\t\t\t\tflexDirection: 'column',\n\t\t\t\talignItems: 'center',\n\t\t\t\tjustifyContent: 'center',\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t\tbackgroundColor: colors.background,\n\t\t\t},\n\t\t\tchildren: [\n\t\t\t\tnoiseDataUrl ? {\n\t\t\t\t\ttype: 'img',\n\t\t\t\t\tprops: {\n\t\t\t\t\t\tsrc: noiseDataUrl,\n\t\t\t\t\t\twidth,\n\t\t\t\t\t\theight,\n\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\tposition: 'absolute',\n\t\t\t\t\t\t\ttop: 0,\n\t\t\t\t\t\t\tleft: 0,\n\t\t\t\t\t\t\twidth,\n\t\t\t\t\t\t\theight,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t} : null,\n\t\t\t\t{\n\t\t\t\t\ttype: 'div',\n\t\t\t\t\tprops: {\n\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\tposition: 'relative',\n\t\t\t\t\t\t\tdisplay: 'flex',\n\t\t\t\t\t\t\tflexDirection: 'column',\n\t\t\t\t\t\t\talignItems: 'center',\n\t\t\t\t\t\t\tjustifyContent: 'center',\n\t\t\t\t\t\t\twidth,\n\t\t\t\t\t\t\theight,\n\t\t\t\t\t\t\tpadding: '0 60px',\n\t\t\t\t\t\t},\n\t\t\t\t\t\tchildren: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: 'h1',\n\t\t\t\t\t\t\t\tprops: {\n\t\t\t\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\t\t\t\tfontSize: 64,\n\t\t\t\t\t\t\t\t\t\tfontWeight: 700,\n\t\t\t\t\t\t\t\t\t\tcolor: colors.text,\n\t\t\t\t\t\t\t\t\t\tletterSpacing: '-0.02em',\n\t\t\t\t\t\t\t\t\t\tmargin: 0,\n\t\t\t\t\t\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\t\t\t\t\t\tlineHeight: 1.1,\n\t\t\t\t\t\t\t\t\t\tmaxWidth: 1000,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tchildren: title,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tdescription ? {\n\t\t\t\t\t\t\t\ttype: 'p',\n\t\t\t\t\t\t\t\tprops: {\n\t\t\t\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\t\t\t\tfontSize: 28,\n\t\t\t\t\t\t\t\t\t\tfontWeight: 400,\n\t\t\t\t\t\t\t\t\t\tcolor: colors.accent,\n\t\t\t\t\t\t\t\t\t\tmarginTop: 28,\n\t\t\t\t\t\t\t\t\t\tmarginBottom: 0,\n\t\t\t\t\t\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\t\t\t\t\t\tlineHeight: 1.4,\n\t\t\t\t\t\t\t\t\t\tmaxWidth: 900,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tchildren: description,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t} : null,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: 'p',\n\t\t\t\t\t\t\t\tprops: {\n\t\t\t\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\t\t\t\tfontSize: 24,\n\t\t\t\t\t\t\t\t\t\tfontWeight: 400,\n\t\t\t\t\t\t\t\t\t\tcolor: colors.accent,\n\t\t\t\t\t\t\t\t\t\tmarginTop: 56,\n\t\t\t\t\t\t\t\t\t\tmarginBottom: 0,\n\t\t\t\t\t\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\t\t\t\t\t\topacity: 0.7,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tchildren: siteName,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t].filter(Boolean),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t].filter(Boolean),\n\t\t},\n\t}\n}\n","/**\n * Profile OG template.\n * Centered layout.\n */\n\nimport type { OgTemplateProps } from '../types.js'\n\nexport function profileTemplate({\n\ttitle,\n\tdescription,\n\tsiteName,\n\timage,\n\tcolors,\n\tnoiseDataUrl,\n\twidth,\n\theight,\n}: OgTemplateProps) {\n\tconst contentChildren: unknown[] = []\n\n\tif (image) {\n\t\tcontentChildren.push({\n\t\t\ttype: 'img',\n\t\t\tprops: {\n\t\t\t\tsrc: image,\n\t\t\t\twidth: 120,\n\t\t\t\theight: 120,\n\t\t\t\tstyle: {\n\t\t\t\t\tborderRadius: '50%',\n\t\t\t\t\tmarginBottom: 32,\n\t\t\t\t\tobjectFit: 'cover',\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t}\n\n\tcontentChildren.push({\n\t\ttype: 'h1',\n\t\tprops: {\n\t\t\tstyle: {\n\t\t\t\tfontSize: 56,\n\t\t\t\tfontWeight: 700,\n\t\t\t\tcolor: colors.text,\n\t\t\t\tletterSpacing: '-0.02em',\n\t\t\t\tmargin: 0,\n\t\t\t\ttextAlign: 'center',\n\t\t\t\tlineHeight: 1.1,\n\t\t\t\tmaxWidth: 900,\n\t\t\t},\n\t\t\tchildren: title,\n\t\t},\n\t})\n\n\tif (description) {\n\t\tcontentChildren.push({\n\t\t\ttype: 'p',\n\t\t\tprops: {\n\t\t\t\tstyle: {\n\t\t\t\t\tfontSize: 26,\n\t\t\t\t\tfontWeight: 400,\n\t\t\t\t\tcolor: colors.accent,\n\t\t\t\t\tmarginTop: 20,\n\t\t\t\t\tmarginBottom: 0,\n\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\tlineHeight: 1.4,\n\t\t\t\t\tmaxWidth: 700,\n\t\t\t\t},\n\t\t\t\tchildren: description,\n\t\t\t},\n\t\t})\n\t}\n\n\tcontentChildren.push({\n\t\ttype: 'p',\n\t\tprops: {\n\t\t\tstyle: {\n\t\t\t\tfontSize: 24,\n\t\t\t\tfontWeight: 400,\n\t\t\t\tcolor: colors.accent,\n\t\t\t\tmarginTop: 48,\n\t\t\t\tmarginBottom: 0,\n\t\t\t\ttextAlign: 'center',\n\t\t\t\topacity: 0.7,\n\t\t\t},\n\t\t\tchildren: siteName,\n\t\t},\n\t})\n\n\treturn {\n\t\ttype: 'div',\n\t\tprops: {\n\t\t\tstyle: {\n\t\t\t\tposition: 'relative',\n\t\t\t\tdisplay: 'flex',\n\t\t\t\tflexDirection: 'column',\n\t\t\t\talignItems: 'center',\n\t\t\t\tjustifyContent: 'center',\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t\tbackgroundColor: colors.background,\n\t\t\t},\n\t\t\tchildren: [\n\t\t\t\tnoiseDataUrl ? {\n\t\t\t\t\ttype: 'img',\n\t\t\t\t\tprops: {\n\t\t\t\t\t\tsrc: noiseDataUrl,\n\t\t\t\t\t\twidth,\n\t\t\t\t\t\theight,\n\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\tposition: 'absolute',\n\t\t\t\t\t\t\ttop: 0,\n\t\t\t\t\t\t\tleft: 0,\n\t\t\t\t\t\t\twidth,\n\t\t\t\t\t\t\theight,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t} : null,\n\t\t\t\t{\n\t\t\t\t\ttype: 'div',\n\t\t\t\t\tprops: {\n\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\tposition: 'relative',\n\t\t\t\t\t\t\tdisplay: 'flex',\n\t\t\t\t\t\t\tflexDirection: 'column',\n\t\t\t\t\t\t\talignItems: 'center',\n\t\t\t\t\t\t\tjustifyContent: 'center',\n\t\t\t\t\t\t\twidth,\n\t\t\t\t\t\t\theight,\n\t\t\t\t\t\t\tpadding: '0 60px',\n\t\t\t\t\t\t},\n\t\t\t\t\t\tchildren: contentChildren,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t].filter(Boolean),\n\t\t},\n\t}\n}\n","/**\n * Default OG template.\n * Clean, centered layout.\n */\n\nimport type { OgTemplateProps } from '../types.js'\n\nexport function defaultTemplate({\n\ttitle,\n\tdescription,\n\tsiteName,\n\tcolors,\n\tnoiseDataUrl,\n\twidth,\n\theight,\n}: OgTemplateProps) {\n\treturn {\n\t\ttype: 'div',\n\t\tprops: {\n\t\t\tstyle: {\n\t\t\t\tposition: 'relative',\n\t\t\t\tdisplay: 'flex',\n\t\t\t\tflexDirection: 'column',\n\t\t\t\talignItems: 'center',\n\t\t\t\tjustifyContent: 'center',\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t\tbackgroundColor: colors.background,\n\t\t\t},\n\t\t\tchildren: [\n\t\t\t\tnoiseDataUrl ? {\n\t\t\t\t\ttype: 'img',\n\t\t\t\t\tprops: {\n\t\t\t\t\t\tsrc: noiseDataUrl,\n\t\t\t\t\t\twidth,\n\t\t\t\t\t\theight,\n\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\tposition: 'absolute',\n\t\t\t\t\t\t\ttop: 0,\n\t\t\t\t\t\t\tleft: 0,\n\t\t\t\t\t\t\twidth,\n\t\t\t\t\t\t\theight,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t} : null,\n\t\t\t\t{\n\t\t\t\t\ttype: 'div',\n\t\t\t\t\tprops: {\n\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\tposition: 'relative',\n\t\t\t\t\t\t\tdisplay: 'flex',\n\t\t\t\t\t\t\tflexDirection: 'column',\n\t\t\t\t\t\t\talignItems: 'center',\n\t\t\t\t\t\t\tjustifyContent: 'center',\n\t\t\t\t\t\t\twidth,\n\t\t\t\t\t\t\theight,\n\t\t\t\t\t\t\tpadding: '0 60px',\n\t\t\t\t\t\t},\n\t\t\t\t\t\tchildren: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: 'h1',\n\t\t\t\t\t\t\t\tprops: {\n\t\t\t\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\t\t\t\tfontSize: 72,\n\t\t\t\t\t\t\t\t\t\tfontWeight: 700,\n\t\t\t\t\t\t\t\t\t\tcolor: colors.text,\n\t\t\t\t\t\t\t\t\t\tletterSpacing: '-0.02em',\n\t\t\t\t\t\t\t\t\t\tmargin: 0,\n\t\t\t\t\t\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tchildren: title,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tdescription ? {\n\t\t\t\t\t\t\t\ttype: 'p',\n\t\t\t\t\t\t\t\tprops: {\n\t\t\t\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\t\t\t\tfontSize: 32,\n\t\t\t\t\t\t\t\t\t\tfontWeight: 400,\n\t\t\t\t\t\t\t\t\t\tcolor: colors.accent,\n\t\t\t\t\t\t\t\t\t\tmarginTop: 24,\n\t\t\t\t\t\t\t\t\t\tmarginBottom: 0,\n\t\t\t\t\t\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\t\t\t\t\t\tmaxWidth: 900,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tchildren: description,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t} : null,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: 'p',\n\t\t\t\t\t\t\t\tprops: {\n\t\t\t\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\t\t\t\tfontSize: 28,\n\t\t\t\t\t\t\t\t\t\tfontWeight: 400,\n\t\t\t\t\t\t\t\t\t\tcolor: colors.accent,\n\t\t\t\t\t\t\t\t\t\tmarginTop: 64,\n\t\t\t\t\t\t\t\t\t\tmarginBottom: 0,\n\t\t\t\t\t\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\t\t\t\t\t\topacity: 0.7,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tchildren: siteName,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t].filter(Boolean),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t].filter(Boolean),\n\t\t},\n\t}\n}\n","/**\n * Built-in OG templates.\n */\n\nexport { blogTemplate } from './blog.js'\nexport { profileTemplate } from './profile.js'\nexport { defaultTemplate } from './default.js'\n\nimport { blogTemplate } from './blog.js'\nimport { profileTemplate } from './profile.js'\nimport { defaultTemplate } from './default.js'\nimport type { OgTemplate } from '../types.js'\n\nexport const templates = {\n\tblog: blogTemplate,\n\tprofile: profileTemplate,\n\tdefault: defaultTemplate,\n} as const\n\nexport type TemplateName = keyof typeof templates\n\nexport function getTemplate(name: TemplateName | OgTemplate): OgTemplate {\n\tif (typeof name === 'function') {\n\t\treturn name\n\t}\n\treturn templates[name]\n}\n","/**\n * @ewanc26/og types\n */\n\n// ─── Colour Configuration ─────────────────────────────────────────────────────\n\nexport interface OgColorConfig {\n\t/** Background color (very dark). @default '#0f1a15' */\n\tbackground: string\n\t/** Primary text color. @default '#e8f5e9' */\n\ttext: string\n\t/** Secondary/accent text (mint). @default '#86efac' */\n\taccent: string\n}\n\nexport const defaultColors: OgColorConfig = {\n\tbackground: '#0f1a15',\n\ttext: '#e8f5e9',\n\taccent: '#86efac',\n}\n\n// ─── Font Configuration ───────────────────────────────────────────────────────\n\nexport interface OgFontConfig {\n\theading?: string\n\tbody?: string\n}\n\n// ─── Noise Configuration ──────────────────────────────────────────────────────\n\nexport interface OgNoiseConfig {\n\tenabled?: boolean\n\tseed?: string\n\topacity?: number\n\tcolorMode?: 'grayscale' | 'hsl'\n}\n\n// ─── Template Props ────────────────────────────────────────────────────────────\n\nexport interface OgTemplateProps {\n\ttitle: string\n\tdescription?: string\n\tsiteName: string\n\timage?: string\n\tcolors: OgColorConfig\n\tnoiseDataUrl?: string\n\tcircleNoiseDataUrl?: string\n\twidth: number\n\theight: number\n}\n\nexport type OgTemplate = (props: OgTemplateProps) => unknown\n\n// ─── Generation Options ───────────────────────────────────────────────────────\n\nexport interface OgGenerateOptions {\n\ttitle: string\n\tdescription?: string\n\tsiteName: string\n\timage?: string\n\ttemplate?: 'blog' | 'profile' | 'default' | OgTemplate\n\tcolors?: Partial<OgColorConfig>\n\tfonts?: OgFontConfig\n\tnoise?: OgNoiseConfig\n\tnoiseSeed?: string\n\twidth?: number\n\theight?: number\n\tdebugSvg?: boolean\n}\n\n// ─── SvelteKit Endpoint Options ───────────────────────────────────────────────\n\nexport interface OgEndpointOptions {\n\tsiteName: string\n\tdefaultTemplate?: 'blog' | 'profile' | 'default' | OgTemplate\n\tcolors?: Partial<OgColorConfig>\n\tfonts?: OgFontConfig\n\tnoise?: OgNoiseConfig\n\tcacheMaxAge?: number\n\twidth?: number\n\theight?: number\n}\n\n// ─── Internal Types ────────────────────────────────────────────────────────────\n\nexport interface InternalGenerateContext {\n\twidth: number\n\theight: number\n\tfonts: { heading: ArrayBuffer; body: ArrayBuffer }\n\tcolors: OgColorConfig\n\tnoiseDataUrl?: string\n}\n","/**\n * SvelteKit endpoint helpers.\n */\n\nimport { generateOgResponse } from './generate.js'\nimport type { OgEndpointOptions, OgGenerateOptions } from './types.js'\n\n/**\n * Create a SvelteKit GET handler for OG image generation.\n *\n * @example\n * ```ts\n * // src/routes/og/[title]/+server.ts\n * import { createOgEndpoint } from '@ewanc26/og';\n *\n * export const GET = createOgEndpoint({\n * siteName: 'ewancroft.uk',\n * defaultTemplate: 'blog',\n * });\n * ```\n *\n * The endpoint expects query parameters:\n * - `title` (required): Page title\n * - `description`: Optional description\n * - `image`: Optional avatar/logo URL\n * - `seed`: Optional noise seed\n */\nexport function createOgEndpoint(options: OgEndpointOptions) {\n\tconst {\n\t\tsiteName,\n\t\tdefaultTemplate: template = 'default',\n\t\tcolors,\n\t\tfonts,\n\t\tnoise,\n\t\tcacheMaxAge = 3600,\n\t\twidth,\n\t\theight,\n\t} = options\n\n\treturn async ({ url }: { url: URL }) => {\n\t\tconst title = url.searchParams.get('title')\n\t\tconst description = url.searchParams.get('description') ?? undefined\n\t\tconst image = url.searchParams.get('image') ?? undefined\n\t\tconst noiseSeed = url.searchParams.get('seed') ?? undefined\n\t\tconst templateParam = url.searchParams.get('template') as 'blog' | 'profile' | 'default' | null\n\t\tconst resolvedTemplate: OgGenerateOptions['template'] = templateParam ?? template\n\n\t\tif (!title) {\n\t\t\treturn new Response('Missing title parameter', { status: 400 })\n\t\t}\n\n\t\ttry {\n\t\t\treturn await generateOgResponse(\n\t\t\t\t{\n\t\t\t\t\ttitle,\n\t\t\t\t\tdescription,\n\t\t\t\t\tsiteName,\n\t\t\t\t\timage,\n\t\t\t\t\ttemplate: resolvedTemplate,\n\t\t\t\t\tcolors,\n\t\t\t\t\tfonts,\n\t\t\t\t\tnoise,\n\t\t\t\t\tnoiseSeed,\n\t\t\t\t\twidth,\n\t\t\t\t\theight,\n\t\t\t\t},\n\t\t\t\tcacheMaxAge\n\t\t\t)\n\t\t} catch (error) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : String(error)\n\t\t\tconsole.error('Failed to generate OG image:', error)\n\t\t\treturn new Response(`Failed to generate image: ${errorMessage}`, { status: 500 })\n\t\t}\n\t}\n}\n\n/**\n * Create a typed OG image URL for use in meta tags.\n */\nexport function createOgImageUrl(\n\tbaseUrl: string,\n\tparams: {\n\t\ttitle: string\n\t\tdescription?: string\n\t\timage?: string\n\t\tseed?: string\n\t}\n): string {\n\tconst url = new URL(baseUrl)\n\turl.searchParams.set('title', params.title)\n\tif (params.description) url.searchParams.set('description', params.description)\n\tif (params.image) url.searchParams.set('image', params.image)\n\tif (params.seed) url.searchParams.set('seed', params.seed)\n\treturn url.toString()\n}\n","/**\n * SVG to PNG conversion using @resvg/resvg-js.\n */\n\nimport { Resvg } from '@resvg/resvg-js'\n\nexport interface SvgToPngOptions {\n\t/** Scale to fit width in pixels */\n\tfitWidth?: number\n\t/** Background colour for transparent areas */\n\tbackgroundColor?: string\n}\n\n/**\n * Convert an SVG string to PNG Buffer.\n */\nexport function svgToPng(svg: string, options: SvgToPngOptions = {}): Buffer {\n\tconst opts = {\n\t\tfitTo: options.fitWidth\n\t\t\t? { mode: 'width' as const, value: options.fitWidth }\n\t\t\t: undefined,\n\t\tbackground: options.backgroundColor,\n\t}\n\n\tconst resvg = new Resvg(svg, opts)\n\tconst rendered = resvg.render()\n\n\treturn Buffer.from(rendered.asPng())\n}\n\n/**\n * Convert an SVG string to PNG data URL.\n */\nexport function svgToPngDataUrl(svg: string, options: SvgToPngOptions = {}): string {\n\tconst png = svgToPng(svg, options)\n\treturn `data:image/png;base64,${png.toString('base64')}`\n}\n\n/**\n * Convert an SVG string to PNG Response (for SvelteKit endpoints).\n */\nexport function svgToPngResponse(svg: string, options: SvgToPngOptions = {}, cacheMaxAge = 3600): Response {\n\tconst png = svgToPng(svg, options)\n\n\treturn new Response(png, {\n\t\theaders: {\n\t\t\t'Content-Type': 'image/png',\n\t\t\t'Cache-Control': `public, max-age=${cacheMaxAge}`,\n\t\t},\n\t})\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKA,IAAM,mBAAmB,MACvB,OAAO,aAAa,cAChB,IAAI,IAAI,QAAQ,UAAU,EAAE,EAAE,OAC7B,SAAS,iBAAiB,SAAS,cAAc,QAAQ,YAAY,MAAM,WAC1E,SAAS,cAAc,MACvB,IAAI,IAAI,WAAW,SAAS,OAAO,EAAE;AAEtC,IAAM,gBAAgC,iCAAiB;;;ACP9D,oBAAmB;AACnB,sBAAsB;;;ACAtB,IAAAA,mBAAyB;AACzB,qBAA2B;AAC3B,IAAAC,oBAAiC;AACjC,IAAAC,mBAA8B;;;ACL9B,sBAAyB;AACzB,uBAAiC;AACjC,sBAA8B;AAN9B;AAWA,SAAS,eAAuB;AAC/B,MAAI,OAAO,gBAAgB,eAAe,eAAiB;AAC1D,eAAO,8BAAQ,+BAAc,aAAe,CAAC;AAAA,EAC9C;AACA,MAAI,OAAO,cAAc,aAAa;AACrC,WAAO;AAAA,EACR;AACA,aAAO,0BAAQ,QAAQ,IAAI,GAAG,+BAA+B;AAC9D;AAUA,eAAsB,oBAA8C;AACnE,QAAM,YAAY,aAAa;AAE/B,QAAM,QAAQ;AAAA,IACb;AAAA,MACC,aAAS,0BAAQ,WAAW,sBAAsB;AAAA,MAClD,UAAM,0BAAQ,WAAW,yBAAyB;AAAA,IACnD;AAAA,IACA;AAAA,MACC,aAAS,0BAAQ,WAAW,yBAAyB;AAAA,MACrD,UAAM,0BAAQ,WAAW,4BAA4B;AAAA,IACtD;AAAA,EACD;AAEA,aAAW,KAAK,OAAO;AACtB,QAAI;AACH,YAAM,CAAC,YAAY,OAAO,IAAI,MAAM,QAAQ,IAAI;AAAA,YAC/C,0BAAS,EAAE,OAAO;AAAA,YAClB,0BAAS,EAAE,IAAI;AAAA,MAChB,CAAC;AAED,aAAO;AAAA,QACN,SAAS,WAAW,OAAO,MAAM,WAAW,YAAY,WAAW,aAAa,WAAW,UAAU;AAAA,QACrG,MAAM,QAAQ,OAAO,MAAM,QAAQ,YAAY,QAAQ,aAAa,QAAQ,UAAU;AAAA,MACvF;AAAA,IACD,QAAQ;AACP;AAAA,IACD;AAAA,EACD;AAEA,SAAO;AACR;;;AD5DA,IAAAC,eAAA;AAsBA,SAASC,gBAAuB;AAE/B,MAAI,OAAOD,iBAAgB,eAAe,eAAiB;AAC1D,eAAO,+BAAQ,gCAAc,aAAe,CAAC;AAAA,EAC9C;AAEA,MAAI,OAAO,cAAc,aAAa;AACrC,WAAO;AAAA,EACR;AAEA,aAAO,2BAAQ,QAAQ,IAAI,GAAG,+BAA+B;AAC9D;AAMA,SAAS,cAAsB;AAC9B,QAAM,aAAa;AAAA;AAAA,QAElB,2BAAQC,cAAa,GAAG,UAAU;AAAA;AAAA,QAElC,2BAAQA,cAAa,GAAG,OAAO;AAAA;AAAA,QAE/B,2BAAQ,QAAQ,IAAI,GAAG,gCAAgC;AAAA,EACxD;AAEA,aAAW,OAAO,YAAY;AAC7B,YAAI,2BAAW,GAAG,GAAG;AACpB,aAAO;AAAA,IACR;AAAA,EACD;AAGA,SAAO,WAAW,CAAC;AACpB;AAKO,IAAM,gBAAgB;AAAA,EAC5B,IAAI,UAAU;AACb,eAAO,2BAAQ,YAAY,GAAG,gBAAgB;AAAA,EAC/C;AAAA,EACA,IAAI,OAAO;AACV,eAAO,2BAAQ,YAAY,GAAG,mBAAmB;AAAA,EAClD;AACD;AAYA,SAAS,cAAc,KAA0B;AAChD,SAAO,IAAI,OAAO,MAAM,IAAI,YAAY,IAAI,aAAa,IAAI,UAAU;AACxE;AAKA,eAAsB,UAAU,QAA6C;AAC5E,QAAM,cAAc,QAAQ,WAAW,cAAc;AACrD,QAAM,WAAW,QAAQ,QAAQ,cAAc;AAE/C,QAAM,CAAC,SAAS,IAAI,IAAI,MAAM,QAAQ,IAAI;AAAA,IACzC,aAAa,WAAW;AAAA,IACxB,aAAa,QAAQ;AAAA,EACtB,CAAC;AAED,SAAO,EAAE,SAAS,KAAK;AACxB;AAMA,eAAe,aAAa,QAAsC;AACjE,MAAI;AACH,UAAM,SAAS,UAAM,2BAAS,MAAM;AACpC,WAAO,cAAc,MAAM;AAAA,EAC5B,SAAS,OAAO;AAEf,UAAM,WAAW,MAAM,kBAAkB;AACzC,QAAI,UAAU;AACb,aAAO,OAAO,SAAS,MAAM,IAAI,SAAS,UAAU,SAAS;AAAA,IAC9D;AACA,UAAM,IAAI,MAAM,4BAA4B,MAAM,EAAE;AAAA,EACrD;AACD;AAeO,SAAS,kBAAkB,OAAwC;AACzE,SAAO;AAAA,IACN;AAAA,MACC,MAAM;AAAA,MACN,MAAM,MAAM;AAAA,MACZ,QAAQ;AAAA,MACR,OAAO;AAAA,IACR;AAAA,IACA;AAAA,MACC,MAAM;AAAA,MACN,MAAM,MAAM;AAAA,MACZ,QAAQ;AAAA,MACR,OAAO;AAAA,IACR;AAAA,EACD;AACD;;;AE9IA,mBAAoC;;;ACCpC,uBAA4B;AAE5B,IAAM,gBAAgB,OAAO,KAAK,CAAC,KAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,EAAI,CAAC;AAElF,SAAS,MAAM,MAAsB;AACpC,MAAI,MAAM;AACV,QAAM,QAAkB,CAAC;AAGzB,WAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC7B,QAAI,IAAI;AACR,aAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC3B,UAAI,IAAI,IAAI,aAAc,MAAM,IAAK,MAAM;AAAA,IAC5C;AACA,UAAM,CAAC,IAAI;AAAA,EACZ;AAGA,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACrC,UAAM,OAAO,MAAM,KAAK,CAAC,KAAK,GAAI,IAAK,QAAQ;AAAA,EAChD;AAEA,UAAQ,MAAM,gBAAgB;AAC/B;AAEA,SAAS,YAAY,MAAc,MAAsB;AACxD,QAAM,SAAS,OAAO,MAAM,CAAC;AAC7B,SAAO,cAAc,KAAK,QAAQ,CAAC;AAEnC,QAAM,aAAa,OAAO,KAAK,MAAM,OAAO;AAC5C,QAAM,UAAU,OAAO,OAAO,CAAC,YAAY,IAAI,CAAC;AAChD,QAAM,MAAM,OAAO,MAAM,CAAC;AAC1B,MAAI,cAAc,MAAM,OAAO,GAAG,CAAC;AAEnC,SAAO,OAAO,OAAO,CAAC,QAAQ,YAAY,MAAM,GAAG,CAAC;AACrD;AAEA,SAAS,WAAW,OAAe,QAAwB;AAC1D,QAAM,OAAO,OAAO,MAAM,EAAE;AAC5B,OAAK,cAAc,OAAO,CAAC;AAC3B,OAAK,cAAc,QAAQ,CAAC;AAC5B,OAAK,WAAW,GAAG,CAAC;AACpB,OAAK,WAAW,GAAG,CAAC;AACpB,OAAK,WAAW,GAAG,EAAE;AACrB,OAAK,WAAW,GAAG,EAAE;AACrB,OAAK,WAAW,GAAG,EAAE;AAErB,SAAO,YAAY,QAAQ,IAAI;AAChC;AAEA,SAAS,WAAW,QAA2B,OAAe,QAAwB;AAErF,QAAM,UAAU,OAAO,MAAM,UAAU,QAAQ,IAAI,EAAE;AAErD,MAAI,YAAY;AAChB,MAAI,YAAY;AAEhB,WAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAChC,YAAQ,WAAW,IAAI;AACvB,aAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC/B,YAAM,IAAI,OAAO,WAAW;AAC5B,YAAM,IAAI,OAAO,WAAW;AAC5B,YAAM,IAAI,OAAO,WAAW;AAC5B;AACA,cAAQ,WAAW,IAAI;AACvB,cAAQ,WAAW,IAAI;AACvB,cAAQ,WAAW,IAAI;AAAA,IACxB;AAAA,EACD;AAEA,QAAM,iBAAa,8BAAY,OAAO;AACtC,SAAO,YAAY,QAAQ,UAAU;AACtC;AAEA,SAAS,aAAqB;AAC7B,SAAO,YAAY,QAAQ,OAAO,MAAM,CAAC,CAAC;AAC3C;AAKO,SAAS,UAAU,QAA2B,OAAe,QAAwB;AAC3F,SAAO,OAAO,OAAO;AAAA,IACpB;AAAA,IACA,WAAW,OAAO,MAAM;AAAA,IACxB,WAAW,QAAQ,OAAO,MAAM;AAAA,IAChC,WAAW;AAAA,EACZ,CAAC;AACF;AAKO,IAAM,aAAa;AAAA,EACzB,QAAQ;AACT;;;AD1EO,SAAS,qBAAqB,SAA+B;AACnE,QAAM,EAAE,MAAM,OAAO,QAAQ,UAAU,KAAK,YAAY,YAAY,IAAI;AAExE,QAAM,aAAS,kCAAoB,OAAO,QAAQ,MAAM;AAAA,IACvD,UAAU;AAAA,IACV,SAAS;AAAA,IACT,WAAW,cAAc,cACtB,EAAE,MAAM,aAAa,OAAO,CAAC,IAAI,EAAE,EAAE,IACrC,EAAE,MAAM,OAAO,UAAU,IAAI,iBAAiB,CAAC,IAAI,EAAE,GAAG,gBAAgB,CAAC,IAAI,EAAE,EAAE;AAAA,EACrF,CAAC;AAED,MAAI,UAAU,GAAG;AAChB,aAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,GAAG;AAC1C,aAAO,CAAC,IAAI,KAAK,MAAM,OAAO,CAAC,IAAI,OAAO;AAAA,IAC3C;AAAA,EACD;AAEA,QAAM,YAAY,WAAW,OAAO,QAAQ,OAAO,MAAM;AACzD,SAAO,yBAAyB,UAAU,SAAS,QAAQ,CAAC;AAC7D;AAMO,SAAS,2BAA2B,SAAqC;AAC/E,QAAM,EAAE,MAAM,MAAM,UAAU,MAAM,YAAY,YAAY,IAAI;AAEhE,QAAM,aAAS,kCAAoB,MAAM,MAAM,MAAM;AAAA,IACpD,UAAU;AAAA,IACV,SAAS;AAAA,IACT,WAAW,cAAc,cACtB,EAAE,MAAM,aAAa,OAAO,CAAC,IAAI,EAAE,EAAE,IACrC,EAAE,MAAM,OAAO,UAAU,IAAI,iBAAiB,CAAC,IAAI,EAAE,GAAG,gBAAgB,CAAC,IAAI,EAAE,EAAE;AAAA,EACrF,CAAC;AAED,QAAM,SAAS,OAAO;AACtB,QAAM,SAAS,OAAO;AAGtB,WAAS,IAAI,GAAG,IAAI,MAAM,KAAK;AAC9B,aAAS,IAAI,GAAG,IAAI,MAAM,KAAK;AAC9B,YAAM,OAAO,IAAI,OAAO,KAAK;AAC7B,YAAM,KAAK,IAAI,SAAS;AACxB,YAAM,KAAK,IAAI,SAAS;AACxB,YAAM,OAAO,KAAK,KAAK,KAAK,KAAK,KAAK,EAAE;AAExC,UAAI,OAAO,QAAQ;AAElB,eAAO,MAAM,CAAC,IAAI;AAAA,MACnB,WAAW,OAAO,SAAS,GAAG;AAE7B,cAAM,eAAe,SAAS,QAAQ;AACtC,eAAO,MAAM,CAAC,IAAI,KAAK,MAAM,MAAM,cAAc,OAAO;AAAA,MACzD,OAAO;AAEN,eAAO,MAAM,CAAC,IAAI,KAAK,MAAM,MAAM,OAAO;AAAA,MAC3C;AAAA,IACD;AAAA,EACD;AAEA,QAAM,YAAY,WAAW,OAAO,QAAQ,MAAM,IAAI;AACtD,SAAO,yBAAyB,UAAU,SAAS,QAAQ,CAAC;AAC7D;;;AElFO,SAAS,aAAa;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,GAAoB;AACnB,SAAO;AAAA,IACN,MAAM;AAAA,IACN,OAAO;AAAA,MACN,OAAO;AAAA,QACN,UAAU;AAAA,QACV,SAAS;AAAA,QACT,eAAe;AAAA,QACf,YAAY;AAAA,QACZ,gBAAgB;AAAA,QAChB;AAAA,QACA;AAAA,QACA,iBAAiB,OAAO;AAAA,MACzB;AAAA,MACA,UAAU;AAAA,QACT,eAAe;AAAA,UACd,MAAM;AAAA,UACN,OAAO;AAAA,YACN,KAAK;AAAA,YACL;AAAA,YACA;AAAA,YACA,OAAO;AAAA,cACN,UAAU;AAAA,cACV,KAAK;AAAA,cACL,MAAM;AAAA,cACN;AAAA,cACA;AAAA,YACD;AAAA,UACD;AAAA,QACD,IAAI;AAAA,QACJ;AAAA,UACC,MAAM;AAAA,UACN,OAAO;AAAA,YACN,OAAO;AAAA,cACN,UAAU;AAAA,cACV,SAAS;AAAA,cACT,eAAe;AAAA,cACf,YAAY;AAAA,cACZ,gBAAgB;AAAA,cAChB;AAAA,cACA;AAAA,cACA,SAAS;AAAA,YACV;AAAA,YACA,UAAU;AAAA,cACT;AAAA,gBACC,MAAM;AAAA,gBACN,OAAO;AAAA,kBACN,OAAO;AAAA,oBACN,UAAU;AAAA,oBACV,YAAY;AAAA,oBACZ,OAAO,OAAO;AAAA,oBACd,eAAe;AAAA,oBACf,QAAQ;AAAA,oBACR,WAAW;AAAA,oBACX,YAAY;AAAA,oBACZ,UAAU;AAAA,kBACX;AAAA,kBACA,UAAU;AAAA,gBACX;AAAA,cACD;AAAA,cACA,cAAc;AAAA,gBACb,MAAM;AAAA,gBACN,OAAO;AAAA,kBACN,OAAO;AAAA,oBACN,UAAU;AAAA,oBACV,YAAY;AAAA,oBACZ,OAAO,OAAO;AAAA,oBACd,WAAW;AAAA,oBACX,cAAc;AAAA,oBACd,WAAW;AAAA,oBACX,YAAY;AAAA,oBACZ,UAAU;AAAA,kBACX;AAAA,kBACA,UAAU;AAAA,gBACX;AAAA,cACD,IAAI;AAAA,cACJ;AAAA,gBACC,MAAM;AAAA,gBACN,OAAO;AAAA,kBACN,OAAO;AAAA,oBACN,UAAU;AAAA,oBACV,YAAY;AAAA,oBACZ,OAAO,OAAO;AAAA,oBACd,WAAW;AAAA,oBACX,cAAc;AAAA,oBACd,WAAW;AAAA,oBACX,SAAS;AAAA,kBACV;AAAA,kBACA,UAAU;AAAA,gBACX;AAAA,cACD;AAAA,YACD,EAAE,OAAO,OAAO;AAAA,UACjB;AAAA,QACD;AAAA,MACD,EAAE,OAAO,OAAO;AAAA,IACjB;AAAA,EACD;AACD;;;ACzGO,SAAS,gBAAgB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,GAAoB;AACnB,QAAM,kBAA6B,CAAC;AAEpC,MAAI,OAAO;AACV,oBAAgB,KAAK;AAAA,MACpB,MAAM;AAAA,MACN,OAAO;AAAA,QACN,KAAK;AAAA,QACL,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,OAAO;AAAA,UACN,cAAc;AAAA,UACd,cAAc;AAAA,UACd,WAAW;AAAA,QACZ;AAAA,MACD;AAAA,IACD,CAAC;AAAA,EACF;AAEA,kBAAgB,KAAK;AAAA,IACpB,MAAM;AAAA,IACN,OAAO;AAAA,MACN,OAAO;AAAA,QACN,UAAU;AAAA,QACV,YAAY;AAAA,QACZ,OAAO,OAAO;AAAA,QACd,eAAe;AAAA,QACf,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,YAAY;AAAA,QACZ,UAAU;AAAA,MACX;AAAA,MACA,UAAU;AAAA,IACX;AAAA,EACD,CAAC;AAED,MAAI,aAAa;AAChB,oBAAgB,KAAK;AAAA,MACpB,MAAM;AAAA,MACN,OAAO;AAAA,QACN,OAAO;AAAA,UACN,UAAU;AAAA,UACV,YAAY;AAAA,UACZ,OAAO,OAAO;AAAA,UACd,WAAW;AAAA,UACX,cAAc;AAAA,UACd,WAAW;AAAA,UACX,YAAY;AAAA,UACZ,UAAU;AAAA,QACX;AAAA,QACA,UAAU;AAAA,MACX;AAAA,IACD,CAAC;AAAA,EACF;AAEA,kBAAgB,KAAK;AAAA,IACpB,MAAM;AAAA,IACN,OAAO;AAAA,MACN,OAAO;AAAA,QACN,UAAU;AAAA,QACV,YAAY;AAAA,QACZ,OAAO,OAAO;AAAA,QACd,WAAW;AAAA,QACX,cAAc;AAAA,QACd,WAAW;AAAA,QACX,SAAS;AAAA,MACV;AAAA,MACA,UAAU;AAAA,IACX;AAAA,EACD,CAAC;AAED,SAAO;AAAA,IACN,MAAM;AAAA,IACN,OAAO;AAAA,MACN,OAAO;AAAA,QACN,UAAU;AAAA,QACV,SAAS;AAAA,QACT,eAAe;AAAA,QACf,YAAY;AAAA,QACZ,gBAAgB;AAAA,QAChB;AAAA,QACA;AAAA,QACA,iBAAiB,OAAO;AAAA,MACzB;AAAA,MACA,UAAU;AAAA,QACT,eAAe;AAAA,UACd,MAAM;AAAA,UACN,OAAO;AAAA,YACN,KAAK;AAAA,YACL;AAAA,YACA;AAAA,YACA,OAAO;AAAA,cACN,UAAU;AAAA,cACV,KAAK;AAAA,cACL,MAAM;AAAA,cACN;AAAA,cACA;AAAA,YACD;AAAA,UACD;AAAA,QACD,IAAI;AAAA,QACJ;AAAA,UACC,MAAM;AAAA,UACN,OAAO;AAAA,YACN,OAAO;AAAA,cACN,UAAU;AAAA,cACV,SAAS;AAAA,cACT,eAAe;AAAA,cACf,YAAY;AAAA,cACZ,gBAAgB;AAAA,cAChB;AAAA,cACA;AAAA,cACA,SAAS;AAAA,YACV;AAAA,YACA,UAAU;AAAA,UACX;AAAA,QACD;AAAA,MACD,EAAE,OAAO,OAAO;AAAA,IACjB;AAAA,EACD;AACD;;;AChIO,SAAS,gBAAgB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,GAAoB;AACnB,SAAO;AAAA,IACN,MAAM;AAAA,IACN,OAAO;AAAA,MACN,OAAO;AAAA,QACN,UAAU;AAAA,QACV,SAAS;AAAA,QACT,eAAe;AAAA,QACf,YAAY;AAAA,QACZ,gBAAgB;AAAA,QAChB;AAAA,QACA;AAAA,QACA,iBAAiB,OAAO;AAAA,MACzB;AAAA,MACA,UAAU;AAAA,QACT,eAAe;AAAA,UACd,MAAM;AAAA,UACN,OAAO;AAAA,YACN,KAAK;AAAA,YACL;AAAA,YACA;AAAA,YACA,OAAO;AAAA,cACN,UAAU;AAAA,cACV,KAAK;AAAA,cACL,MAAM;AAAA,cACN;AAAA,cACA;AAAA,YACD;AAAA,UACD;AAAA,QACD,IAAI;AAAA,QACJ;AAAA,UACC,MAAM;AAAA,UACN,OAAO;AAAA,YACN,OAAO;AAAA,cACN,UAAU;AAAA,cACV,SAAS;AAAA,cACT,eAAe;AAAA,cACf,YAAY;AAAA,cACZ,gBAAgB;AAAA,cAChB;AAAA,cACA;AAAA,cACA,SAAS;AAAA,YACV;AAAA,YACA,UAAU;AAAA,cACT;AAAA,gBACC,MAAM;AAAA,gBACN,OAAO;AAAA,kBACN,OAAO;AAAA,oBACN,UAAU;AAAA,oBACV,YAAY;AAAA,oBACZ,OAAO,OAAO;AAAA,oBACd,eAAe;AAAA,oBACf,QAAQ;AAAA,oBACR,WAAW;AAAA,kBACZ;AAAA,kBACA,UAAU;AAAA,gBACX;AAAA,cACD;AAAA,cACA,cAAc;AAAA,gBACb,MAAM;AAAA,gBACN,OAAO;AAAA,kBACN,OAAO;AAAA,oBACN,UAAU;AAAA,oBACV,YAAY;AAAA,oBACZ,OAAO,OAAO;AAAA,oBACd,WAAW;AAAA,oBACX,cAAc;AAAA,oBACd,WAAW;AAAA,oBACX,UAAU;AAAA,kBACX;AAAA,kBACA,UAAU;AAAA,gBACX;AAAA,cACD,IAAI;AAAA,cACJ;AAAA,gBACC,MAAM;AAAA,gBACN,OAAO;AAAA,kBACN,OAAO;AAAA,oBACN,UAAU;AAAA,oBACV,YAAY;AAAA,oBACZ,OAAO,OAAO;AAAA,oBACd,WAAW;AAAA,oBACX,cAAc;AAAA,oBACd,WAAW;AAAA,oBACX,SAAS;AAAA,kBACV;AAAA,kBACA,UAAU;AAAA,gBACX;AAAA,cACD;AAAA,YACD,EAAE,OAAO,OAAO;AAAA,UACjB;AAAA,QACD;AAAA,MACD,EAAE,OAAO,OAAO;AAAA,IACjB;AAAA,EACD;AACD;;;AChGO,IAAM,YAAY;AAAA,EACxB,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AACV;AAIO,SAAS,YAAY,MAA6C;AACxE,MAAI,OAAO,SAAS,YAAY;AAC/B,WAAO;AAAA,EACR;AACA,SAAO,UAAU,IAAI;AACtB;;;ACXO,IAAM,gBAA+B;AAAA,EAC3C,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,QAAQ;AACT;;;ATDO,IAAM,WAAW;AACjB,IAAM,YAAY;AAKzB,eAAsB,gBAAgB,SAA6C;AAClF,QAAM;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,OAAO;AAAA,IACP;AAAA,IACA,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,WAAW;AAAA,EACZ,IAAI;AAGJ,QAAM,SAAwB;AAAA,IAC7B,GAAG;AAAA,IACH,GAAG;AAAA,EACJ;AAGA,QAAM,QAAQ,MAAM,UAAU,UAAU;AACxC,QAAM,cAAc,kBAAkB,KAAK;AAG3C,QAAM,eAAe,aAAa,YAAY;AAC9C,QAAM,iBAAiB,aAAa,aAAa,QAAQ;AACzD,QAAM,eAAe,eAClB,qBAAqB;AAAA,IACrB,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,SAAS,aAAa,WAAW;AAAA,IACjC,WAAW,aAAa,aAAa;AAAA,EACtC,CAAC,IACA;AAGH,QAAM,qBAAqB,eACxB,2BAA2B;AAAA,IAC3B,MAAM,GAAG,cAAc;AAAA,IACvB,MAAM;AAAA,IACN,SAAS,aAAa,WAAW;AAAA,IACjC,WAAW,aAAa,aAAa;AAAA,EACtC,CAAC,IACA;AAGH,QAAM,aAAa,YAAY,QAA6C;AAG5E,QAAM,QAAyB;AAAA,IAC9B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACD;AAGA,QAAM,UAAU,WAAW,KAAK;AAGhC,QAAM,MAAM,UAAM,cAAAC,SAAO,SAAyC;AAAA,IACjE;AAAA,IACA;AAAA,IACA,OAAO;AAAA,EACR,CAAC;AAGD,MAAI,UAAU;AACb,WAAO,OAAO,KAAK,GAAG;AAAA,EACvB;AAGA,QAAM,QAAQ,IAAI,sBAAM,KAAK;AAAA,IAC5B,OAAO;AAAA,MACN,MAAM;AAAA,MACN,OAAO;AAAA,IACR;AAAA,EACD,CAAC;AACD,QAAM,UAAU,MAAM,OAAO;AAE7B,SAAO,OAAO,KAAK,QAAQ,MAAM,CAAC;AACnC;AAKA,eAAsB,uBAAuB,SAA6C;AACzF,QAAM,MAAM,MAAM,gBAAgB,OAAO;AACzC,SAAO,yBAAyB,IAAI,SAAS,QAAQ,CAAC;AACvD;AAKA,eAAsB,mBAAmB,SAA4B,cAAc,MAAyB;AAC3G,QAAM,MAAM,MAAM,gBAAgB,OAAO;AAEzC,SAAO,IAAI,SAAS,KAAK;AAAA,IACxB,SAAS;AAAA,MACR,gBAAgB;AAAA,MAChB,iBAAiB,mBAAmB,WAAW;AAAA,IAChD;AAAA,EACD,CAAC;AACF;;;AU7GO,SAAS,iBAAiB,SAA4B;AAC5D,QAAM;AAAA,IACL;AAAA,IACA,iBAAiB,WAAW;AAAA,IAC5B;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc;AAAA,IACd;AAAA,IACA;AAAA,EACD,IAAI;AAEJ,SAAO,OAAO,EAAE,IAAI,MAAoB;AACvC,UAAM,QAAQ,IAAI,aAAa,IAAI,OAAO;AAC1C,UAAM,cAAc,IAAI,aAAa,IAAI,aAAa,KAAK;AAC3D,UAAM,QAAQ,IAAI,aAAa,IAAI,OAAO,KAAK;AAC/C,UAAM,YAAY,IAAI,aAAa,IAAI,MAAM,KAAK;AAClD,UAAM,gBAAgB,IAAI,aAAa,IAAI,UAAU;AACrD,UAAM,mBAAkD,iBAAiB;AAEzE,QAAI,CAAC,OAAO;AACX,aAAO,IAAI,SAAS,2BAA2B,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC/D;AAEA,QAAI;AACH,aAAO,MAAM;AAAA,QACZ;AAAA,UACC;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,UAAU;AAAA,UACV;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACD;AAAA,QACA;AAAA,MACD;AAAA,IACD,SAAS,OAAO;AACf,YAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC1E,cAAQ,MAAM,gCAAgC,KAAK;AACnD,aAAO,IAAI,SAAS,6BAA6B,YAAY,IAAI,EAAE,QAAQ,IAAI,CAAC;AAAA,IACjF;AAAA,EACD;AACD;;;ACtEA,IAAAC,mBAAsB;AAYf,SAAS,SAAS,KAAa,UAA2B,CAAC,GAAW;AAC5E,QAAM,OAAO;AAAA,IACZ,OAAO,QAAQ,WACZ,EAAE,MAAM,SAAkB,OAAO,QAAQ,SAAS,IAClD;AAAA,IACH,YAAY,QAAQ;AAAA,EACrB;AAEA,QAAM,QAAQ,IAAI,uBAAM,KAAK,IAAI;AACjC,QAAM,WAAW,MAAM,OAAO;AAE9B,SAAO,OAAO,KAAK,SAAS,MAAM,CAAC;AACpC;AAKO,SAAS,gBAAgB,KAAa,UAA2B,CAAC,GAAW;AACnF,QAAM,MAAM,SAAS,KAAK,OAAO;AACjC,SAAO,yBAAyB,IAAI,SAAS,QAAQ,CAAC;AACvD;AAKO,SAAS,iBAAiB,KAAa,UAA2B,CAAC,GAAG,cAAc,MAAgB;AAC1G,QAAM,MAAM,SAAS,KAAK,OAAO;AAEjC,SAAO,IAAI,SAAS,KAAK;AAAA,IACxB,SAAS;AAAA,MACR,gBAAgB;AAAA,MAChB,iBAAiB,mBAAmB,WAAW;AAAA,IAChD;AAAA,EACD,CAAC;AACF;","names":["import_promises","import_node_path","import_node_url","import_meta","getModuleDir","satori","import_resvg_js"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -66,8 +66,7 @@ interface LoadedFonts {
|
|
|
66
66
|
body: ArrayBuffer;
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
* Load fonts from config, falling back to bundled fonts
|
|
70
|
-
* with CDN fallback for serverless environments.
|
|
69
|
+
* Load fonts from config, falling back to bundled fonts.
|
|
71
70
|
*/
|
|
72
71
|
declare function loadFonts(config?: OgFontConfig): Promise<LoadedFonts>;
|
|
73
72
|
type SatoriFontConfig = {
|
package/dist/index.d.ts
CHANGED
|
@@ -66,8 +66,7 @@ interface LoadedFonts {
|
|
|
66
66
|
body: ArrayBuffer;
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
* Load fonts from config, falling back to bundled fonts
|
|
70
|
-
* with CDN fallback for serverless environments.
|
|
69
|
+
* Load fonts from config, falling back to bundled fonts.
|
|
71
70
|
*/
|
|
72
71
|
declare function loadFonts(config?: OgFontConfig): Promise<LoadedFonts>;
|
|
73
72
|
type SatoriFontConfig = {
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
|
+
__dirname,
|
|
2
3
|
getTemplate
|
|
3
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-2I73D34T.js";
|
|
4
5
|
|
|
5
6
|
// src/generate.ts
|
|
6
7
|
import satori from "satori";
|
|
7
8
|
import { Resvg } from "@resvg/resvg-js";
|
|
8
9
|
|
|
9
10
|
// src/fonts.ts
|
|
10
|
-
import { readFile } from "fs/promises";
|
|
11
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
11
12
|
import { existsSync } from "fs";
|
|
13
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
14
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
15
|
+
|
|
16
|
+
// src/fonts-data.ts
|
|
17
|
+
import { readFile } from "fs/promises";
|
|
12
18
|
import { dirname, resolve } from "path";
|
|
13
19
|
import { fileURLToPath } from "url";
|
|
14
20
|
function getModuleDir() {
|
|
@@ -20,14 +26,53 @@ function getModuleDir() {
|
|
|
20
26
|
}
|
|
21
27
|
return resolve(process.cwd(), "node_modules/@ewanc26/og/dist");
|
|
22
28
|
}
|
|
29
|
+
async function loadEmbeddedFonts() {
|
|
30
|
+
const moduleDir = getModuleDir();
|
|
31
|
+
const paths = [
|
|
32
|
+
{
|
|
33
|
+
heading: resolve(moduleDir, "fonts/Inter-Bold.ttf"),
|
|
34
|
+
body: resolve(moduleDir, "fonts/Inter-Regular.ttf")
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
heading: resolve(moduleDir, "../fonts/Inter-Bold.ttf"),
|
|
38
|
+
body: resolve(moduleDir, "../fonts/Inter-Regular.ttf")
|
|
39
|
+
}
|
|
40
|
+
];
|
|
41
|
+
for (const p of paths) {
|
|
42
|
+
try {
|
|
43
|
+
const [headingBuf, bodyBuf] = await Promise.all([
|
|
44
|
+
readFile(p.heading),
|
|
45
|
+
readFile(p.body)
|
|
46
|
+
]);
|
|
47
|
+
return {
|
|
48
|
+
heading: headingBuf.buffer.slice(headingBuf.byteOffset, headingBuf.byteOffset + headingBuf.byteLength),
|
|
49
|
+
body: bodyBuf.buffer.slice(bodyBuf.byteOffset, bodyBuf.byteOffset + bodyBuf.byteLength)
|
|
50
|
+
};
|
|
51
|
+
} catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/fonts.ts
|
|
59
|
+
function getModuleDir2() {
|
|
60
|
+
if (typeof import.meta !== "undefined" && import.meta.url) {
|
|
61
|
+
return dirname2(fileURLToPath2(import.meta.url));
|
|
62
|
+
}
|
|
63
|
+
if (typeof __dirname !== "undefined") {
|
|
64
|
+
return __dirname;
|
|
65
|
+
}
|
|
66
|
+
return resolve2(process.cwd(), "node_modules/@ewanc26/og/dist");
|
|
67
|
+
}
|
|
23
68
|
function getFontsDir() {
|
|
24
69
|
const candidates = [
|
|
25
70
|
// Standard: fonts next to dist
|
|
26
|
-
|
|
71
|
+
resolve2(getModuleDir2(), "../fonts"),
|
|
27
72
|
// Vercel serverless: fonts inside dist
|
|
28
|
-
|
|
73
|
+
resolve2(getModuleDir2(), "fonts"),
|
|
29
74
|
// Fallback: node_modules path
|
|
30
|
-
|
|
75
|
+
resolve2(process.cwd(), "node_modules/@ewanc26/og/fonts")
|
|
31
76
|
];
|
|
32
77
|
for (const dir of candidates) {
|
|
33
78
|
if (existsSync(dir)) {
|
|
@@ -38,47 +83,35 @@ function getFontsDir() {
|
|
|
38
83
|
}
|
|
39
84
|
var BUNDLED_FONTS = {
|
|
40
85
|
get heading() {
|
|
41
|
-
return
|
|
86
|
+
return resolve2(getFontsDir(), "Inter-Bold.ttf");
|
|
42
87
|
},
|
|
43
88
|
get body() {
|
|
44
|
-
return
|
|
89
|
+
return resolve2(getFontsDir(), "Inter-Regular.ttf");
|
|
45
90
|
}
|
|
46
91
|
};
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
92
|
+
function toArrayBuffer(buf) {
|
|
93
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
94
|
+
}
|
|
51
95
|
async function loadFonts(config) {
|
|
52
96
|
const headingPath = config?.heading ?? BUNDLED_FONTS.heading;
|
|
53
97
|
const bodyPath = config?.body ?? BUNDLED_FONTS.body;
|
|
54
98
|
const [heading, body] = await Promise.all([
|
|
55
|
-
|
|
56
|
-
|
|
99
|
+
loadFontFile(headingPath),
|
|
100
|
+
loadFontFile(bodyPath)
|
|
57
101
|
]);
|
|
58
102
|
return { heading, body };
|
|
59
103
|
}
|
|
60
|
-
async function
|
|
104
|
+
async function loadFontFile(source) {
|
|
61
105
|
try {
|
|
62
|
-
|
|
106
|
+
const buffer = await readFile2(source);
|
|
107
|
+
return toArrayBuffer(buffer);
|
|
63
108
|
} catch (error) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return
|
|
67
|
-
} catch (fallbackError) {
|
|
68
|
-
throw new Error(`Failed to load font from both local path (${path}) and CDN (${fallbackUrl}): ${fallbackError}`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
async function loadFontFile(source) {
|
|
73
|
-
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
74
|
-
const response = await fetch(source);
|
|
75
|
-
if (!response.ok) {
|
|
76
|
-
throw new Error(`Failed to load font from URL: ${source}`);
|
|
109
|
+
const embedded = await loadEmbeddedFonts();
|
|
110
|
+
if (embedded) {
|
|
111
|
+
return source.includes("Bold") ? embedded.heading : embedded.body;
|
|
77
112
|
}
|
|
78
|
-
|
|
113
|
+
throw new Error(`Failed to load font from ${source}`);
|
|
79
114
|
}
|
|
80
|
-
const buffer = await readFile(source);
|
|
81
|
-
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
82
115
|
}
|
|
83
116
|
function createSatoriFonts(fonts) {
|
|
84
117
|
return [
|
|
@@ -324,6 +357,8 @@ function createOgEndpoint(options) {
|
|
|
324
357
|
const description = url.searchParams.get("description") ?? void 0;
|
|
325
358
|
const image = url.searchParams.get("image") ?? void 0;
|
|
326
359
|
const noiseSeed = url.searchParams.get("seed") ?? void 0;
|
|
360
|
+
const templateParam = url.searchParams.get("template");
|
|
361
|
+
const resolvedTemplate = templateParam ?? template;
|
|
327
362
|
if (!title) {
|
|
328
363
|
return new Response("Missing title parameter", { status: 400 });
|
|
329
364
|
}
|
|
@@ -334,7 +369,7 @@ function createOgEndpoint(options) {
|
|
|
334
369
|
description,
|
|
335
370
|
siteName,
|
|
336
371
|
image,
|
|
337
|
-
template,
|
|
372
|
+
template: resolvedTemplate,
|
|
338
373
|
colors,
|
|
339
374
|
fonts,
|
|
340
375
|
noise,
|
|
@@ -345,8 +380,9 @@ function createOgEndpoint(options) {
|
|
|
345
380
|
cacheMaxAge
|
|
346
381
|
);
|
|
347
382
|
} catch (error) {
|
|
383
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
348
384
|
console.error("Failed to generate OG image:", error);
|
|
349
|
-
return new Response(
|
|
385
|
+
return new Response(`Failed to generate image: ${errorMessage}`, { status: 500 });
|
|
350
386
|
}
|
|
351
387
|
};
|
|
352
388
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/generate.ts","../src/fonts.ts","../src/noise.ts","../src/png-encoder.ts","../src/types.ts","../src/endpoint.ts","../src/svg.ts"],"sourcesContent":["/**\n * Core OG image generation.\n * Uses satori for JSX-to-SVG and resvg-js for SVG-to-PNG.\n */\n\nimport satori from 'satori'\nimport { Resvg } from '@resvg/resvg-js'\nimport { loadFonts, createSatoriFonts } from './fonts.js'\nimport { generateNoiseDataUrl, generateCircleNoiseDataUrl } from './noise.js'\nimport { getTemplate } from './templates/index.js'\nimport { defaultColors } from './types.js'\nimport type {\n\tOgGenerateOptions,\n\tOgColorConfig,\n\tOgTemplateProps,\n} from './types.js'\n\n// Standard OG image dimensions\nexport const OG_WIDTH = 1200\nexport const OG_HEIGHT = 630\n\n/**\n * Generate an OG image as PNG Buffer.\n */\nexport async function generateOgImage(options: OgGenerateOptions): Promise<Buffer> {\n\tconst {\n\t\ttitle,\n\t\tdescription,\n\t\tsiteName,\n\t\timage,\n\t\ttemplate = 'blog',\n\t\tcolors: colorOverrides,\n\t\tfonts: fontConfig,\n\t\tnoise: noiseConfig,\n\t\tnoiseSeed,\n\t\twidth = OG_WIDTH,\n\t\theight = OG_HEIGHT,\n\t\tdebugSvg = false,\n\t} = options\n\n\t// Merge colours\n\tconst colors: OgColorConfig = {\n\t\t...defaultColors,\n\t\t...colorOverrides,\n\t}\n\n\t// Load fonts\n\tconst fonts = await loadFonts(fontConfig)\n\tconst satoriFonts = createSatoriFonts(fonts)\n\n\t// Generate noise background\n\tconst noiseEnabled = noiseConfig?.enabled !== false\n\tconst noiseSeedValue = noiseSeed || noiseConfig?.seed || title\n\tconst noiseDataUrl = noiseEnabled\n\t\t? generateNoiseDataUrl({\n\t\t\t\tseed: noiseSeedValue,\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t\topacity: noiseConfig?.opacity ?? 0.4,\n\t\t\t\tcolorMode: noiseConfig?.colorMode ?? 'grayscale',\n\t\t\t})\n\t\t: undefined\n\n\t// Generate circular noise decoration\n\tconst circleNoiseDataUrl = noiseEnabled\n\t\t? generateCircleNoiseDataUrl({\n\t\t\t\tseed: `${noiseSeedValue}-circle`,\n\t\t\t\tsize: 200,\n\t\t\t\topacity: noiseConfig?.opacity ?? 0.15,\n\t\t\t\tcolorMode: noiseConfig?.colorMode ?? 'grayscale',\n\t\t\t})\n\t\t: undefined\n\n\t// Get template function\n\tconst templateFn = getTemplate(template as Parameters<typeof getTemplate>[0])\n\n\t// Build template props\n\tconst props: OgTemplateProps = {\n\t\ttitle,\n\t\tdescription,\n\t\tsiteName,\n\t\timage,\n\t\tcolors,\n\t\tnoiseDataUrl,\n\t\tcircleNoiseDataUrl,\n\t\twidth,\n\t\theight,\n\t}\n\n\t// Render template to Satori-compatible structure\n\tconst element = templateFn(props)\n\n\t// Generate SVG with satori\n\tconst svg = await satori(element as Parameters<typeof satori>[0], {\n\t\twidth,\n\t\theight,\n\t\tfonts: satoriFonts,\n\t})\n\n\t// Debug: return SVG string\n\tif (debugSvg) {\n\t\treturn Buffer.from(svg)\n\t}\n\n\t// Convert SVG to PNG with resvg-js\n\tconst resvg = new Resvg(svg, {\n\t\tfitTo: {\n\t\t\tmode: 'width',\n\t\t\tvalue: width,\n\t\t},\n\t})\n\tconst pngData = resvg.render()\n\n\treturn Buffer.from(pngData.asPng())\n}\n\n/**\n * Generate OG image and return as base64 data URL.\n */\nexport async function generateOgImageDataUrl(options: OgGenerateOptions): Promise<string> {\n\tconst png = await generateOgImage(options)\n\treturn `data:image/png;base64,${png.toString('base64')}`\n}\n\n/**\n * Generate OG image and return as Response (for SvelteKit endpoints).\n */\nexport async function generateOgResponse(options: OgGenerateOptions, cacheMaxAge = 3600): Promise<Response> {\n\tconst png = await generateOgImage(options)\n\n\treturn new Response(png, {\n\t\theaders: {\n\t\t\t'Content-Type': 'image/png',\n\t\t\t'Cache-Control': `public, max-age=${cacheMaxAge}`,\n\t\t},\n\t})\n}\n","/**\n * @ewanc26/og fonts\n *\n * Font loading utilities. Bundles Inter font by default.\n */\n\nimport { readFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { OgFontConfig } from './types.js'\n\n// Declare __dirname for CJS contexts (injected by bundlers)\ndeclare const __dirname: string | undefined\n\n// ─── Paths ────────────────────────────────────────────────────────────────────\n\n/**\n * Get the directory of the current module.\n * Works in both ESM and bundled contexts.\n */\nfunction getModuleDir(): string {\n\t// ESM context\n\tif (typeof import.meta !== 'undefined' && import.meta.url) {\n\t\treturn dirname(fileURLToPath(import.meta.url))\n\t}\n\t// Bundled CJS context - __dirname is injected by bundlers\n\tif (typeof __dirname !== 'undefined') {\n\t\treturn __dirname\n\t}\n\t// Fallback\n\treturn resolve(process.cwd(), 'node_modules/@ewanc26/og/dist')\n}\n\n/**\n * Resolve the fonts directory relative to the installed package.\n * Tries multiple possible locations for serverless compatibility.\n */\nfunction getFontsDir(): string {\n\tconst candidates = [\n\t\t// Standard: fonts next to dist\n\t\tresolve(getModuleDir(), '../fonts'),\n\t\t// Vercel serverless: fonts inside dist\n\t\tresolve(getModuleDir(), 'fonts'),\n\t\t// Fallback: node_modules path\n\t\tresolve(process.cwd(), 'node_modules/@ewanc26/og/fonts'),\n\t]\n\n\tfor (const dir of candidates) {\n\t\tif (existsSync(dir)) {\n\t\t\treturn dir\n\t\t}\n\t}\n\n\t// Return first candidate as fallback (will fail gracefully)\n\treturn candidates[0]\n}\n\n/**\n * Resolve bundled font paths. Uses getters to defer resolution until runtime.\n */\nexport const BUNDLED_FONTS = {\n\tget heading() {\n\t\treturn resolve(getFontsDir(), 'Inter-Bold.ttf')\n\t},\n\tget body() {\n\t\treturn resolve(getFontsDir(), 'Inter-Regular.ttf')\n\t},\n} as const\n\n// Google Fonts CDN fallback URLs\nconst FONT_FALLBACKS = {\n\theading: 'https://github.com/rsms/inter/raw/refs/heads/main/docs/font-files/Inter-Bold.ttf',\n\tbody: 'https://github.com/rsms/inter/raw/refs/heads/main/docs/font-files/Inter-Regular.ttf',\n}\n\n// ─── Font Loading ──────────────────────────────────────────────────────────────\n\nexport interface LoadedFonts {\n\theading: ArrayBuffer\n\tbody: ArrayBuffer\n}\n\n/**\n * Load fonts from config, falling back to bundled fonts,\n * with CDN fallback for serverless environments.\n */\nexport async function loadFonts(config?: OgFontConfig): Promise<LoadedFonts> {\n\tconst headingPath = config?.heading ?? BUNDLED_FONTS.heading\n\tconst bodyPath = config?.body ?? BUNDLED_FONTS.body\n\n\tconst [heading, body] = await Promise.all([\n\t\tloadFontFileWithFallback(headingPath, FONT_FALLBACKS.heading),\n\t\tloadFontFileWithFallback(bodyPath, FONT_FALLBACKS.body),\n\t])\n\n\treturn { heading, body }\n}\n\n/**\n * Load a font file, falling back to CDN if local file fails.\n */\nasync function loadFontFileWithFallback(path: string, fallbackUrl: string): Promise<ArrayBuffer> {\n\ttry {\n\t\treturn await loadFontFile(path)\n\t} catch (error) {\n\t\tconsole.warn(`Failed to load local font at ${path}, trying CDN fallback:`, error)\n\t\ttry {\n\t\t\treturn await loadFontFile(fallbackUrl)\n\t\t} catch (fallbackError) {\n\t\t\tthrow new Error(`Failed to load font from both local path (${path}) and CDN (${fallbackUrl}): ${fallbackError}`)\n\t\t}\n\t}\n}\n\n/**\n * Load a font from file path or URL.\n */\nasync function loadFontFile(source: string): Promise<ArrayBuffer> {\n\t// Handle URLs\n\tif (source.startsWith('http://') || source.startsWith('https://')) {\n\t\tconst response = await fetch(source)\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`Failed to load font from URL: ${source}`)\n\t\t}\n\t\treturn response.arrayBuffer()\n\t}\n\n\t// Handle file paths\n\tconst buffer = await readFile(source)\n\treturn buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)\n}\n\n// ─── Font Registration for Satori ─────────────────────────────────────────────\n\nexport type SatoriFontConfig = {\n\tname: string\n\tdata: ArrayBuffer\n\tweight: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900\n\tstyle: 'normal' | 'italic'\n}\n\n/**\n * Create Satori-compatible font config from loaded fonts.\n * Uses Inter font family with weight 700 for headings and 400 for body.\n */\nexport function createSatoriFonts(fonts: LoadedFonts): SatoriFontConfig[] {\n\treturn [\n\t\t{\n\t\t\tname: 'Inter',\n\t\t\tdata: fonts.heading,\n\t\t\tweight: 700,\n\t\t\tstyle: 'normal',\n\t\t},\n\t\t{\n\t\t\tname: 'Inter',\n\t\t\tdata: fonts.body,\n\t\t\tweight: 400,\n\t\t\tstyle: 'normal',\n\t\t},\n\t]\n}\n","/**\n * @ewanc26/og noise\n */\n\nimport { generateNoisePixels } from '@ewanc26/noise'\nimport { PNGEncoder } from './png-encoder.js'\nimport type { OgNoiseConfig } from './types.js'\n\nexport interface NoiseOptions {\n\tseed: string\n\twidth: number\n\theight: number\n\topacity?: number\n\tcolorMode?: OgNoiseConfig['colorMode']\n}\n\nexport interface CircleNoiseOptions {\n\tseed: string\n\tsize: number\n\topacity?: number\n\tcolorMode?: OgNoiseConfig['colorMode']\n}\n\n/**\n * Generate a noise PNG as a data URL.\n */\nexport function generateNoiseDataUrl(options: NoiseOptions): string {\n\tconst { seed, width, height, opacity = 0.4, colorMode = 'grayscale' } = options\n\n\tconst pixels = generateNoisePixels(width, height, seed, {\n\t\tgridSize: 4,\n\t\toctaves: 3,\n\t\tcolorMode: colorMode === 'grayscale'\n\t\t\t? { type: 'grayscale', range: [20, 60] }\n\t\t\t: { type: 'hsl', hueRange: 40, saturationRange: [30, 50], lightnessRange: [30, 50] }\n\t})\n\n\tif (opacity < 1) {\n\t\tfor (let i = 3; i < pixels.length; i += 4) {\n\t\t\tpixels[i] = Math.round(pixels[i] * opacity)\n\t\t}\n\t}\n\n\tconst pngBuffer = PNGEncoder.encode(pixels, width, height)\n\treturn `data:image/png;base64,${pngBuffer.toString('base64')}`\n}\n\n/**\n * Generate a circular noise PNG as a data URL.\n * Creates a square image with circular transparency mask.\n */\nexport function generateCircleNoiseDataUrl(options: CircleNoiseOptions): string {\n\tconst { seed, size, opacity = 0.15, colorMode = 'grayscale' } = options\n\n\tconst pixels = generateNoisePixels(size, size, seed, {\n\t\tgridSize: 4,\n\t\toctaves: 3,\n\t\tcolorMode: colorMode === 'grayscale'\n\t\t\t? { type: 'grayscale', range: [30, 70] }\n\t\t\t: { type: 'hsl', hueRange: 40, saturationRange: [30, 50], lightnessRange: [30, 50] }\n\t})\n\n\tconst center = size / 2\n\tconst radius = size / 2\n\n\t// Apply circular mask\n\tfor (let y = 0; y < size; y++) {\n\t\tfor (let x = 0; x < size; x++) {\n\t\t\tconst idx = (y * size + x) * 4\n\t\t\tconst dx = x - center + 0.5\n\t\t\tconst dy = y - center + 0.5\n\t\t\tconst dist = Math.sqrt(dx * dx + dy * dy)\n\n\t\t\tif (dist > radius) {\n\t\t\t\t// Outside circle - fully transparent\n\t\t\t\tpixels[idx + 3] = 0\n\t\t\t} else if (dist > radius - 2) {\n\t\t\t\t// Anti-alias edge\n\t\t\t\tconst edgeOpacity = (radius - dist) / 2\n\t\t\t\tpixels[idx + 3] = Math.round(255 * edgeOpacity * opacity)\n\t\t\t} else {\n\t\t\t\t// Inside circle - apply opacity\n\t\t\t\tpixels[idx + 3] = Math.round(255 * opacity)\n\t\t\t}\n\t\t}\n\t}\n\n\tconst pngBuffer = PNGEncoder.encode(pixels, size, size)\n\treturn `data:image/png;base64,${pngBuffer.toString('base64')}`\n}\n","/**\n * Minimal PNG encoder for noise backgrounds.\n * Uses node:zlib for deflate compression.\n */\n\nimport { deflateSync } from 'node:zlib'\n\nconst PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])\n\nfunction crc32(data: Buffer): number {\n\tlet crc = 0xffffffff\n\tconst table: number[] = []\n\n\t// Build CRC table\n\tfor (let n = 0; n < 256; n++) {\n\t\tlet c = n\n\t\tfor (let k = 0; k < 8; k++) {\n\t\t\tc = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1\n\t\t}\n\t\ttable[n] = c\n\t}\n\n\t// Calculate CRC\n\tfor (let i = 0; i < data.length; i++) {\n\t\tcrc = table[(crc ^ data[i]) & 0xff] ^ (crc >>> 8)\n\t}\n\n\treturn (crc ^ 0xffffffff) >>> 0\n}\n\nfunction createChunk(type: string, data: Buffer): Buffer {\n\tconst length = Buffer.alloc(4)\n\tlength.writeUInt32BE(data.length, 0)\n\n\tconst typeBuffer = Buffer.from(type, 'ascii')\n\tconst crcData = Buffer.concat([typeBuffer, data])\n\tconst crc = Buffer.alloc(4)\n\tcrc.writeUInt32BE(crc32(crcData), 0)\n\n\treturn Buffer.concat([length, typeBuffer, data, crc])\n}\n\nfunction createIHDR(width: number, height: number): Buffer {\n\tconst data = Buffer.alloc(13)\n\tdata.writeUInt32BE(width, 0) // Width\n\tdata.writeUInt32BE(height, 4) // Height\n\tdata.writeUInt8(8, 8) // Bit depth: 8 bits\n\tdata.writeUInt8(2, 9) // Colour type: 2 (RGB)\n\tdata.writeUInt8(0, 10) // Compression method\n\tdata.writeUInt8(0, 11) // Filter method\n\tdata.writeUInt8(0, 12) // Interlace method\n\n\treturn createChunk('IHDR', data)\n}\n\nfunction createIDAT(pixels: Uint8ClampedArray, width: number, height: number): Buffer {\n\t// Apply filter (none filter = 0) per row\n\tconst rawData = Buffer.alloc(height * (width * 3 + 1))\n\n\tlet srcOffset = 0\n\tlet dstOffset = 0\n\n\tfor (let y = 0; y < height; y++) {\n\t\trawData[dstOffset++] = 0 // Filter type: none\n\t\tfor (let x = 0; x < width; x++) {\n\t\t\tconst r = pixels[srcOffset++]\n\t\t\tconst g = pixels[srcOffset++]\n\t\t\tconst b = pixels[srcOffset++]\n\t\t\tsrcOffset++ // Skip alpha\n\t\t\trawData[dstOffset++] = r\n\t\t\trawData[dstOffset++] = g\n\t\t\trawData[dstOffset++] = b\n\t\t}\n\t}\n\n\tconst compressed = deflateSync(rawData)\n\treturn createChunk('IDAT', compressed)\n}\n\nfunction createIEND(): Buffer {\n\treturn createChunk('IEND', Buffer.alloc(0))\n}\n\n/**\n * Encode raw RGBA pixel data as a PNG Buffer.\n */\nexport function encodePNG(pixels: Uint8ClampedArray, width: number, height: number): Buffer {\n\treturn Buffer.concat([\n\t\tPNG_SIGNATURE,\n\t\tcreateIHDR(width, height),\n\t\tcreateIDAT(pixels, width, height),\n\t\tcreateIEND(),\n\t])\n}\n\n/**\n * PNGEncoder namespace for cleaner imports.\n */\nexport const PNGEncoder = {\n\tencode: encodePNG,\n}\n","/**\n * @ewanc26/og types\n */\n\n// ─── Colour Configuration ─────────────────────────────────────────────────────\n\nexport interface OgColorConfig {\n\t/** Background color (very dark). @default '#0f1a15' */\n\tbackground: string\n\t/** Primary text color. @default '#e8f5e9' */\n\ttext: string\n\t/** Secondary/accent text (mint). @default '#86efac' */\n\taccent: string\n}\n\nexport const defaultColors: OgColorConfig = {\n\tbackground: '#0f1a15',\n\ttext: '#e8f5e9',\n\taccent: '#86efac',\n}\n\n// ─── Font Configuration ───────────────────────────────────────────────────────\n\nexport interface OgFontConfig {\n\theading?: string\n\tbody?: string\n}\n\n// ─── Noise Configuration ──────────────────────────────────────────────────────\n\nexport interface OgNoiseConfig {\n\tenabled?: boolean\n\tseed?: string\n\topacity?: number\n\tcolorMode?: 'grayscale' | 'hsl'\n}\n\n// ─── Template Props ────────────────────────────────────────────────────────────\n\nexport interface OgTemplateProps {\n\ttitle: string\n\tdescription?: string\n\tsiteName: string\n\timage?: string\n\tcolors: OgColorConfig\n\tnoiseDataUrl?: string\n\tcircleNoiseDataUrl?: string\n\twidth: number\n\theight: number\n}\n\nexport type OgTemplate = (props: OgTemplateProps) => unknown\n\n// ─── Generation Options ───────────────────────────────────────────────────────\n\nexport interface OgGenerateOptions {\n\ttitle: string\n\tdescription?: string\n\tsiteName: string\n\timage?: string\n\ttemplate?: 'blog' | 'profile' | 'default' | OgTemplate\n\tcolors?: Partial<OgColorConfig>\n\tfonts?: OgFontConfig\n\tnoise?: OgNoiseConfig\n\tnoiseSeed?: string\n\twidth?: number\n\theight?: number\n\tdebugSvg?: boolean\n}\n\n// ─── SvelteKit Endpoint Options ───────────────────────────────────────────────\n\nexport interface OgEndpointOptions {\n\tsiteName: string\n\tdefaultTemplate?: 'blog' | 'profile' | 'default' | OgTemplate\n\tcolors?: Partial<OgColorConfig>\n\tfonts?: OgFontConfig\n\tnoise?: OgNoiseConfig\n\tcacheMaxAge?: number\n\twidth?: number\n\theight?: number\n}\n\n// ─── Internal Types ────────────────────────────────────────────────────────────\n\nexport interface InternalGenerateContext {\n\twidth: number\n\theight: number\n\tfonts: { heading: ArrayBuffer; body: ArrayBuffer }\n\tcolors: OgColorConfig\n\tnoiseDataUrl?: string\n}\n","/**\n * SvelteKit endpoint helpers.\n */\n\nimport { generateOgResponse } from './generate.js'\nimport type { OgEndpointOptions, OgTemplate } from './types.js'\n\n/**\n * Create a SvelteKit GET handler for OG image generation.\n *\n * @example\n * ```ts\n * // src/routes/og/[title]/+server.ts\n * import { createOgEndpoint } from '@ewanc26/og';\n *\n * export const GET = createOgEndpoint({\n * siteName: 'ewancroft.uk',\n * defaultTemplate: 'blog',\n * });\n * ```\n *\n * The endpoint expects query parameters:\n * - `title` (required): Page title\n * - `description`: Optional description\n * - `image`: Optional avatar/logo URL\n * - `seed`: Optional noise seed\n */\nexport function createOgEndpoint(options: OgEndpointOptions) {\n\tconst {\n\t\tsiteName,\n\t\tdefaultTemplate: template = 'default',\n\t\tcolors,\n\t\tfonts,\n\t\tnoise,\n\t\tcacheMaxAge = 3600,\n\t\twidth,\n\t\theight,\n\t} = options\n\n\treturn async ({ url }: { url: URL }) => {\n\t\tconst title = url.searchParams.get('title')\n\t\tconst description = url.searchParams.get('description') ?? undefined\n\t\tconst image = url.searchParams.get('image') ?? undefined\n\t\tconst noiseSeed = url.searchParams.get('seed') ?? undefined\n\n\t\tif (!title) {\n\t\t\treturn new Response('Missing title parameter', { status: 400 })\n\t\t}\n\n\t\ttry {\n\t\t\treturn await generateOgResponse(\n\t\t\t\t{\n\t\t\t\t\ttitle,\n\t\t\t\t\tdescription,\n\t\t\t\t\tsiteName,\n\t\t\t\t\timage,\n\t\t\t\t\ttemplate: template as OgTemplate,\n\t\t\t\t\tcolors,\n\t\t\t\t\tfonts,\n\t\t\t\t\tnoise,\n\t\t\t\t\tnoiseSeed,\n\t\t\t\t\twidth,\n\t\t\t\t\theight,\n\t\t\t\t},\n\t\t\t\tcacheMaxAge\n\t\t\t)\n\t\t} catch (error) {\n\t\t\tconsole.error('Failed to generate OG image:', error)\n\t\t\treturn new Response('Failed to generate image', { status: 500 })\n\t\t}\n\t}\n}\n\n/**\n * Create a typed OG image URL for use in meta tags.\n */\nexport function createOgImageUrl(\n\tbaseUrl: string,\n\tparams: {\n\t\ttitle: string\n\t\tdescription?: string\n\t\timage?: string\n\t\tseed?: string\n\t}\n): string {\n\tconst url = new URL(baseUrl)\n\turl.searchParams.set('title', params.title)\n\tif (params.description) url.searchParams.set('description', params.description)\n\tif (params.image) url.searchParams.set('image', params.image)\n\tif (params.seed) url.searchParams.set('seed', params.seed)\n\treturn url.toString()\n}\n","/**\n * SVG to PNG conversion using @resvg/resvg-js.\n */\n\nimport { Resvg } from '@resvg/resvg-js'\n\nexport interface SvgToPngOptions {\n\t/** Scale to fit width in pixels */\n\tfitWidth?: number\n\t/** Background colour for transparent areas */\n\tbackgroundColor?: string\n}\n\n/**\n * Convert an SVG string to PNG Buffer.\n */\nexport function svgToPng(svg: string, options: SvgToPngOptions = {}): Buffer {\n\tconst opts = {\n\t\tfitTo: options.fitWidth\n\t\t\t? { mode: 'width' as const, value: options.fitWidth }\n\t\t\t: undefined,\n\t\tbackground: options.backgroundColor,\n\t}\n\n\tconst resvg = new Resvg(svg, opts)\n\tconst rendered = resvg.render()\n\n\treturn Buffer.from(rendered.asPng())\n}\n\n/**\n * Convert an SVG string to PNG data URL.\n */\nexport function svgToPngDataUrl(svg: string, options: SvgToPngOptions = {}): string {\n\tconst png = svgToPng(svg, options)\n\treturn `data:image/png;base64,${png.toString('base64')}`\n}\n\n/**\n * Convert an SVG string to PNG Response (for SvelteKit endpoints).\n */\nexport function svgToPngResponse(svg: string, options: SvgToPngOptions = {}, cacheMaxAge = 3600): Response {\n\tconst png = svgToPng(svg, options)\n\n\treturn new Response(png, {\n\t\theaders: {\n\t\t\t'Content-Type': 'image/png',\n\t\t\t'Cache-Control': `public, max-age=${cacheMaxAge}`,\n\t\t},\n\t})\n}\n"],"mappings":"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","names":["Resvg"]}
|
|
1
|
+
{"version":3,"sources":["../src/generate.ts","../src/fonts.ts","../src/fonts-data.ts","../src/noise.ts","../src/png-encoder.ts","../src/types.ts","../src/endpoint.ts","../src/svg.ts"],"sourcesContent":["/**\n * Core OG image generation.\n * Uses satori for JSX-to-SVG and resvg-js for SVG-to-PNG.\n */\n\nimport satori from 'satori'\nimport { Resvg } from '@resvg/resvg-js'\nimport { loadFonts, createSatoriFonts } from './fonts.js'\nimport { generateNoiseDataUrl, generateCircleNoiseDataUrl } from './noise.js'\nimport { getTemplate } from './templates/index.js'\nimport { defaultColors } from './types.js'\nimport type {\n\tOgGenerateOptions,\n\tOgColorConfig,\n\tOgTemplateProps,\n} from './types.js'\n\n// Standard OG image dimensions\nexport const OG_WIDTH = 1200\nexport const OG_HEIGHT = 630\n\n/**\n * Generate an OG image as PNG Buffer.\n */\nexport async function generateOgImage(options: OgGenerateOptions): Promise<Buffer> {\n\tconst {\n\t\ttitle,\n\t\tdescription,\n\t\tsiteName,\n\t\timage,\n\t\ttemplate = 'blog',\n\t\tcolors: colorOverrides,\n\t\tfonts: fontConfig,\n\t\tnoise: noiseConfig,\n\t\tnoiseSeed,\n\t\twidth = OG_WIDTH,\n\t\theight = OG_HEIGHT,\n\t\tdebugSvg = false,\n\t} = options\n\n\t// Merge colours\n\tconst colors: OgColorConfig = {\n\t\t...defaultColors,\n\t\t...colorOverrides,\n\t}\n\n\t// Load fonts\n\tconst fonts = await loadFonts(fontConfig)\n\tconst satoriFonts = createSatoriFonts(fonts)\n\n\t// Generate noise background\n\tconst noiseEnabled = noiseConfig?.enabled !== false\n\tconst noiseSeedValue = noiseSeed || noiseConfig?.seed || title\n\tconst noiseDataUrl = noiseEnabled\n\t\t? generateNoiseDataUrl({\n\t\t\t\tseed: noiseSeedValue,\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t\topacity: noiseConfig?.opacity ?? 0.4,\n\t\t\t\tcolorMode: noiseConfig?.colorMode ?? 'grayscale',\n\t\t\t})\n\t\t: undefined\n\n\t// Generate circular noise decoration\n\tconst circleNoiseDataUrl = noiseEnabled\n\t\t? generateCircleNoiseDataUrl({\n\t\t\t\tseed: `${noiseSeedValue}-circle`,\n\t\t\t\tsize: 200,\n\t\t\t\topacity: noiseConfig?.opacity ?? 0.15,\n\t\t\t\tcolorMode: noiseConfig?.colorMode ?? 'grayscale',\n\t\t\t})\n\t\t: undefined\n\n\t// Get template function\n\tconst templateFn = getTemplate(template as Parameters<typeof getTemplate>[0])\n\n\t// Build template props\n\tconst props: OgTemplateProps = {\n\t\ttitle,\n\t\tdescription,\n\t\tsiteName,\n\t\timage,\n\t\tcolors,\n\t\tnoiseDataUrl,\n\t\tcircleNoiseDataUrl,\n\t\twidth,\n\t\theight,\n\t}\n\n\t// Render template to Satori-compatible structure\n\tconst element = templateFn(props)\n\n\t// Generate SVG with satori\n\tconst svg = await satori(element as Parameters<typeof satori>[0], {\n\t\twidth,\n\t\theight,\n\t\tfonts: satoriFonts,\n\t})\n\n\t// Debug: return SVG string\n\tif (debugSvg) {\n\t\treturn Buffer.from(svg)\n\t}\n\n\t// Convert SVG to PNG with resvg-js\n\tconst resvg = new Resvg(svg, {\n\t\tfitTo: {\n\t\t\tmode: 'width',\n\t\t\tvalue: width,\n\t\t},\n\t})\n\tconst pngData = resvg.render()\n\n\treturn Buffer.from(pngData.asPng())\n}\n\n/**\n * Generate OG image and return as base64 data URL.\n */\nexport async function generateOgImageDataUrl(options: OgGenerateOptions): Promise<string> {\n\tconst png = await generateOgImage(options)\n\treturn `data:image/png;base64,${png.toString('base64')}`\n}\n\n/**\n * Generate OG image and return as Response (for SvelteKit endpoints).\n */\nexport async function generateOgResponse(options: OgGenerateOptions, cacheMaxAge = 3600): Promise<Response> {\n\tconst png = await generateOgImage(options)\n\n\treturn new Response(png, {\n\t\theaders: {\n\t\t\t'Content-Type': 'image/png',\n\t\t\t'Cache-Control': `public, max-age=${cacheMaxAge}`,\n\t\t},\n\t})\n}\n","/**\n * @ewanc26/og fonts\n *\n * Font loading utilities. Bundles Inter font by default.\n */\n\nimport { readFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { OgFontConfig } from './types.js'\nimport { loadEmbeddedFonts } from './fonts-data.js'\n\n// Declare __dirname for CJS contexts (injected by bundlers)\ndeclare const __dirname: string | undefined\n\n// ─── Paths ────────────────────────────────────────────────────────────────────\n\n/**\n * Get the directory of the current module.\n * Works in both ESM and bundled contexts.\n */\nfunction getModuleDir(): string {\n\t// ESM context\n\tif (typeof import.meta !== 'undefined' && import.meta.url) {\n\t\treturn dirname(fileURLToPath(import.meta.url))\n\t}\n\t// Bundled CJS context - __dirname is injected by bundlers\n\tif (typeof __dirname !== 'undefined') {\n\t\treturn __dirname\n\t}\n\t// Fallback\n\treturn resolve(process.cwd(), 'node_modules/@ewanc26/og/dist')\n}\n\n/**\n * Resolve the fonts directory relative to the installed package.\n * Tries multiple possible locations for serverless compatibility.\n */\nfunction getFontsDir(): string {\n\tconst candidates = [\n\t\t// Standard: fonts next to dist\n\t\tresolve(getModuleDir(), '../fonts'),\n\t\t// Vercel serverless: fonts inside dist\n\t\tresolve(getModuleDir(), 'fonts'),\n\t\t// Fallback: node_modules path\n\t\tresolve(process.cwd(), 'node_modules/@ewanc26/og/fonts'),\n\t]\n\n\tfor (const dir of candidates) {\n\t\tif (existsSync(dir)) {\n\t\t\treturn dir\n\t\t}\n\t}\n\n\t// Return first candidate as fallback (will fail gracefully)\n\treturn candidates[0]\n}\n\n/**\n * Resolve bundled font paths. Uses getters to defer resolution until runtime.\n */\nexport const BUNDLED_FONTS = {\n\tget heading() {\n\t\treturn resolve(getFontsDir(), 'Inter-Bold.ttf')\n\t},\n\tget body() {\n\t\treturn resolve(getFontsDir(), 'Inter-Regular.ttf')\n\t},\n} as const\n\n// ─── Font Loading ──────────────────────────────────────────────────────────────\n\nexport interface LoadedFonts {\n\theading: ArrayBuffer\n\tbody: ArrayBuffer\n}\n\n/**\n * Helper to convert Buffer to ArrayBuffer\n */\nfunction toArrayBuffer(buf: Buffer): ArrayBuffer {\n\treturn buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer\n}\n\n/**\n * Load fonts from config, falling back to bundled fonts.\n */\nexport async function loadFonts(config?: OgFontConfig): Promise<LoadedFonts> {\n\tconst headingPath = config?.heading ?? BUNDLED_FONTS.heading\n\tconst bodyPath = config?.body ?? BUNDLED_FONTS.body\n\n\tconst [heading, body] = await Promise.all([\n\t\tloadFontFile(headingPath),\n\t\tloadFontFile(bodyPath),\n\t])\n\n\treturn { heading, body }\n}\n\n/**\n * Load a font from file path.\n * Falls back to alternative locations if local file not found.\n */\nasync function loadFontFile(source: string): Promise<ArrayBuffer> {\n\ttry {\n\t\tconst buffer = await readFile(source)\n\t\treturn toArrayBuffer(buffer)\n\t} catch (error) {\n\t\t// Try embedded fonts (loaded from alternative locations)\n\t\tconst embedded = await loadEmbeddedFonts()\n\t\tif (embedded) {\n\t\t\treturn source.includes('Bold') ? embedded.heading : embedded.body\n\t\t}\n\t\tthrow new Error(`Failed to load font from ${source}`)\n\t}\n}\n\n// ─── Font Registration for Satori ─────────────────────────────────────────────\n\nexport type SatoriFontConfig = {\n\tname: string\n\tdata: ArrayBuffer\n\tweight: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900\n\tstyle: 'normal' | 'italic'\n}\n\n/**\n * Create Satori-compatible font config from loaded fonts.\n * Uses Inter font family with weight 700 for headings and 400 for body.\n */\nexport function createSatoriFonts(fonts: LoadedFonts): SatoriFontConfig[] {\n\treturn [\n\t\t{\n\t\t\tname: 'Inter',\n\t\t\tdata: fonts.heading,\n\t\t\tweight: 700,\n\t\t\tstyle: 'normal',\n\t\t},\n\t\t{\n\t\t\tname: 'Inter',\n\t\t\tdata: fonts.body,\n\t\t\tweight: 400,\n\t\t\tstyle: 'normal',\n\t\t},\n\t]\n}\n","/**\n * Font loading helper for serverless environments\n */\n\nimport { readFile } from 'node:fs/promises'\nimport { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n// Declare __dirname for CJS contexts (injected by bundlers)\ndeclare const __dirname: string | undefined\n\nfunction getModuleDir(): string {\n\tif (typeof import.meta !== 'undefined' && import.meta.url) {\n\t\treturn dirname(fileURLToPath(import.meta.url))\n\t}\n\tif (typeof __dirname !== 'undefined') {\n\t\treturn __dirname\n\t}\n\treturn resolve(process.cwd(), 'node_modules/@ewanc26/og/dist')\n}\n\nexport interface FontData {\n\theading: ArrayBuffer\n\tbody: ArrayBuffer\n}\n\n/**\n * Try loading fonts from dist/fonts directory\n */\nexport async function loadEmbeddedFonts(): Promise<FontData | null> {\n\tconst moduleDir = getModuleDir()\n\n\tconst paths = [\n\t\t{\n\t\t\theading: resolve(moduleDir, 'fonts/Inter-Bold.ttf'),\n\t\t\tbody: resolve(moduleDir, 'fonts/Inter-Regular.ttf'),\n\t\t},\n\t\t{\n\t\t\theading: resolve(moduleDir, '../fonts/Inter-Bold.ttf'),\n\t\t\tbody: resolve(moduleDir, '../fonts/Inter-Regular.ttf'),\n\t\t},\n\t]\n\n\tfor (const p of paths) {\n\t\ttry {\n\t\t\tconst [headingBuf, bodyBuf] = await Promise.all([\n\t\t\t\treadFile(p.heading),\n\t\t\t\treadFile(p.body),\n\t\t\t])\n\t\t\t// Convert Buffer to ArrayBuffer\n\t\t\treturn {\n\t\t\t\theading: headingBuf.buffer.slice(headingBuf.byteOffset, headingBuf.byteOffset + headingBuf.byteLength),\n\t\t\t\tbody: bodyBuf.buffer.slice(bodyBuf.byteOffset, bodyBuf.byteOffset + bodyBuf.byteLength),\n\t\t\t}\n\t\t} catch {\n\t\t\tcontinue\n\t\t}\n\t}\n\n\treturn null\n}\n","/**\n * @ewanc26/og noise\n */\n\nimport { generateNoisePixels } from '@ewanc26/noise'\nimport { PNGEncoder } from './png-encoder.js'\nimport type { OgNoiseConfig } from './types.js'\n\nexport interface NoiseOptions {\n\tseed: string\n\twidth: number\n\theight: number\n\topacity?: number\n\tcolorMode?: OgNoiseConfig['colorMode']\n}\n\nexport interface CircleNoiseOptions {\n\tseed: string\n\tsize: number\n\topacity?: number\n\tcolorMode?: OgNoiseConfig['colorMode']\n}\n\n/**\n * Generate a noise PNG as a data URL.\n */\nexport function generateNoiseDataUrl(options: NoiseOptions): string {\n\tconst { seed, width, height, opacity = 0.4, colorMode = 'grayscale' } = options\n\n\tconst pixels = generateNoisePixels(width, height, seed, {\n\t\tgridSize: 4,\n\t\toctaves: 3,\n\t\tcolorMode: colorMode === 'grayscale'\n\t\t\t? { type: 'grayscale', range: [20, 60] }\n\t\t\t: { type: 'hsl', hueRange: 40, saturationRange: [30, 50], lightnessRange: [30, 50] }\n\t})\n\n\tif (opacity < 1) {\n\t\tfor (let i = 3; i < pixels.length; i += 4) {\n\t\t\tpixels[i] = Math.round(pixels[i] * opacity)\n\t\t}\n\t}\n\n\tconst pngBuffer = PNGEncoder.encode(pixels, width, height)\n\treturn `data:image/png;base64,${pngBuffer.toString('base64')}`\n}\n\n/**\n * Generate a circular noise PNG as a data URL.\n * Creates a square image with circular transparency mask.\n */\nexport function generateCircleNoiseDataUrl(options: CircleNoiseOptions): string {\n\tconst { seed, size, opacity = 0.15, colorMode = 'grayscale' } = options\n\n\tconst pixels = generateNoisePixels(size, size, seed, {\n\t\tgridSize: 4,\n\t\toctaves: 3,\n\t\tcolorMode: colorMode === 'grayscale'\n\t\t\t? { type: 'grayscale', range: [30, 70] }\n\t\t\t: { type: 'hsl', hueRange: 40, saturationRange: [30, 50], lightnessRange: [30, 50] }\n\t})\n\n\tconst center = size / 2\n\tconst radius = size / 2\n\n\t// Apply circular mask\n\tfor (let y = 0; y < size; y++) {\n\t\tfor (let x = 0; x < size; x++) {\n\t\t\tconst idx = (y * size + x) * 4\n\t\t\tconst dx = x - center + 0.5\n\t\t\tconst dy = y - center + 0.5\n\t\t\tconst dist = Math.sqrt(dx * dx + dy * dy)\n\n\t\t\tif (dist > radius) {\n\t\t\t\t// Outside circle - fully transparent\n\t\t\t\tpixels[idx + 3] = 0\n\t\t\t} else if (dist > radius - 2) {\n\t\t\t\t// Anti-alias edge\n\t\t\t\tconst edgeOpacity = (radius - dist) / 2\n\t\t\t\tpixels[idx + 3] = Math.round(255 * edgeOpacity * opacity)\n\t\t\t} else {\n\t\t\t\t// Inside circle - apply opacity\n\t\t\t\tpixels[idx + 3] = Math.round(255 * opacity)\n\t\t\t}\n\t\t}\n\t}\n\n\tconst pngBuffer = PNGEncoder.encode(pixels, size, size)\n\treturn `data:image/png;base64,${pngBuffer.toString('base64')}`\n}\n","/**\n * Minimal PNG encoder for noise backgrounds.\n * Uses node:zlib for deflate compression.\n */\n\nimport { deflateSync } from 'node:zlib'\n\nconst PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])\n\nfunction crc32(data: Buffer): number {\n\tlet crc = 0xffffffff\n\tconst table: number[] = []\n\n\t// Build CRC table\n\tfor (let n = 0; n < 256; n++) {\n\t\tlet c = n\n\t\tfor (let k = 0; k < 8; k++) {\n\t\t\tc = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1\n\t\t}\n\t\ttable[n] = c\n\t}\n\n\t// Calculate CRC\n\tfor (let i = 0; i < data.length; i++) {\n\t\tcrc = table[(crc ^ data[i]) & 0xff] ^ (crc >>> 8)\n\t}\n\n\treturn (crc ^ 0xffffffff) >>> 0\n}\n\nfunction createChunk(type: string, data: Buffer): Buffer {\n\tconst length = Buffer.alloc(4)\n\tlength.writeUInt32BE(data.length, 0)\n\n\tconst typeBuffer = Buffer.from(type, 'ascii')\n\tconst crcData = Buffer.concat([typeBuffer, data])\n\tconst crc = Buffer.alloc(4)\n\tcrc.writeUInt32BE(crc32(crcData), 0)\n\n\treturn Buffer.concat([length, typeBuffer, data, crc])\n}\n\nfunction createIHDR(width: number, height: number): Buffer {\n\tconst data = Buffer.alloc(13)\n\tdata.writeUInt32BE(width, 0) // Width\n\tdata.writeUInt32BE(height, 4) // Height\n\tdata.writeUInt8(8, 8) // Bit depth: 8 bits\n\tdata.writeUInt8(2, 9) // Colour type: 2 (RGB)\n\tdata.writeUInt8(0, 10) // Compression method\n\tdata.writeUInt8(0, 11) // Filter method\n\tdata.writeUInt8(0, 12) // Interlace method\n\n\treturn createChunk('IHDR', data)\n}\n\nfunction createIDAT(pixels: Uint8ClampedArray, width: number, height: number): Buffer {\n\t// Apply filter (none filter = 0) per row\n\tconst rawData = Buffer.alloc(height * (width * 3 + 1))\n\n\tlet srcOffset = 0\n\tlet dstOffset = 0\n\n\tfor (let y = 0; y < height; y++) {\n\t\trawData[dstOffset++] = 0 // Filter type: none\n\t\tfor (let x = 0; x < width; x++) {\n\t\t\tconst r = pixels[srcOffset++]\n\t\t\tconst g = pixels[srcOffset++]\n\t\t\tconst b = pixels[srcOffset++]\n\t\t\tsrcOffset++ // Skip alpha\n\t\t\trawData[dstOffset++] = r\n\t\t\trawData[dstOffset++] = g\n\t\t\trawData[dstOffset++] = b\n\t\t}\n\t}\n\n\tconst compressed = deflateSync(rawData)\n\treturn createChunk('IDAT', compressed)\n}\n\nfunction createIEND(): Buffer {\n\treturn createChunk('IEND', Buffer.alloc(0))\n}\n\n/**\n * Encode raw RGBA pixel data as a PNG Buffer.\n */\nexport function encodePNG(pixels: Uint8ClampedArray, width: number, height: number): Buffer {\n\treturn Buffer.concat([\n\t\tPNG_SIGNATURE,\n\t\tcreateIHDR(width, height),\n\t\tcreateIDAT(pixels, width, height),\n\t\tcreateIEND(),\n\t])\n}\n\n/**\n * PNGEncoder namespace for cleaner imports.\n */\nexport const PNGEncoder = {\n\tencode: encodePNG,\n}\n","/**\n * @ewanc26/og types\n */\n\n// ─── Colour Configuration ─────────────────────────────────────────────────────\n\nexport interface OgColorConfig {\n\t/** Background color (very dark). @default '#0f1a15' */\n\tbackground: string\n\t/** Primary text color. @default '#e8f5e9' */\n\ttext: string\n\t/** Secondary/accent text (mint). @default '#86efac' */\n\taccent: string\n}\n\nexport const defaultColors: OgColorConfig = {\n\tbackground: '#0f1a15',\n\ttext: '#e8f5e9',\n\taccent: '#86efac',\n}\n\n// ─── Font Configuration ───────────────────────────────────────────────────────\n\nexport interface OgFontConfig {\n\theading?: string\n\tbody?: string\n}\n\n// ─── Noise Configuration ──────────────────────────────────────────────────────\n\nexport interface OgNoiseConfig {\n\tenabled?: boolean\n\tseed?: string\n\topacity?: number\n\tcolorMode?: 'grayscale' | 'hsl'\n}\n\n// ─── Template Props ────────────────────────────────────────────────────────────\n\nexport interface OgTemplateProps {\n\ttitle: string\n\tdescription?: string\n\tsiteName: string\n\timage?: string\n\tcolors: OgColorConfig\n\tnoiseDataUrl?: string\n\tcircleNoiseDataUrl?: string\n\twidth: number\n\theight: number\n}\n\nexport type OgTemplate = (props: OgTemplateProps) => unknown\n\n// ─── Generation Options ───────────────────────────────────────────────────────\n\nexport interface OgGenerateOptions {\n\ttitle: string\n\tdescription?: string\n\tsiteName: string\n\timage?: string\n\ttemplate?: 'blog' | 'profile' | 'default' | OgTemplate\n\tcolors?: Partial<OgColorConfig>\n\tfonts?: OgFontConfig\n\tnoise?: OgNoiseConfig\n\tnoiseSeed?: string\n\twidth?: number\n\theight?: number\n\tdebugSvg?: boolean\n}\n\n// ─── SvelteKit Endpoint Options ───────────────────────────────────────────────\n\nexport interface OgEndpointOptions {\n\tsiteName: string\n\tdefaultTemplate?: 'blog' | 'profile' | 'default' | OgTemplate\n\tcolors?: Partial<OgColorConfig>\n\tfonts?: OgFontConfig\n\tnoise?: OgNoiseConfig\n\tcacheMaxAge?: number\n\twidth?: number\n\theight?: number\n}\n\n// ─── Internal Types ────────────────────────────────────────────────────────────\n\nexport interface InternalGenerateContext {\n\twidth: number\n\theight: number\n\tfonts: { heading: ArrayBuffer; body: ArrayBuffer }\n\tcolors: OgColorConfig\n\tnoiseDataUrl?: string\n}\n","/**\n * SvelteKit endpoint helpers.\n */\n\nimport { generateOgResponse } from './generate.js'\nimport type { OgEndpointOptions, OgGenerateOptions } from './types.js'\n\n/**\n * Create a SvelteKit GET handler for OG image generation.\n *\n * @example\n * ```ts\n * // src/routes/og/[title]/+server.ts\n * import { createOgEndpoint } from '@ewanc26/og';\n *\n * export const GET = createOgEndpoint({\n * siteName: 'ewancroft.uk',\n * defaultTemplate: 'blog',\n * });\n * ```\n *\n * The endpoint expects query parameters:\n * - `title` (required): Page title\n * - `description`: Optional description\n * - `image`: Optional avatar/logo URL\n * - `seed`: Optional noise seed\n */\nexport function createOgEndpoint(options: OgEndpointOptions) {\n\tconst {\n\t\tsiteName,\n\t\tdefaultTemplate: template = 'default',\n\t\tcolors,\n\t\tfonts,\n\t\tnoise,\n\t\tcacheMaxAge = 3600,\n\t\twidth,\n\t\theight,\n\t} = options\n\n\treturn async ({ url }: { url: URL }) => {\n\t\tconst title = url.searchParams.get('title')\n\t\tconst description = url.searchParams.get('description') ?? undefined\n\t\tconst image = url.searchParams.get('image') ?? undefined\n\t\tconst noiseSeed = url.searchParams.get('seed') ?? undefined\n\t\tconst templateParam = url.searchParams.get('template') as 'blog' | 'profile' | 'default' | null\n\t\tconst resolvedTemplate: OgGenerateOptions['template'] = templateParam ?? template\n\n\t\tif (!title) {\n\t\t\treturn new Response('Missing title parameter', { status: 400 })\n\t\t}\n\n\t\ttry {\n\t\t\treturn await generateOgResponse(\n\t\t\t\t{\n\t\t\t\t\ttitle,\n\t\t\t\t\tdescription,\n\t\t\t\t\tsiteName,\n\t\t\t\t\timage,\n\t\t\t\t\ttemplate: resolvedTemplate,\n\t\t\t\t\tcolors,\n\t\t\t\t\tfonts,\n\t\t\t\t\tnoise,\n\t\t\t\t\tnoiseSeed,\n\t\t\t\t\twidth,\n\t\t\t\t\theight,\n\t\t\t\t},\n\t\t\t\tcacheMaxAge\n\t\t\t)\n\t\t} catch (error) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : String(error)\n\t\t\tconsole.error('Failed to generate OG image:', error)\n\t\t\treturn new Response(`Failed to generate image: ${errorMessage}`, { status: 500 })\n\t\t}\n\t}\n}\n\n/**\n * Create a typed OG image URL for use in meta tags.\n */\nexport function createOgImageUrl(\n\tbaseUrl: string,\n\tparams: {\n\t\ttitle: string\n\t\tdescription?: string\n\t\timage?: string\n\t\tseed?: string\n\t}\n): string {\n\tconst url = new URL(baseUrl)\n\turl.searchParams.set('title', params.title)\n\tif (params.description) url.searchParams.set('description', params.description)\n\tif (params.image) url.searchParams.set('image', params.image)\n\tif (params.seed) url.searchParams.set('seed', params.seed)\n\treturn url.toString()\n}\n","/**\n * SVG to PNG conversion using @resvg/resvg-js.\n */\n\nimport { Resvg } from '@resvg/resvg-js'\n\nexport interface SvgToPngOptions {\n\t/** Scale to fit width in pixels */\n\tfitWidth?: number\n\t/** Background colour for transparent areas */\n\tbackgroundColor?: string\n}\n\n/**\n * Convert an SVG string to PNG Buffer.\n */\nexport function svgToPng(svg: string, options: SvgToPngOptions = {}): Buffer {\n\tconst opts = {\n\t\tfitTo: options.fitWidth\n\t\t\t? { mode: 'width' as const, value: options.fitWidth }\n\t\t\t: undefined,\n\t\tbackground: options.backgroundColor,\n\t}\n\n\tconst resvg = new Resvg(svg, opts)\n\tconst rendered = resvg.render()\n\n\treturn Buffer.from(rendered.asPng())\n}\n\n/**\n * Convert an SVG string to PNG data URL.\n */\nexport function svgToPngDataUrl(svg: string, options: SvgToPngOptions = {}): string {\n\tconst png = svgToPng(svg, options)\n\treturn `data:image/png;base64,${png.toString('base64')}`\n}\n\n/**\n * Convert an SVG string to PNG Response (for SvelteKit endpoints).\n */\nexport function svgToPngResponse(svg: string, options: SvgToPngOptions = {}, cacheMaxAge = 3600): Response {\n\tconst png = svgToPng(svg, options)\n\n\treturn new Response(png, {\n\t\theaders: {\n\t\t\t'Content-Type': 'image/png',\n\t\t\t'Cache-Control': `public, max-age=${cacheMaxAge}`,\n\t\t},\n\t})\n}\n"],"mappings":"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","names":["readFile","dirname","resolve","fileURLToPath","getModuleDir","dirname","fileURLToPath","resolve","readFile","Resvg"]}
|