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>
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,176 @@
|
||||
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/`);
|
||||
Reference in New Issue
Block a user