#!/usr/bin/env node /** * render_sample_suite_v3.mjs — "one art, four targets" sample set from two real * Ideogram 4 base images (no GPU re-run needed). * * Template: scripts/og-gen/template-v2.2.html * - outside consultant's v2.0 Bento spec (frosted card, blueprint grid, 2-line * description, full-width evidence bar) * - plus a self-fitting headline script: shrinks font until the title fits * <=3 lines with no widow line, so long Chinese/English titles never leave a * single orphan character on the last row. * * Chrome runs with --virtual-time-budget so the fitting script has settled before * the screenshot; the chosen size is read back from as proof it ran. */ import { readFileSync, writeFileSync, mkdirSync } 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-v2'; 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.2.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: '独自运维 162 个生产容器:两起故障改变了我验证数字的方式', desc_en: 'One operator, three hosts, 162 containers in 78 compose stacks — and the two failures that changed how I verify my own numbers.', desc_zh: '一个人、三台宿主、78 个 compose 栈里的 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 杀死了它', desc_en: 'A container kept stopping while Docker reported exit 0 and OOMKilled=false. The honest evidence was in dmesg: six cgroup OOM kills.', desc_zh: '容器不停意外停止,而 Docker 报的是 exit 0 / OOMKilled=false。真正的证据在 dmesg 里:六次 cgroup OOM。' } ]; function startTitleSize(title, isZh) { return isZh ? 50 : title.length <= 42 ? 48 : 44; } /** Consultant v2.0: CJK natural wrapping splits compounds ("两|起故障"). Break at a * colon/comma instead — the self-fitting loop then sizes each segment to one line. */ function formatSmartTitle(title, isZh) { if (!isZh || title.includes('<br>')) return title; const m = title.match(/^(.{6,30}?[:,,——])(.{5,})$/); return m ? `${m[1]}<br>${m[2]}` : title; } /** Keep a word and the number right after it on one line ("Exit 0." must never * break as "Exit / 0."), so numeric evidence stays intact. */ function glueNumbers(title) { return title.replace(/([\p{L}])\s+(\d)/gu, '$1\u00A0$2'); } function renderCard(sample, isZh) { const lang = isZh ? 'zh' : 'en'; const title = glueNumbers(formatSmartTitle(isZh ? sample.title_zh : sample.title_en, isZh)); const desc = isZh ? sample.desc_zh : sample.desc_en; const startPx = startTitleSize(title, isZh); const tagsHtml = sample.tags.map((t) => `<span>${t}</span>`).join(''); const bgUrl = 'file:///' + sample.bg.replace(/\\/g, '/'); const html = TPL_RAW .replace(/\{\{LANG\}\}/g, isZh ? 'zh-CN' : 'en') .replace(/\{\{BG_IMAGE_SRC\}\}/g, bgUrl) .replace(/\{\{CATEGORY\}\}/g, sample.category) .replace(/\{\{TAGS_HTML\}\}/g, tagsHtml) .replace(/\{\{TITLE_SIZE\}\}/g, String(startPx)) .replace(/\{\{TITLE_HTML\}\}/g, title) .replace(/\{\{DESCRIPTION\}\}/g, desc) .replace(/\{\{EVIDENCE_SUMMARY\}\}/g, sample.evidence) .replace(/\{\{AVATAR_PATH\}\}/g, AVATAR_URL); if (/\{\{[A-Z_]+\}\}/.test(html)) { throw new Error(`unsubstituted placeholder: ${html.match(/\{\{[A-Z_]+\}\}/g)}`); } const tmpHtml = join(OUT_DIR, `temp_${sample.slug}_${lang}.html`); writeFileSync(tmpHtml, html, 'utf8'); const fileUrl = 'file:///' + tmpHtml.replace(/\\/g, '/'); const dom = execSync( `"${CHROME}" --headless --dump-dom --virtual-time-budget=2500 --disable-gpu "${fileUrl}"`, { stdio: 'pipe', maxBuffer: 32 * 1024 * 1024 } ).toString('utf8'); const m = dom.match(/<title>([^<]*)<\/title>/); const fit = m ? m[1] : '(no title)'; if (!/^FIT \d+px/.test(fit)) throw new Error(`fitting script did not settle: ${fit}`); const outPng = join(OUT_DIR, `${sample.slug}_og_${lang}.png`); execSync( `"${CHROME}" --headless --screenshot="${outPng}" --window-size=1200,630 ` + `--hide-scrollbars --disable-gpu --force-device-scale-factor=1 ` + `--virtual-time-budget=2500 "${fileUrl}"`, { stdio: 'pipe' } ); console.log(` OG (${lang}) start ${startPx}px -> ${fit}`); } async function processSample(s) { console.log(`\n=== ${s.slug} ===`); const banner = join(OUT_DIR, `${s.slug}_banner_1600x900.webp`); await sharp(s.bg).resize(1600, 900, { fit: 'cover', position: 'right' }) .webp({ quality: 88 }).toFile(banner); const thumb = join(OUT_DIR, `${s.slug}_thumb_800x420.webp`); await sharp(s.bg).resize(800, 420, { fit: 'cover', position: 'right' }) .webp({ quality: 86 }).toFile(thumb); renderCard(s, false); renderCard(s, true); } for (const s of SAMPLES) await processSample(s); console.log(`\nDone -> ${OUT_DIR}`);