// site/scripts/render-brand-assets.ts // 검색·공유용 이미지를 만든다. 문구는 i18n 번역에서, 로고는 public/favicon.svg 에서 가져온다. // public/og/.png 1200×630 공유 미리보기(Open Graph·X 카드) // public/icons/*.png 파비콘·apple-touch-icon·PWA 아이콘 // 실행: npx vite-node site/scripts/render-brand-assets.ts (Chrome 또는 Playwright Chromium, 인터넷으로 Pretendard 로드) // 문구나 로고가 바뀌면 다시 실행해서 커밋한다. import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { chromium } from 'playwright' import { LOCALES, translations } from '../src/i18n' const siteRoot = join(dirname(fileURLToPath(import.meta.url)), '..') const publicDir = join(siteRoot, 'public') const favicon = readFileSync(join(publicDir, 'favicon.svg'), 'utf8') const FONT_CSS = 'https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css' // 앱 녹음 캡슐(recording-tip)과 같은 막대 색 const BAR_COLORS = ['#93c5fd', '#60a5fa', '#4f8dfa', '#3b82f6', '#3b82f6', '#5b75f7', '#6e71f7', '#818cf8', '#a78bfa'] const BAR_HEIGHTS = [10, 18, 28, 38, 44, 38, 28, 18, 10] function escapeHtml(value: string): string { return value.replace(/&/g, '&').replace(//g, '>') } function ogHtml(code: (typeof LOCALES)[number]['code']): string { const t = translations[code] const bars = BAR_HEIGHTS.map( (h, i) => ``, ).join('') return `
${favicon}D3RO·VOICE

${escapeHtml(t.hero.title1)}${escapeHtml(t.hero.title2)}

${escapeHtml(t.hero.trust)}

${bars}
0:03
` } function iconHtml(size: number, padding: number, background: string): string { return `${favicon}` } // 설치된 Chrome 을 먼저 쓰고, 없으면 Playwright 가 받은 Chromium 을 쓴다. const browser = await chromium.launch({ channel: 'chrome' }).catch(() => chromium.launch()) try { const page = await browser.newPage({ deviceScaleFactor: 1 }) mkdirSync(join(publicDir, 'og'), { recursive: true }) for (const { code } of LOCALES) { await page.setViewportSize({ width: 1200, height: 630 }) await page.setContent(ogHtml(code), { waitUntil: 'networkidle' }) await page.evaluate(() => document.fonts.ready) writeFileSync(join(publicDir, 'og', `${code}.png`), await page.screenshot({ type: 'png' })) } mkdirSync(join(publicDir, 'icons'), { recursive: true }) const icons: Array<[name: string, size: number, padding: number, background: string]> = [ ['favicon-32.png', 32, 0, 'transparent'], ['apple-touch-icon.png', 180, 14, '#08090c'], ['icon-192.png', 192, 0, 'transparent'], ['icon-512.png', 512, 0, 'transparent'], // 안드로이드 마스커블: 안전 영역(80%) 안에 로고를 둔다 ['icon-maskable-512.png', 512, 64, '#19191b'], ] for (const [name, size, padding, background] of icons) { await page.setViewportSize({ width: size, height: size }) await page.setContent(iconHtml(size, padding, background)) writeFileSync( join(publicDir, 'icons', name), await page.screenshot({ type: 'png', omitBackground: background === 'transparent' }), ) } } finally { await browser.close() } console.log(`[brand-assets] og/*.png ×${LOCALES.length}, icons ×5`)