← Public packages
@kentcdodds/christ-paintings
Official Latter-day Saint paintings of Jesus Christ, one at a time, to show children during church, plus coloring pages of the Savior. Swipe to move. Works offline.
scripts/render-og.mjs
91 lines · 6.3 KB · JavaScript// Renders the 1200x630 social preview (/og.jpg) with headless Chrome: a real screenshot
// of the running app (one painting from the Church's Media Library, with its caption)
// set in a quiet, warm scene, and every word is live text, so nothing is AI generated.
// Writes src/og-image.ts (base64) and /tmp/christ-paintings-og.jpg.
//
// Usage: node scripts/render-og.mjs [appUrl]
// appUrl defaults to https://christ-paintings.kentcdodds.com/ (or a local
// `kody dev --app-base-path /` URL such as http://127.0.0.1:4598/).
// Needs playwright-core resolvable from here (node_modules is gitignored) and a Chromium;
// set CHROME_PATH to use a specific browser. Fonts: Cormorant Garamond and Inter.
// The address at the bottom is the app URL's host; set OG_SITE_LABEL to change it.
import { chromium } from 'playwright-core'
import { writeFileSync } from 'node:fs'
const APP_URL = process.argv[2] || 'https://christ-paintings.kentcdodds.com/'
const SITE_LABEL = process.env.OG_SITE_LABEL || new URL(APP_URL).host
// "Jesus the Christ" by Del Parson.
const PAINTING_ID = process.env.OG_PAINTING_ID || 'jesus-christ-74e791b'
const browser = await chromium.launch(process.env.CHROME_PATH ? { executablePath: process.env.CHROME_PATH } : {})
// 1. Screenshot the real app on a phone-sized screen, showing one painting and its caption.
const phone = await browser.newPage({ viewport: { width: 390, height: 800 }, deviceScaleFactor: 3, isMobile: true, hasTouch: true })
await phone.addInitScript((id) => {
try {
localStorage.setItem('christ-paintings:current', id)
localStorage.setItem('christ-paintings:caption', 'shown')
} catch (e) {}
}, PAINTING_ID)
await phone.goto(APP_URL, { waitUntil: 'load' })
await phone.waitForFunction(() => {
const slide = [...document.querySelectorAll('.slide')].find((x) => x.getAttribute('aria-hidden') === 'false')
return slide && slide.querySelector('img.ready') && slide.querySelector('img').complete
}, null, { timeout: 30000 })
await phone.evaluate(() => document.querySelectorAll('.update-toast').forEach((el) => el.remove()))
await phone.waitForTimeout(600)
const shot = (await phone.screenshot({ type: 'png' })).toString('base64')
await phone.close()
// 2. Compose the scene: a soft light from above, a few motes of dust, the phone, the words.
let seed = 7
const rnd = () => ((seed = (seed * 16807) % 2147483647) / 2147483647)
const motes = Array.from({ length: 70 }, () => `<i style="left:${(560 + rnd() * 600).toFixed(0)}px;top:${(rnd() * 600).toFixed(0)}px;opacity:${(0.12 + rnd() * 0.5).toFixed(2)};transform:scale(${(0.5 + rnd() * 1.3).toFixed(2)})"></i>`).join('')
const html = `<!doctype html><html><head><meta charset="utf-8"><style>
* { box-sizing: border-box; margin: 0; }
body { width: 1200px; height: 630px; overflow: hidden; background: #0b0a0d; font-family: Inter, system-ui, sans-serif; }
.stage { position: absolute; inset: 0; overflow: hidden;
background:
radial-gradient(ellipse 460px 420px at 900px 250px, rgba(255, 226, 168, 0.22), transparent 70%),
radial-gradient(ellipse 900px 520px at 300px 600px, rgba(120, 88, 48, 0.20), transparent 70%),
radial-gradient(ellipse 1000px 700px at 620px 260px, #1b1813, #0b0a0d 78%); }
.light { position: absolute; left: 700px; top: -160px; width: 420px; height: 900px; transform: rotate(8deg); transform-origin: top center;
background: linear-gradient(to bottom, rgba(255, 240, 205, 0.30), rgba(255, 240, 205, 0.06) 60%, transparent 85%);
clip-path: polygon(40% 0, 60% 0, 100% 100%, 0 100%); filter: blur(18px); mix-blend-mode: screen; }
.motes i { position: absolute; width: 2px; height: 2px; border-radius: 50%; background: #fff3d6; box-shadow: 0 0 5px #fff3d6; }
.phone-wrap { position: absolute; left: 782px; top: 38px; width: 270px; height: 554px; }
.phone { position: absolute; inset: 0; border-radius: 40px; padding: 10px; transform: rotate(3deg);
background: linear-gradient(150deg, #403a31, #0f0e0c 40%, #1f1c18 70%, #4d463b);
box-shadow: 0 0 0 1.5px rgba(255, 255, 255, 0.10) inset, 0 40px 80px rgba(0, 0, 0, 0.75), 0 0 110px rgba(255, 222, 160, 0.22); }
.screen { width: 100%; height: 100%; border-radius: 31px; overflow: hidden; position: relative; background: #0b0b0e; }
.screen img { width: 100%; height: 100%; object-fit: cover; display: block; }
.screen::after { content: ''; position: absolute; inset: 0; background: linear-gradient(120deg, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0) 30%); }
.copy { position: absolute; left: 76px; top: 0; bottom: 0; width: 640px; display: flex; flex-direction: column; justify-content: center; gap: 22px; }
.eyebrow { font-size: 16px; font-weight: 600; letter-spacing: 0.30em; text-transform: uppercase; color: #d6b678; }
h1 { font-family: 'Cormorant Garamond', Georgia, serif; font-weight: 600; font-size: 92px; line-height: 0.98; letter-spacing: 0.005em; color: #f6efe1;
text-shadow: 0 0 28px rgba(255, 226, 168, 0.25), 0 3px 2px rgba(0, 0, 0, 0.6); }
.rule { width: 120px; height: 1px; background: linear-gradient(90deg, #d6b678, rgba(214, 182, 120, 0)); }
.tag { font-size: 27px; line-height: 1.38; color: #e8e2d4; font-weight: 400; max-width: 560px; }
.tag em { font-style: normal; color: #d6b678; }
.url { position: absolute; left: 76px; bottom: 42px; font-size: 20px; font-weight: 500; letter-spacing: 0.06em; color: rgba(232, 226, 212, 0.66); }
</style></head><body><div class="stage">
<div class="light"></div>
<div class="motes">${motes}</div>
<div class="phone-wrap"><div class="phone"><div class="screen"><img src="data:image/png;base64,${shot}" alt=""></div></div></div>
<div class="copy">
<div class="eyebrow">For children at church</div>
<h1>Paintings of<br>Jesus Christ</h1>
<div class="rule"></div>
<p class="tag">Official Latter-day Saint art, one painting at a time. <em>Plus coloring pages.</em></p>
</div>
<div class="url">${SITE_LABEL}</div>
</div></body></html>`
const page = await browser.newPage({ viewport: { width: 1200, height: 630 }, deviceScaleFactor: 1 })
await page.setContent(html, { waitUntil: 'load' })
await page.evaluate(() => document.fonts.ready)
const jpeg = await page.screenshot({ type: 'jpeg', quality: 88, clip: { x: 0, y: 0, width: 1200, height: 630 } })
await browser.close()
writeFileSync('/tmp/christ-paintings-og.jpg', jpeg)
writeFileSync('src/og-image.ts', '// Generated by scripts/render-og.mjs. Do not edit by hand.\nexport const OG_IMAGE_JPEG = \'' + jpeg.toString('base64') + '\'\n')
console.log('og.jpg bytes', jpeg.length)