website/scripts/generate-og-images.js

180 lines
4.8 KiB
JavaScript

import { createCanvas, loadImage, GlobalFonts } from "@napi-rs/canvas";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Registra il font JetBrains Mono
const fontPath = path.join(
__dirname,
"../static/fonts/JetBrainsMono-Medium.ttf",
);
GlobalFonts.registerFromPath(fontPath, "JetBrains Mono");
// Configurazione (come in Hugo)
const config = {
x: 141,
y: 300,
fontSize: 55,
color: "#ffffff",
lineSpacing: 1.2,
maxWidth: 1080, // larghezza massima per il testo
basePngPath: path.join(__dirname, "../og_base.png"),
};
/**
* Genera un'immagine OpenGraph con il titolo sovrapposto
* @param {string} title - Il titolo da scrivere
* @param {string} outputPath - Percorso dove salvare l'immagine
*/
export async function generateOGImage(title, outputPath) {
try {
// Carica l'immagine base
const baseImage = await loadImage(config.basePngPath);
// Crea canvas con le stesse dimensioni dell'immagine base
const canvas = createCanvas(baseImage.width, baseImage.height);
const ctx = canvas.getContext("2d");
// Disegna l'immagine base
ctx.drawImage(baseImage, 0, 0);
// Configura il testo
ctx.font = `${config.fontSize}px "JetBrains Mono"`;
ctx.fillStyle = config.color;
ctx.textBaseline = "top";
// Gestisci il text wrapping
const lines = wrapText(ctx, title, config.maxWidth);
// Disegna ogni linea
lines.forEach((line, index) => {
const y = config.y + index * config.fontSize * config.lineSpacing;
ctx.fillText(line, config.x, y);
});
// Salva l'immagine
const dir = path.dirname(outputPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const buffer = canvas.toBuffer("image/png");
fs.writeFileSync(outputPath, buffer);
console.log(`✓ Generated OG image: ${outputPath}`);
return outputPath;
} catch (error) {
console.error(`✗ Error generating OG image for "${title}":`, error);
throw error;
}
}
/**
* Divide il testo in più righe se necessario
* @param {CanvasRenderingContext2D} ctx
* @param {string} text
* @param {number} maxWidth
* @returns {string[]}
*/
function wrapText(ctx, text, maxWidth) {
const words = text.split(" ");
const lines = [];
let currentLine = words[0];
for (let i = 1; i < words.length; i++) {
const word = words[i];
const width = ctx.measureText(currentLine + " " + word).width;
if (width < maxWidth) {
currentLine += " " + word;
} else {
lines.push(currentLine);
currentLine = word;
}
}
lines.push(currentLine);
return lines;
}
/**
* Genera immagini OG per tutti i contenuti markdown
*/
export async function generateAllOGImages() {
const contentDir = path.join(__dirname, "../content");
const publicDir = path.join(__dirname, "../public/og");
// Leggi tutti i file markdown
const files = getAllMarkdownFiles(contentDir);
console.log(`\nGenerating OpenGraph images for ${files.length} files...\n`);
for (const file of files) {
const content = fs.readFileSync(file, "utf-8");
const title = extractTitle(content);
if (title) {
const relativePath = path.relative(contentDir, file);
const slug = relativePath.replace(/\.md$/, "").replace(/\\/g, "/");
const outputPath = path.join(publicDir, `${slug}.png`);
await generateOGImage(title, outputPath);
}
}
console.log(`\nAll OpenGraph images generated!\n`);
}
/**
* Trova tutti i file markdown ricorsivamente
* @param {string} dir
* @returns {string[]}
*/
function getAllMarkdownFiles(dir, fileList = []) {
const files = fs.readdirSync(dir);
files.forEach((file) => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
getAllMarkdownFiles(filePath, fileList);
} else if (file.endsWith(".md") && !file.startsWith("_")) {
fileList.push(filePath);
}
});
return fileList;
}
/**
* Estrae il titolo dal frontmatter o dal primo heading
* @param {string} content
* @returns {string|null}
*/
function extractTitle(content) {
// Cerca nel frontmatter
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (frontmatterMatch) {
const titleMatch = frontmatterMatch[1].match(/title:\s*['"](.*?)['"]/);
if (titleMatch) return titleMatch[1];
}
// Cerca nel primo heading
const headingMatch = content.match(/^#\s+(.+)$/m);
if (headingMatch) return headingMatch[1];
return null;
}
// Esegui lo script se chiamato direttamente
if (import.meta.url.startsWith("file:")) {
const modulePath = fileURLToPath(import.meta.url);
const scriptPath = process.argv[1];
if (modulePath === scriptPath) {
generateAllOGImages().catch(console.error);
}
}