#!/usr/bin/env node // Generates a branded 1200x630 raster Open Graph image (public/og.png). // // Why: social scrapers (Slack, Twitter/X, LinkedIn, Discord, and most LLM // crawlers that render link previews) cannot rasterize SVG og:image tags — // they need a PNG/JPEG. This composes the existing brand hexagon icon + // wordmark vector art from public/gctrl/*.svg into one on-brand raster via // @resvg/resvg-js (no native build step, prebuilt binaries). // // Run manually with `npm run gen:og`, or automatically via `prebuild`. import { readFileSync, writeFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import path from 'node:path' import { Resvg } from '@resvg/resvg-js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const root = path.resolve(__dirname, '..') const W = 1200 const H = 630 // The hexagon icon's inner markup (viewBox -55 -55 110 110) — reused as-is, // just re-positioned/scaled inside the composite canvas below. const iconSvg = readFileSync(path.join(root, 'public/gctrl/icon-color.svg'), 'utf8') const iconInner = iconSvg.match(/[\s\S]*?<\/g>\s*<\/g>/)?.[0] ?? iconSvg.replace(/<\?xml[^>]*\?>/, '').replace(/<\/?svg[^>]*>/g, '') const composite = ` ${iconInner} GCTRL The Enterprise Memory Layer for AI Self-hosted knowledge graphs. Governed memory. No token tax. GCTRL.TECH · ON-PREM · GDPR-READY · OPEN SOURCE `.trim() const resvg = new Resvg(composite, { font: { loadSystemFonts: true, defaultFontFamily: 'Arial' }, background: '#04040f', fitTo: { mode: 'width', value: W }, }) const png = resvg.render().asPng() const outPath = path.join(root, 'public/og.png') writeFileSync(outPath, png) console.log(`[gen-og] wrote ${outPath} (${png.length} bytes)`)