Files
davegilligan-new/scripts/generate-og-images.mjs
T
daveadmin 6b509fe052 Add complete Open Graph metadata layer across all 20 pages
- Add og:title, og:description, og:image, og:url, og:type, og:site_name to BaseLayout
- Add og:locale (en_GB) + og:locale:alternate (fr_FR, nb_NO) multilingual signals
- Add article:published_time, article:section, article:author on all 5 article pages
- Add Twitter/X summary_large_image card and canonical link on every page
- Generate 13 Jazz Noir branded 1200x630 PNG OG images (satori + resvg-js)
- Add scripts/generate-og-images.mjs + Special Elite font for future regeneration
- Add public/images/articles/ and public/assets/ which were previously untracked

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 01:38:51 +02:00

177 lines
5.2 KiB
JavaScript

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/`);