@ewanc26/og 0.1.2 → 0.1.4

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/index.cjs CHANGED
@@ -54,6 +54,7 @@ var import_resvg_js = require("@resvg/resvg-js");
54
54
 
55
55
  // src/fonts.ts
56
56
  var import_promises = require("fs/promises");
57
+ var import_node_fs = require("fs");
57
58
  var import_node_path = require("path");
58
59
  var import_node_url = require("url");
59
60
  var import_meta = {};
@@ -67,7 +68,20 @@ function getModuleDir() {
67
68
  return (0, import_node_path.resolve)(process.cwd(), "node_modules/@ewanc26/og/dist");
68
69
  }
69
70
  function getFontsDir() {
70
- return (0, import_node_path.resolve)(getModuleDir(), "../fonts");
71
+ const candidates = [
72
+ // Standard: fonts next to dist
73
+ (0, import_node_path.resolve)(getModuleDir(), "../fonts"),
74
+ // Vercel serverless: fonts inside dist
75
+ (0, import_node_path.resolve)(getModuleDir(), "fonts"),
76
+ // Fallback: node_modules path
77
+ (0, import_node_path.resolve)(process.cwd(), "node_modules/@ewanc26/og/fonts")
78
+ ];
79
+ for (const dir of candidates) {
80
+ if ((0, import_node_fs.existsSync)(dir)) {
81
+ return dir;
82
+ }
83
+ }
84
+ return candidates[0];
71
85
  }
