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