import satori from 'satori' import { Resvg } from '@resvg/resvg-js' let fontCache async function loadFonts() { if (fontCache) return fontCache const storage = useStorage('assets:server') const [brygadaRegular, brygadaSemiBold, commitRegular, commitBold] = await Promise.all([ storage.getItemRaw('fonts/Brygada1918-Regular.ttf'), storage.getItemRaw('fonts/Brygada1918-SemiBold.ttf'), storage.getItemRaw('fonts/commit-mono-400.woff'), storage.getItemRaw('fonts/commit-mono-600.woff') ]) fontCache = [ { name: 'Brygada 1918', data: brygadaRegular, weight: 400, style: 'normal' }, { name: 'Brygada 1918', data: brygadaSemiBold, weight: 600, style: 'normal' }, { name: 'Commit Mono', data: commitRegular, weight: 400, style: 'normal' }, { name: 'Commit Mono', data: commitBold, weight: 600, style: 'normal' } ] return fontCache } // Insert Cloudinary transform params (resize, quality) into the URL so we // embed a smaller image and keep the resulting PNG under a reasonable size. function withCloudinaryTransform(url) { if (typeof url !== 'string') return url return url.replace( /\/image\/upload\/(?!w_|h_|c_|q_|f_)/, '/image/upload/w_1200,h_340,c_fill,q_auto,f_jpg/' ) } async function fetchImageAsDataUrl(url) { if (!url) return null try { const transformed = withCloudinaryTransform(url) const res = await fetch(transformed) if (!res.ok) return null const buf = Buffer.from(await res.arrayBuffer()) const contentType = res.headers.get('content-type') || 'image/jpeg' return `data:${contentType};base64,${buf.toString('base64')}` } catch { return null } } function formatEventDate(startDate, timeZone) { const date = new Date(startDate) const dateFmt = new Intl.DateTimeFormat('en-US', { weekday: 'long', month: 'long', day: 'numeric', timeZone }).format(date) const timeFmt = new Intl.DateTimeFormat('en-US', { hour: 'numeric', minute: '2-digit', timeZoneName: 'short', timeZone }).format(date) return `${dateFmt} · ${timeFmt}` } // Design tokens lifted from app/assets/css/main.css (light palette). // Hardcoded here because satori doesn't resolve CSS variables. const COLOR = { bg: '#f4efe4', textBright: '#1a1008', textDim: '#5a5040', candle: '#7a5a10', border: '#b8a880', parch: '#2a2015', parchText: '#ede4d0', parchAccent: '#c4a448' } function buildTemplate({ title, dateLabel, imageDataUrl }) { const hasImage = Boolean(imageDataUrl) const imageBlock = hasImage ? { type: 'div', props: { style: { display: 'flex', width: '100%', height: 340, overflow: 'hidden', borderBottom: `2px dashed ${COLOR.border}` }, children: { type: 'img', props: { src: imageDataUrl, width: 1200, height: 340, style: { width: '100%', height: '100%', objectFit: 'cover' } } } } } : { type: 'div', props: { style: { display: 'flex', width: '100%', height: 340, background: COLOR.parch, alignItems: 'center', justifyContent: 'center', borderBottom: `2px dashed ${COLOR.border}` }, children: { type: 'div', props: { style: { fontFamily: 'Brygada 1918', fontSize: 64, fontWeight: 600, color: COLOR.parchText, letterSpacing: '-0.01em' }, children: 'GHOST GUILD' } } } } return { type: 'div', props: { style: { display: 'flex', flexDirection: 'column', width: 1200, height: 630, background: COLOR.bg, fontFamily: 'Commit Mono' }, children: [ imageBlock, { type: 'div', props: { style: { display: 'flex', flexDirection: 'column', padding: '36px 60px 48px 60px', flex: 1, justifyContent: 'space-between' }, children: [ { type: 'div', props: { style: { display: 'flex', flexDirection: 'column', gap: 18 }, children: [ { type: 'div', props: { style: { fontFamily: 'Commit Mono', fontSize: 18, fontWeight: 600, color: COLOR.candle, letterSpacing: '0.12em', textTransform: 'uppercase' }, children: 'Ghost Guild · Event' } }, { type: 'div', props: { style: { fontFamily: 'Brygada 1918', fontSize: 56, fontWeight: 600, color: COLOR.textBright, lineHeight: 1.1, letterSpacing: '-0.01em', maxHeight: 140, overflow: 'hidden' }, children: title } } ] } }, { type: 'div', props: { style: { fontFamily: 'Commit Mono', fontSize: 22, color: COLOR.textDim }, children: dateLabel } } ] } } ] } } } export async function renderEventOG(event) { const fonts = await loadFonts() const imageDataUrl = event.featureImage?.url ? await fetchImageAsDataUrl(event.featureImage.url) : null const dateLabel = formatEventDate( event.startDate, event.displayTimezone || 'America/Toronto' ) const svg = await satori( buildTemplate({ title: event.title, dateLabel, imageDataUrl }), { width: 1200, height: 630, fonts } ) const png = new Resvg(svg, { fitTo: { mode: 'width', value: 1200 } }) .render() .asPng() return png }