Files
andwati.com/scripts/generate-og-images.mjs
T
andwati cae62d4a66 Apply Biome formatting across the workspace
Fixes the formatting deltas from the initial Astro/Strapi scaffold
and brings the legacy generate-og-images/generate-llms/validate-post
scripts in line with Biome's style. No behavioral changes.
2026-08-30 17:59:30 +03:00

285 lines
7.1 KiB
JavaScript

import fs from "node:fs/promises";
import path from "node:path";
import { Resvg } from "@resvg/resvg-js";
import satori from "satori";
import { decompress } from "wawoff2";
const root = process.cwd();
const postsDir = path.join(root, "content", "posts");
const outDir = path.join(root, "static", "images", "og");
const COLORS = {
bg: "#131311",
text: "#e9e7de",
muted: "#8b8a80",
accent: "#ff5a46",
border: "rgba(233, 231, 222, 0.16)",
};
function tomlString(source, key) {
const match = source.match(
new RegExp(`^${key}\\s*=\\s*("(?:[^"\\\\]|\\\\.)*")`, "m"),
);
return match ? JSON.parse(match[1]) : undefined;
}
function tomlStringArray(source, key) {
const match = source.match(
new RegExp(`^${key}\\s*=\\s*\\[([^\\]]*)\\]`, "m"),
);
if (!match) return [];
return [...match[1].matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((m) =>
JSON.parse(`"${m[1]}"`),
);
}
function frontMatter(source) {
return source.match(/^\+\+\+\s*\n([\s\S]*?)\n\+\+\+/)?.[1];
}
function taxonomies(source) {
return (
source.match(/^\[taxonomies\]\s*\n([\s\S]*?)(?:\n\[|\n?$)/m)?.[1] ?? ""
);
}
function markdownFiles(directory) {
return fs.readdir(directory, { withFileTypes: true }).then((entries) =>
Promise.all(
entries.map((entry) => {
const location = path.join(directory, entry.name);
if (entry.isDirectory()) return markdownFiles(location);
return entry.isFile() && entry.name.endsWith(".md") ? [location] : [];
}),
).then((lists) => lists.flat()),
);
}
function slugFor(file, metadata) {
const relative = path.relative(postsDir, file).replaceAll(path.sep, "/");
const defaultSlug = relative.endsWith("/index.md")
? relative.slice(0, -"/index.md".length)
: relative.slice(0, -".md".length);
return tomlString(metadata, "slug") ?? defaultSlug;
}
async function loadFont(relPath) {
const woff2 = await fs.readFile(path.join(root, "static", relPath));
const ttf = await decompress(woff2);
return Buffer.from(ttf);
}
function h(type, props, ...children) {
return {
type,
props: {
...props,
children: children.flat().filter((c) => c !== null && c !== undefined),
},
};
}
function formatDate(date) {
return new Date(`${date}T00:00:00Z`).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
timeZone: "UTC",
});
}
function card({ title, date, tags }) {
const stampTag = tags[0];
const footerTags =
tags.length > 4 ? `${tags.slice(0, 4).join(" · ")} ⋯` : tags.join(" · ");
const titleSize = title.length > 70 ? 46 : title.length > 45 ? 54 : 64;
return h(
"div",
{
style: {
width: "1200px",
height: "630px",
display: "flex",
backgroundColor: COLORS.bg,
color: COLORS.text,
fontFamily: "IBM Plex Serif",
},
},
h(
"div",
{
style: {
flex: 1,
margin: "40px",
border: `1px solid ${COLORS.border}`,
borderRadius: "6px",
padding: "48px 56px",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
},
},
h(
"div",
{
style: {
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-start",
},
},
h(
"div",
{
style: {
display: "flex",
fontFamily: "JetBrains Mono",
fontWeight: 400,
fontSize: "22px",
color: COLORS.muted,
letterSpacing: "2px",
},
},
"ANDWATI.COM · CASE FILE",
),
stampTag
? h(
"div",
{
style: {
display: "flex",
border: `2px solid ${COLORS.accent}`,
borderRadius: "4px",
padding: "8px 16px",
color: COLORS.accent,
fontFamily: "JetBrains Mono",
fontWeight: 700,
fontSize: "20px",
letterSpacing: "2px",
textTransform: "uppercase",
transform: "rotate(3deg)",
},
},
stampTag,
)
: null,
),
h(
"div",
{
style: {
display: "flex",
fontFamily: "IBM Plex Serif",
fontWeight: 700,
fontSize: `${titleSize}px`,
lineHeight: 1.15,
letterSpacing: "-1px",
color: COLORS.text,
overflow: "hidden",
},
},
title,
),
h(
"div",
{
style: {
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
borderTop: `1px solid ${COLORS.border}`,
paddingTop: "24px",
},
},
h(
"div",
{
style: {
display: "flex",
fontFamily: "JetBrains Mono",
fontWeight: 400,
fontSize: "20px",
color: COLORS.muted,
letterSpacing: "1px",
},
},
formatDate(date),
),
h(
"div",
{
style: {
display: "flex",
fontFamily: "JetBrains Mono",
fontWeight: 400,
fontSize: "20px",
color: COLORS.muted,
letterSpacing: "1px",
},
},
footerTags,
),
),
),
);
}
const fonts = [
{
name: "IBM Plex Serif",
data: await loadFont(
"fonts/ibm-plex-serif/ibm-plex-serif-v20-latin-700.woff2",
),
weight: 700,
style: "normal",
},
{
name: "JetBrains Mono",
data: await loadFont("fonts/jetbrains-mono/JetBrainsMono-Regular.woff2"),
weight: 400,
style: "normal",
},
{
name: "JetBrains Mono",
data: await loadFont("fonts/jetbrains-mono/JetBrainsMono-Bold.woff2"),
weight: 700,
style: "normal",
},
];
const files = (await markdownFiles(postsDir)).filter(
(file) => path.basename(file) !== "_index.md",
);
await fs.mkdir(outDir, { recursive: true });
let count = 0;
for (const file of files) {
const source = await fs.readFile(file, "utf8");
const metadata = frontMatter(source);
if (!metadata || /^draft\s*=\s*true$/m.test(metadata)) continue;
const title = tomlString(metadata, "title");
const date = metadata.match(/^date\s*=\s*(\d{4}-\d{2}-\d{2})/m)?.[1];
if (!title || !date) continue;
const tags = tomlStringArray(taxonomies(metadata), "tags");
const slug = slugFor(file, metadata);
const svg = await satori(card({ title, date, tags }), {
width: 1200,
height: 630,
fonts,
});
const png = new Resvg(svg).render().asPng();
await fs.writeFile(path.join(outDir, `${slug}.png`), png);
count += 1;
}
console.log(
`Generated ${count} OG image${count === 1 ? "" : "s"} in ${path.relative(root, outDir)}/`,
);