72
86
  var BUNDLED_FONTS = {
73
87
  get heading() {
@@ -87,15 +101,22 @@ async function loadFonts(config) {
87
101
  return { heading, body };
88
102
  }
89
103
  async function loadFontFile(source) {
90
- if (source.startsWith("http://") || source.startsWith("https://")) {
91
- const response = await fetch(source);
92
- if (!response.ok) {
93
- throw new Error(`Failed to load font from URL: ${source}`);
104
+ try {
105
+ const buffer = await (0, import_promises.readFile)(source);
106
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
107
+ } catch (error) {
108
+ const filename = source.split("/").pop();
109
+ const cdnUrl = `https://raw.githubusercontent.com/rsms/inter/master/docs/font-files/${filename}`;
110
+ try {
111
+ const response = await fetch(cdnUrl);
112
+ if (!response.ok) {
113
+ throw new Error(`CDN fetch failed: ${response.status}`);
114
+ }
115
+ return response.arrayBuffer();
116
+ } catch (cdnError) {
117
+ throw new Error(`Failed to load font ${filename} from both local path and CDN: ${cdnError}`);
94
118
  }
95
- return response.arrayBuffer();
96
119
  }
97
- const buffer = await (0, import_promises.readFile)(source);
98
- return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
99
120
  }
100
121
  function createSatoriFonts(fonts) {
101
122
  return [
@@ -612,8 +633,9 @@ function createOgEndpoint(options) {
612
633
  cacheMaxAge
613
634
  );
614
635
  } catch (error) {
636
+ const errorMessage = error instanceof Error ? error.message : String(error);
615
637
  console.error("Failed to generate OG image:", error);
616
- return new Response("Failed to generate image", { status: 500 });
638
+ return new Response(`Failed to generate image: ${errorMessage}`, { status: 500 });
617
639
  }
618
640
  };
619
641
  }
@@ -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 { 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 */\nfunction getFontsDir(): string {\n\treturn resolve(getModuleDir(), '../fonts')\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 * Load fonts from config, falling back to bundled DM Sans.\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 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKA,oBAAmB;AACnB,sBAAsB;;;ACAtB,sBAAyB;AACzB,uBAAiC;AACjC,sBAA8B;AAR9B;AAoBA,SAAS,eAAuB;AAE/B,MAAI,OAAO,gBAAgB,eAAe,YAAY,KAAK;AAC1D,eAAO,8BAAQ,+BAAc,YAAY,GAAG,CAAC;AAAA,EAC9C;AAEA,MAAI,OAAO,cAAc,aAAa;AACrC,WAAO;AAAA,EACR;AAEA,aAAO,0BAAQ,QAAQ,IAAI,GAAG,+BAA+B;AAC9D;AAKA,SAAS,cAAsB;AAC9B,aAAO,0BAAQ,aAAa,GAAG,UAAU;AAC1C;AAKO,IAAM,gBAAgB;AAAA,EAC5B,IAAI,UAAU;AACb,eAAO,0BAAQ,YAAY,GAAG,gBAAgB;AAAA,EAC/C;AAAA,EACA,IAAI,OAAO;AACV,eAAO,0BAAQ,YAAY,GAAG,mBAAmB;AAAA,EAClD;AACD;AAYA,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;AAKA,eAAe,aAAa,QAAsC;AAEjE,MAAI,OAAO,WAAW,SAAS,KAAK,OAAO,WAAW,UAAU,GAAG;AAClE,UAAM,WAAW,MAAM,MAAM,MAAM;AACnC,QAAI,CAAC,SAAS,IAAI;AACjB,YAAM,IAAI,MAAM,iCAAiC,MAAM,EAAE;AAAA,IAC1D;AACA,WAAO,SAAS,YAAY;AAAA,EAC7B;AAGA,QAAM,SAAS,UAAM,0BAAS,MAAM;AACpC,SAAO,OAAO,OAAO,MAAM,OAAO,YAAY,OAAO,aAAa,OAAO,UAAU;AACpF;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;;;ACpHA,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;AACD,GAAoB;AACnB,SAAO;AAAA,IACN,MAAM;AAAA,IACN,OAAO;AAAA,MACN,OAAO;AAAA,QACN,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;AAAA,UACC,MAAM;AAAA,UACN,OAAO;AAAA,YACN,OAAO;AAAA,cACN,UAAU;AAAA,cACV,YAAY;AAAA,cACZ,OAAO,OAAO;AAAA,cACd,eAAe;AAAA,cACf,QAAQ;AAAA,cACR,WAAW;AAAA,cACX,YAAY;AAAA,cACZ,UAAU;AAAA,YACX;AAAA,YACA,UAAU;AAAA,UACX;AAAA,QACD;AAAA,QACA,cAAc;AAAA,UACb,MAAM;AAAA,UACN,OAAO;AAAA,YACN,OAAO;AAAA,cACN,UAAU;AAAA,cACV,YAAY;AAAA,cACZ,OAAO,OAAO;AAAA,cACd,WAAW;AAAA,cACX,cAAc;AAAA,cACd,WAAW;AAAA,cACX,YAAY;AAAA,cACZ,UAAU;AAAA,YACX;AAAA,YACA,UAAU;AAAA,UACX;AAAA,QACD,IAAI;AAAA,QACJ;AAAA,UACC,MAAM;AAAA,UACN,OAAO;AAAA,YACN,OAAO;AAAA,cACN,UAAU;AAAA,cACV,YAAY;AAAA,cACZ,OAAO,OAAO;AAAA,cACd,WAAW;AAAA,cACX,cAAc;AAAA,cACd,WAAW;AAAA,cACX,SAAS;AAAA,YACV;AAAA,YACA,UAAU;AAAA,UACX;AAAA,QACD;AAAA,MACD,EAAE,OAAO,OAAO;AAAA,IACjB;AAAA,EACD;AACD;;;ACvEO,SAAS,gBAAgB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,GAAoB;AACnB,QAAM,WAAsB,CAAC;AAE7B,MAAI,OAAO;AACV,aAAS,KAAK;AAAA,MACb,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,WAAS,KAAK;AAAA,IACb,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,aAAS,KAAK;AAAA,MACb,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,WAAS,KAAK;AAAA,IACb,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,SAAS;AAAA,QACT,eAAe;AAAA,QACf,YAAY;AAAA,QACZ,gBAAgB;AAAA,QAChB;AAAA,QACA;AAAA,QACA,iBAAiB,OAAO;AAAA,MACzB;AAAA,MACA;AAAA,IACD;AAAA,EACD;AACD;;;AC9FO,SAAS,gBAAgB;AAAA,EAC/B;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,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;AAAA,UACC,MAAM;AAAA,UACN,OAAO;AAAA,YACN,OAAO;AAAA,cACN,UAAU;AAAA,cACV,YAAY;AAAA,cACZ,OAAO,OAAO;AAAA,cACd,eAAe;AAAA,cACf,QAAQ;AAAA,cACR,WAAW;AAAA,YACZ;AAAA,YACA,UAAU;AAAA,UACX;AAAA,QACD;AAAA,QACA,cAAc;AAAA,UACb,MAAM;AAAA,UACN,OAAO;AAAA,YACN,OAAO;AAAA,cACN,UAAU;AAAA,cACV,YAAY;AAAA,cACZ,OAAO,OAAO;AAAA,cACd,WAAW;AAAA,cACX,cAAc;AAAA,cACd,WAAW;AAAA,cACX,UAAU;AAAA,YACX;AAAA,YACA,UAAU;AAAA,UACX;AAAA,QACD,IAAI;AAAA,QACJ;AAAA,UACC,MAAM;AAAA,UACN,OAAO;AAAA,YACN,OAAO;AAAA,cACN,UAAU;AAAA,cACV,YAAY;AAAA,cACZ,OAAO,OAAO;AAAA,cACd,WAAW;AAAA,cACX,cAAc;AAAA,cACd,WAAW;AAAA,cACX,SAAS;AAAA,YACV;AAAA,YACA,UAAU;AAAA,UACX;AAAA,QACD;AAAA,MACD,EAAE,OAAO,OAAO;AAAA,IACjB;AAAA,EACD;AACD;;;AC9DO,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;;;ARDO,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,cAAAA,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;;;AS7GO,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;AAElD,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;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACD;AAAA,QACA;AAAA,MACD;AAAA,IACD,SAAS,OAAO;AACf,cAAQ,MAAM,gCAAgC,KAAK;AACnD,aAAO,IAAI,SAAS,4BAA4B,EAAE,QAAQ,IAAI,CAAC;AAAA,IAChE;AAAA,EACD;AACD;;;ACnEA,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":["satori","import_resvg_js"]}
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// ─── 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 * In serverless environments, falls back to fetching from upstream CDN.\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 fetching from github raw 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 buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)\n\t} catch (error) {\n\t\t// In serverless, fonts might not be at expected path - fetch from CDN\n\t\tconst filename = source.split('/').pop()\n\t\tconst cdnUrl = `https://raw.githubusercontent.com/rsms/inter/master/docs/font-files/${filename}`\n\n\t\ttry {\n\t\t\tconst response = await fetch(cdnUrl)\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(`CDN fetch failed: ${response.status}`)\n\t\t\t}\n\t\t\treturn response.arrayBuffer()\n\t\t} catch (cdnError) {\n\t\t\tthrow new Error(`Failed to load font ${filename} from both local path and CDN: ${cdnError}`)\n\t\t}\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 * @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\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,oBAAmB;AACnB,sBAAsB;;;ACAtB,sBAAyB;AACzB,qBAA2B;AAC3B,uBAAiC;AACjC,sBAA8B;AAT9B;AAqBA,SAAS,eAAuB;AAE/B,MAAI,OAAO,gBAAgB,eAAe,YAAY,KAAK;AAC1D,eAAO,8BAAQ,+BAAc,YAAY,GAAG,CAAC;AAAA,EAC9C;AAEA,MAAI,OAAO,cAAc,aAAa;AACrC,WAAO;AAAA,EACR;AAEA,aAAO,0BAAQ,QAAQ,IAAI,GAAG,+BAA+B;AAC9D;AAMA,SAAS,cAAsB;AAC9B,QAAM,aAAa;AAAA;AAAA,QAElB,0BAAQ,aAAa,GAAG,UAAU;AAAA;AAAA,QAElC,0BAAQ,aAAa,GAAG,OAAO;AAAA;AAAA,QAE/B,0BAAQ,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,0BAAQ,YAAY,GAAG,gBAAgB;AAAA,EAC/C;AAAA,EACA,IAAI,OAAO;AACV,eAAO,0BAAQ,YAAY,GAAG,mBAAmB;AAAA,EAClD;AACD;AAaA,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,0BAAS,MAAM;AACpC,WAAO,OAAO,OAAO,MAAM,OAAO,YAAY,OAAO,aAAa,OAAO,UAAU;AAAA,EACpF,SAAS,OAAO;AAEf,UAAM,WAAW,OAAO,MAAM,GAAG,EAAE,IAAI;AACvC,UAAM,SAAS,uEAAuE,QAAQ;AAE9F,QAAI;AACH,YAAM,WAAW,MAAM,MAAM,MAAM;AACnC,UAAI,CAAC,SAAS,IAAI;AACjB,cAAM,IAAI,MAAM,qBAAqB,SAAS,MAAM,EAAE;AAAA,MACvD;AACA,aAAO,SAAS,YAAY;AAAA,IAC7B,SAAS,UAAU;AAClB,YAAM,IAAI,MAAM,uBAAuB,QAAQ,kCAAkC,QAAQ,EAAE;AAAA,IAC5F;AAAA,EACD;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;;;AC9IA,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;AACD,GAAoB;AACnB,SAAO;AAAA,IACN,MAAM;AAAA,IACN,OAAO;AAAA,MACN,OAAO;AAAA,QACN,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;AAAA,UACC,MAAM;AAAA,UACN,OAAO;AAAA,YACN,OAAO;AAAA,cACN,UAAU;AAAA,cACV,YAAY;AAAA,cACZ,OAAO,OAAO;AAAA,cACd,eAAe;AAAA,cACf,QAAQ;AAAA,cACR,WAAW;AAAA,cACX,YAAY;AAAA,cACZ,UAAU;AAAA,YACX;AAAA,YACA,UAAU;AAAA,UACX;AAAA,QACD;AAAA,QACA,cAAc;AAAA,UACb,MAAM;AAAA,UACN,OAAO;AAAA,YACN,OAAO;AAAA,cACN,UAAU;AAAA,cACV,YAAY;AAAA,cACZ,OAAO,OAAO;AAAA,cACd,WAAW;AAAA,cACX,cAAc;AAAA,cACd,WAAW;AAAA,cACX,YAAY;AAAA,cACZ,UAAU;AAAA,YACX;AAAA,YACA,UAAU;AAAA,UACX;AAAA,QACD,IAAI;AAAA,QACJ;AAAA,UACC,MAAM;AAAA,UACN,OAAO;AAAA,YACN,OAAO;AAAA,cACN,UAAU;AAAA,cACV,YAAY;AAAA,cACZ,OAAO,OAAO;AAAA,cACd,WAAW;AAAA,cACX,cAAc;AAAA,cACd,WAAW;AAAA,cACX,SAAS;AAAA,YACV;AAAA,YACA,UAAU;AAAA,UACX;AAAA,QACD;AAAA,MACD,EAAE,OAAO,OAAO;AAAA,IACjB;AAAA,EACD;AACD;;;ACvEO,SAAS,gBAAgB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,GAAoB;AACnB,QAAM,WAAsB,CAAC;AAE7B,MAAI,OAAO;AACV,aAAS,KAAK;AAAA,MACb,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,WAAS,KAAK;AAAA,IACb,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,aAAS,KAAK;AAAA,MACb,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,WAAS,KAAK;AAAA,IACb,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,SAAS;AAAA,QACT,eAAe;AAAA,QACf,YAAY;AAAA,QACZ,gBAAgB;AAAA,QAChB;AAAA,QACA;AAAA,QACA,iBAAiB,OAAO;AAAA,MACzB;AAAA,MACA;AAAA,IACD;AAAA,EACD;AACD;;;AC9FO,SAAS,gBAAgB;AAAA,EAC/B;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,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;AAAA,UACC,MAAM;AAAA,UACN,OAAO;AAAA,YACN,OAAO;AAAA,cACN,UAAU;AAAA,cACV,YAAY;AAAA,cACZ,OAAO,OAAO;AAAA,cACd,eAAe;AAAA,cACf,QAAQ;AAAA,cACR,WAAW;AAAA,YACZ;AAAA,YACA,UAAU;AAAA,UACX;AAAA,QACD;AAAA,QACA,cAAc;AAAA,UACb,MAAM;AAAA,UACN,OAAO;AAAA,YACN,OAAO;AAAA,cACN,UAAU;AAAA,cACV,YAAY;AAAA,cACZ,OAAO,OAAO;AAAA,cACd,WAAW;AAAA,cACX,cAAc;AAAA,cACd,WAAW;AAAA,cACX,UAAU;AAAA,YACX;AAAA,YACA,UAAU;AAAA,UACX;AAAA,QACD,IAAI;AAAA,QACJ;AAAA,UACC,MAAM;AAAA,UACN,OAAO;AAAA,YACN,OAAO;AAAA,cACN,UAAU;AAAA,cACV,YAAY;AAAA,cACZ,OAAO,OAAO;AAAA,cACd,WAAW;AAAA,cACX,cAAc;AAAA,cACd,WAAW;AAAA,cACX,SAAS;AAAA,YACV;AAAA,YACA,UAAU;AAAA,UACX;AAAA,QACD;AAAA,MACD,EAAE,OAAO,OAAO;AAAA,IACjB;AAAA,EACD;AACD;;;AC9DO,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;;;ARDO,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,cAAAA,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;;;AS7GO,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;AAElD,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;AAAA,UACA;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;;;ACpEA,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":["satori","import_resvg_js"]}
package/dist/index.d.cts CHANGED
@@ -66,7 +66,8 @@ interface LoadedFonts {
66
66
  body: ArrayBuffer;
67
67
  }
68
68
  /**
69
- * Load fonts from config, falling back to bundled DM Sans.
69
+ * Load fonts from config, falling back to bundled fonts.
70
+ * In serverless environments, falls back to fetching from upstream CDN.
70
71
  */
71
72
  declare function loadFonts(config?: OgFontConfig): Promise<LoadedFonts>;
72
73
  type SatoriFontConfig = {
package/dist/index.d.ts CHANGED
@@ -66,7 +66,8 @@ interface LoadedFonts {
66
66
  body: ArrayBuffer;
67
67
  }
68
68
  /**
69
- * Load fonts from config, falling back to bundled DM Sans.
69
+ * Load fonts from config, falling back to bundled fonts.
70
+ * In serverless environments, falls back to fetching from upstream CDN.
70
71
  */
71
72
  declare function loadFonts(config?: OgFontConfig): Promise<LoadedFonts>;
72
73
  type SatoriFontConfig = {
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import { Resvg } from "@resvg/resvg-js";
8
8
 
9
9
  // src/fonts.ts
10
10
  import { readFile } from "fs/promises";
11
+ import { existsSync } from "fs";
11
12
  import { dirname, resolve } from "path";
12
13
  import { fileURLToPath } from "url";
13
14
  function getModuleDir() {
@@ -20,7 +21,20 @@ function getModuleDir() {
20
21
  return resolve(process.cwd(), "node_modules/@ewanc26/og/dist");
21
22
  }
22
23
  function getFontsDir() {
23
- return resolve(getModuleDir(), "../fonts");
24
+ const candidates = [
25
+ // Standard: fonts next to dist
26
+ resolve(getModuleDir(), "../fonts"),
27
+ // Vercel serverless: fonts inside dist
28
+ resolve(getModuleDir(), "fonts"),
29
+ // Fallback: node_modules path
30
+ resolve(process.cwd(), "node_modules/@ewanc26/og/fonts")
31
+ ];
32
+ for (const dir of candidates) {
33
+ if (existsSync(dir)) {
34
+ return dir;
35
+ }
36
+ }
37
+ return candidates[0];
24
38
  }
25
39
  var BUNDLED_FONTS = {
26
40
  get heading() {
@@ -40,15 +54,22 @@ async function loadFonts(config) {
40
54
  return { heading, body };
41
55
  }
42
56
  async function loadFontFile(source) {
43
- if (source.startsWith("http://") || source.startsWith("https://")) {
44
- const response = await fetch(source);
45
- if (!response.ok) {
46
- throw new Error(`Failed to load font from URL: ${source}`);
57
+ try {
58
+ const buffer = await readFile(source);
59
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
60
+ } catch (error) {
61
+ const filename = source.split("/").pop();
62
+ const cdnUrl = `https://raw.githubusercontent.com/rsms/inter/master/docs/font-files/${filename}`;
63
+ try {
64
+ const response = await fetch(cdnUrl);
65
+ if (!response.ok) {
66
+ throw new Error(`CDN fetch failed: ${response.status}`);
67
+ }
68
+ return response.arrayBuffer();
69
+ } catch (cdnError) {
70
+ throw new Error(`Failed to load font ${filename} from both local path and CDN: ${cdnError}`);
47
71
  }
48
- return response.arrayBuffer();
49
72
  }
50
- const buffer = await readFile(source);
51
- return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
52
73
  }
53
74
  function createSatoriFonts(fonts) {
54
75
  return [
@@ -315,8 +336,9 @@ function createOgEndpoint(options) {
315
336
  cacheMaxAge
316
337
  );
317
338
  } catch (error) {
339
+ const errorMessage = error instanceof Error ? error.message : String(error);
318
340
  console.error("Failed to generate OG image:", error);
319
- return new Response("Failed to generate image", { status: 500 });
341
+ return new Response(`Failed to generate image: ${errorMessage}`, { status: 500 });
320
342
  }
321
343
  };
322
344
  }
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 { 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 */\nfunction getFontsDir(): string {\n\treturn resolve(getModuleDir(), '../fonts')\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 * Load fonts from config, falling back to bundled DM Sans.\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 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/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// ─── 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 * In serverless environments, falls back to fetching from upstream CDN.\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 fetching from github raw 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 buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)\n\t} catch (error) {\n\t\t// In serverless, fonts might not be at expected path - fetch from CDN\n\t\tconst filename = source.split('/').pop()\n\t\tconst cdnUrl = `https://raw.githubusercontent.com/rsms/inter/master/docs/font-files/${filename}`\n\n\t\ttry {\n\t\t\tconst response = await fetch(cdnUrl)\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(`CDN fetch failed: ${response.status}`)\n\t\t\t}\n\t\t\treturn response.arrayBuffer()\n\t\t} catch (cdnError) {\n\t\t\tthrow new Error(`Failed to load font ${filename} from both local path and CDN: ${cdnError}`)\n\t\t}\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 * @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\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":["Resvg"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ewanc26/og",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Dynamic OpenGraph image generator with noise backgrounds, bold typography, and Satori-based rendering. Works in SvelteKit endpoints, edge runtimes, and build scripts.",
5
5
  "author": "Ewan Croft",
6
6
  "license": "AGPL-3.0-only",
package/src/endpoint.ts CHANGED
@@ -65,8 +65,9 @@ export function createOgEndpoint(options: OgEndpointOptions) {
65
65
  cacheMaxAge
66
66
  )
67
67
  } catch (error) {
68
+ const errorMessage = error instanceof Error ? error.message : String(error)
68
69
  console.error('Failed to generate OG image:', error)
69
- return new Response('Failed to generate image', { status: 500 })
70
+ return new Response(`Failed to generate image: ${errorMessage}`, { status: 500 })
70
71
  }
71
72
  }
72
73
  }
package/src/fonts.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { readFile } from 'node:fs/promises'
8
+ import { existsSync } from 'node:fs'
8
9
  import { dirname, resolve } from 'node:path'
9
10
  import { fileURLToPath } from 'node:url'
10
11
  import type { OgFontConfig } from './types.js'
@@ -33,9 +34,26 @@ function getModuleDir(): string {
33
34
 
34
35
  /**
35
36
  * Resolve the fonts directory relative to the installed package.
37
+ * Tries multiple possible locations for serverless compatibility.
36
38
  */
37
39
  function getFontsDir(): string {
38
- return resolve(getModuleDir(), '../fonts')
40
+ const candidates = [
41
+ // Standard: fonts next to dist
42
+ resolve(getModuleDir(), '../fonts'),
43
+ // Vercel serverless: fonts inside dist
44
+ resolve(getModuleDir(), 'fonts'),
45
+ // Fallback: node_modules path
46
+ resolve(process.cwd(), 'node_modules/@ewanc26/og/fonts'),
47
+ ]
48
+
49
+ for (const dir of candidates) {
50
+ if (existsSync(dir)) {
51
+ return dir
52
+ }
53
+ }
54
+
55
+ // Return first candidate as fallback (will fail gracefully)
56
+ return candidates[0]
39
57
  }
40
58
 
41
59
  /**
@@ -58,7 +76,8 @@ export interface LoadedFonts {
58
76
  }
59
77
 
60
78
  /**
61
- * Load fonts from config, falling back to bundled DM Sans.
79
+ * Load fonts from config, falling back to bundled fonts.
80
+ * In serverless environments, falls back to fetching from upstream CDN.
62
81
  */
63
82
  export async function loadFonts(config?: OgFontConfig): Promise<LoadedFonts> {
64
83
  const headingPath = config?.heading ?? BUNDLED_FONTS.heading
@@ -73,21 +92,28 @@ export async function loadFonts(config?: OgFontConfig): Promise<LoadedFonts> {
73
92
  }
74
93
 
75
94
  /**
76
- * Load a font from file path or URL.
95
+ * Load a font from file path.
96
+ * Falls back to fetching from github raw if local file not found.
77
97
  */
78
98
  async function loadFontFile(source: string): Promise<ArrayBuffer> {
79
- // Handle URLs
80
- if (source.startsWith('http://') || source.startsWith('https://')) {
81
- const response = await fetch(source)
82
- if (!response.ok) {
83
- throw new Error(`Failed to load font from URL: ${source}`)
99
+ try {
100
+ const buffer = await readFile(source)
101
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
102
+ } catch (error) {
103
+ // In serverless, fonts might not be at expected path - fetch from CDN
104
+ const filename = source.split('/').pop()
105
+ const cdnUrl = `https://raw.githubusercontent.com/rsms/inter/master/docs/font-files/${filename}`
106
+
107
+ try {
108
+ const response = await fetch(cdnUrl)
109
+ if (!response.ok) {
110
+ throw new Error(`CDN fetch failed: ${response.status}`)
111
+ }
112
+ return response.arrayBuffer()
113
+ } catch (cdnError) {
114
+ throw new Error(`Failed to load font ${filename} from both local path and CDN: ${cdnError}`)
84
115
  }
85
- return response.arrayBuffer()
86
116
  }
87
-
88
- // Handle file paths
89
- const buffer = await readFile(source)
90
- return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
91
117
  }
92
118
 
93
119
  // ─── Font Registration for Satori ─────────────────────────────────────────────