export type SatoriNode = | string | number | { type: string; props: { style?: Record; children?: SatoriNode | SatoriNode[]; [key: string]: unknown; }; }; export const OG_WIDTH = 1200; export const OG_HEIGHT = 630; export const DIM_COLOR = '#555555'; export function svgToDataUri(svg: string, fill?: string): string { let processed = svg; if (fill) { processed = processed.replace(/ string; paddingRight?: number; }): SatoriNode | undefined { const fmt = params.formatText ?? ((t: string) => t); const items: SatoriNode[] = []; if (params.logotypeDataUri) { items.push({ type: 'div', props: { style: { display: 'flex', alignItems: 'center', justifyContent: 'center', width: 64, height: 64, borderRadius: '50%', backgroundColor: 'white', border: '2px solid white', boxShadow: '0 2px 8px rgba(0,0,0,0.1)', overflow: 'hidden', }, children: [ { type: 'img', props: { src: params.logotypeDataUri, width: 54, height: 54, style: { objectFit: 'contain' as const }, }, }, ], }, }); } if (params.siteName) { items.push({ type: 'span', props: { style: { fontSize: 36, color: DIM_COLOR, fontWeight: 600, }, children: fmt(params.siteName), }, }); } if (items.length === 0) return undefined; return { type: 'div', props: { style: { display: 'flex', alignItems: 'center', gap: 14, paddingLeft: 56, paddingRight: params.paddingRight ?? 60, paddingTop: 48, }, children: items, }, }; } export function ogDescription(params: { description: string; formatText?: (text: string) => string; maxLen?: number; paddingRight?: number; }): SatoriNode { const fmt = params.formatText ?? ((t: string) => t); const trimmed = truncate(params.description, params.maxLen ?? 360); return { type: 'div', props: { style: { display: 'flex', paddingLeft: 60, paddingRight: params.paddingRight ?? 60, paddingBottom: 48, }, children: [ { type: 'div', props: { style: { fontSize: 24, fontWeight: 400, color: DIM_COLOR, textAlign: 'left' as const, lineHeight: 1.4, wordBreak: 'break-word' as const, }, children: fmt(trimmed), }, }, ], }, }; } const ARROW_RIGHT_SVG = ``; export function ogActionButton(brandColor: string, text: string): SatoriNode { return { type: 'div', props: { style: { display: 'flex', alignItems: 'center', gap: 10, marginTop: 24, backgroundColor: brandColor, borderRadius: 32, paddingLeft: 32, paddingRight: 26, paddingTop: 14, paddingBottom: 14, boxShadow: `0 6px 20px rgba(0,0,0,0.25), 0 2px 6px rgba(0,0,0,0.1)`, alignSelf: 'flex-start' as const, border: '3px solid rgba(255,255,255,0.4)', }, children: [ { type: 'span', props: { style: { fontSize: 24, fontWeight: 700, color: '#ffffff', letterSpacing: 0.5, }, children: text, }, }, { type: 'img', props: { src: 'data:image/svg+xml,' + encodeURIComponent(ARROW_RIGHT_SVG), width: 22, height: 22, }, }, ], }, }; } export function ogBottomSpacer(): SatoriNode { return { type: 'div', props: { style: { display: 'flex', paddingTop: 80 }, children: [], }, }; } export function sendOgPng(event: Parameters[0], png: Buffer) { setHeader(event, 'Content-Type', 'image/png'); setHeader(event, 'Cache-Control', 'public, max-age=86400, s-maxage=86400'); return png; } function getOgImageConfig() { return ERUDIT.config.public.seo?.ogImage; } export function checkOgEnabled() { const ogImageConfig = getOgImageConfig(); if (ogImageConfig?.type !== 'auto') { throw createError({ statusCode: 404, statusMessage: 'OG image generation is disabled', }); } } export function getOgBrandColor(): string { const ogImageConfig = getOgImageConfig(); if (ogImageConfig?.type === 'auto') { return ogImageConfig.siteColor; } return '#4aa44c'; } export function getOgSiteName(): string | undefined { const ogImageConfig = getOgImageConfig(); if (ogImageConfig?.type === 'auto') { return ogImageConfig.siteName; } return undefined; } export function getOgSiteShort(): string | undefined { const ogImageConfig = getOgImageConfig(); if (ogImageConfig?.type === 'auto') { return ogImageConfig.siteShort; } return undefined; } export function getOgLearnPhrase(): string { const ogImageConfig = getOgImageConfig(); if (ogImageConfig?.type === 'auto') { return typeof ogImageConfig.phrases === 'string' ? ogImageConfig.phrases : ogImageConfig.phrases.learn; } return 'Learn'; } export function getOgOpenPhrase(): string { const ogImageConfig = getOgImageConfig(); if (ogImageConfig?.type === 'auto') { return typeof ogImageConfig.phrases === 'string' ? ogImageConfig.phrases : ogImageConfig.phrases.open; } return 'Open'; } export function getOgLogotypePath(): string | undefined { const ogImageConfig = getOgImageConfig(); if (ogImageConfig?.type === 'auto') { return ogImageConfig.logotype; } return undefined; }