#!/usr/bin/env node
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
import { execSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';
import sharp from 'sharp';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, '..', '..');
const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
const OUT_DIR = 'C:/Users/hoelee/Desktop/blog-og-image-redesign-brief/samples';
mkdirSync(OUT_DIR, { recursive: true });
const AVATAR_FILE = join(ROOT, 'public', 'images', 'lee-teong-hoe-640.jpg');
const AVATAR_URL = 'file:///' + AVATAR_FILE.replace(/\\/g, '/');
const TPL_RAW = readFileSync(join(__dirname, 'template-v2.html'), 'utf8');
const SAMPLES = [
{
slug: 'running-production-infrastructure-solo',
bg: 'C:/AI/ideogram4/out/sample_prod_169_00001_.png',
category: 'case-studies',
tags: ['DOCKER', 'INFRASTRUCTURE', 'PROMETHEUS'],
evidence: '162 containers · 3 hosts · 78 compose stacks',
title_en: 'Running Production Infrastructure Solo: 162 Containers Across Three Hosts',
title_zh: '独自运维 3 台主机上的 162 个生产容器:
两起故障改变了我验证数字的方式'
},
{
slug: 'docker-reported-exit-0-the-oom-killer-disagreed',
bg: 'C:/AI/ideogram4/out/sample_oom_169_00001_.png',
category: 'devops',
tags: ['DOCKER', 'DMESG', 'OOM-KILLER'],
evidence: 'exit 0 · dmesg: 6x cgroup OOM kills',
title_en: 'Docker Reported Exit 0. The Kernel Had Already OOM-Killed It.',
title_zh: 'Docker 报告 Exit 0,内核却早已通过 OOM 杀死了它'
}
];
function calcTitleSize(title, isZh) {
const len = title.length;
if (isZh) {
if (len <= 18) return 50;
if (len <= 26) return 44;
return 38;
}
if (len <= 45) return 48;
if (len <= 65) return 42;
return 36;
}
async function renderCard(sample, isZh) {
const lang = isZh ? 'zh' : 'en';
const title = isZh ? sample.title_zh : sample.title_en;
const titleSize = calcTitleSize(title, isZh);
const tagsHtml = sample.tags.map(t => `${t}`).join('');
const bgUrl = 'file:///' + sample.bg.replace(/\\/g, '/');
let html = TPL_RAW
.replace('{{LANG}}', isZh ? 'zh-CN' : 'en')
.replace('{{BG_IMAGE_SRC}}', bgUrl)
.replace('{{CATEGORY}}', sample.category)
.replace('{{TAGS_HTML}}', tagsHtml)
.replace('{{TITLE_SIZE}}', titleSize)
.replace('{{TITLE_HTML}}', title)
.replace('{{EVIDENCE_SUMMARY}}', sample.evidence)
.replace('{{AVATAR_PATH}}', AVATAR_URL);
const tmpHtml = join(OUT_DIR, `temp_${sample.slug}_${lang}.html`);
writeFileSync(tmpHtml, html, 'utf8');
const outPng = join(OUT_DIR, `${sample.slug}_og_${lang}.png`);
const chromeCmd = `"${CHROME}" --headless --screenshot="${outPng}" --window-size=1200,630 --hide-scrollbars --disable-gpu "file:///${tmpHtml.replace(/\\/g, '/')}"`;
execSync(chromeCmd, { stdio: 'pipe' });
console.log(`Rendered OG card (${lang}): ${outPng}`);
}
async function processSample(sample) {
console.log(`\n=== Processing Sample: ${sample.slug} ===`);
// 1. 生成 1600x900 内页 Hero Banner (无文字纯视觉)
const bannerWebp = join(OUT_DIR, `${sample.slug}_banner_1600x900.webp`);
await sharp(sample.bg)
.resize(1600, 900, { fit: 'cover', position: 'center' })
.webp({ quality: 88 })
.toFile(bannerWebp);
console.log(`Generated Banner: ${bannerWebp}`);
// 2. 生成 800x420 列表缩略图 (聚焦画面右侧 3D 核心主体)
const thumbWebp = join(OUT_DIR, `${sample.slug}_thumb_800x420.webp`);
await sharp(sample.bg)
.resize(800, 420, { fit: 'cover', position: 'right' })
.webp({ quality: 86 })
.toFile(thumbWebp);
console.log(`Generated Thumbnail: ${thumbWebp}`);
// 3. 渲染 1200x630 英文版与中文版 OG 分享卡
await renderCard(sample, false);
await renderCard(sample, true);
}
for (const sample of SAMPLES) {
await processSample(sample);
}
console.log(`\nAll sample visual assets generated successfully in ${OUT_DIR}!`);