import { readFileSync, mkdirSync, writeFileSync } from "fs"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import satori from "satori"; import { Resvg } from "@resvg/resvg-js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const outDir = join(__dirname, "../public/images/og"); const fontPath = join(__dirname, "fonts/SpecialElite-Regular.ttf"); mkdirSync(outDir, { recursive: true }); const fontData = readFileSync(fontPath); const W = 1200; const H = 630; const NAVY = "#0a0f1e"; const GOLD = "#c9a84c"; const CREAM = "#f6f0e1"; const MUTED = "#7a8aaa"; const sections = [ { slug: "home", title: "Dave Gilligan", strap: "Private AI. Jazz rooms. Civic weather. Pataphysical field notes." }, { slug: "business", title: "Business", strap: "Operator studies for adults tired of consultant vapor." }, { slug: "education", title: "Education", strap: "Degrees as cities, schools as chapters, study as migration." }, { slug: "writing", title: "Writing", strap: "Boris Vian, jazz syntax, and the science of glorious exception." }, { slug: "jazz-music", title: "Jazz and Music", strap: "Listening notes, Kongsberg nights, Caveau memories." }, { slug: "ai-lab", title: "AI Lab", strap: "Forward-looking, grounded, open-source friendly." }, { slug: "norway", title: "Norway", strap: "Field reports from Norwegian civic and family life." }, { slug: "cv", title: "CV", strap: "Professional, legible, confident." }, { slug: "family", title: "Family", strap: "The soft archive, still edited like it matters." }, { slug: "fun-postings",title: "Fun Postings", strap: "Useful nonsense, neatly set." }, { slug: "languages", title: "Languages", strap: "Polyglot, sly, welcoming." }, { slug: "projects", title: "Projects", strap: "Builder energy, clean receipts." }, { slug: "family-lab", title: "Family Lab", strap: "Private archive, atlas, memory room." }, ]; function buildNode({ title, strap }) { return { type: "div", props: { style: { display: "flex", flexDirection: "column", width: W, height: H, background: NAVY, position: "relative", }, children: [ // top gold rule { type: "div", props: { style: { position: "absolute", top: 0, left: 0, width: W, height: 8, background: GOLD, }, }, }, // domain label { type: "div", props: { style: { position: "absolute", top: 28, left: 64, fontFamily: "SpecialElite", fontSize: 16, color: GOLD, letterSpacing: "0.18em", textTransform: "uppercase", }, children: "DAVEGILLIGAN.COM", }, }, // left accent line { type: "div", props: { style: { position: "absolute", top: 64, left: 64, width: 3, height: H - 128, background: GOLD, opacity: 0.35, }, }, }, // section title { type: "div", props: { style: { position: "absolute", top: 200, left: 96, right: 64, fontFamily: "SpecialElite", fontSize: title.length > 12 ? 64 : 80, color: CREAM, lineHeight: 1.1, }, children: title, }, }, // strap line { type: "div", props: { style: { position: "absolute", top: title.length > 12 ? 330 : 360, left: 96, right: 64, fontFamily: "SpecialElite", fontSize: 22, color: MUTED, lineHeight: 1.5, }, children: strap, }, }, // bottom label { type: "div", props: { style: { position: "absolute", bottom: 36, left: 96, fontFamily: "SpecialElite", fontSize: 14, color: GOLD, letterSpacing: "0.12em", textTransform: "uppercase", }, children: "Blue Note Logic · Kongsberg", }, }, ], }, }; } for (const section of sections) { const svg = await satori(buildNode(section), { width: W, height: H, fonts: [ { name: "SpecialElite", data: fontData, weight: 400, style: "normal", }, ], }); const resvg = new Resvg(svg, { fitTo: { mode: "width", value: W }, }); const png = resvg.render().asPng(); const outPath = join(outDir, `${section.slug}.png`); writeFileSync(outPath, png); console.log(`✓ ${section.slug}.png (${(png.length / 1024).toFixed(0)} KB)`); } console.log(`\nAll ${sections.length} OG images written to public/images/og/`);