import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { ImageResponse } from 'next/og'; /** * Social-share images (Open Graph / Twitter cards), 1200 × 630, rendered server-side with the site * fonts (WOFF copies in src/assets/fonts). Shared by the site default (`app/opengraph-image.tsx`) * and the entity images (cancer, gene, drug, biomarker, trial). Content is limited to sourced facts * passed in by the caller — never estimates. */ export const OG_SIZE = { width: 1200, height: 630 } as const; export const OG_CONTENT_TYPE = 'image/png'; const PAPER = '#fafaf7'; const INK = '#1c1c1a'; const INK2 = '#4a4a46'; const INK3 = '#7b7b75'; const RULE = '#dcdcd4'; const ACCENT = '#0f5f63'; const ACCENT_SOFT = '#e2eeee'; let fontCache: Promise> | null = null; let markCache: Promise | null = null; function assetDir(): string { return path.join(process.cwd(), 'src', 'assets', 'fonts'); } async function loadFonts() { if (!fontCache) { fontCache = (async () => { const dir = assetDir(); const read = async (f: string) => { const b = await readFile(path.join(dir, f)); return b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength) as ArrayBuffer; }; return [ { name: 'Newsreader', data: await read('newsreader-500.woff'), weight: 500 as const, style: 'normal' as const }, { name: 'Newsreader', data: await read('newsreader-500i.woff'), weight: 500 as const, style: 'italic' as const }, { name: 'Inter', data: await read('inter-400.woff'), weight: 400 as const, style: 'normal' as const }, { name: 'Inter', data: await read('inter-600.woff'), weight: 600 as const, style: 'normal' as const }, ]; })(); } return fontCache; } /** The brand mark as a PNG data URI (satori renders ; SVG dash arrays are not supported). */ async function loadMark(): Promise { if (!markCache) { markCache = readFile(path.join(process.cwd(), 'public', 'brand', 'logo-mark.png')).then((b) => `data:image/png;base64,${b.toString('base64')}`); } return markCache; } export interface OgFact { label: string; value: string; } export interface OgProps { /** Small upper label: entity kind or page family ("Cancer · NCIt C9005", "Gene", "Clinical trial"). */ kicker: string; title: string; /** One line under the title (canonical name, mechanism, sponsor…). */ subtitle?: string | null; /** Up to four sourced figures shown as chips (label + value). */ facts?: OgFact[]; /** Footer right-hand note, e.g. the sources behind the facts. */ sourcesNote?: string | null; } function clampTitle(t: string): { text: string; size: number } { const text = t.length > 110 ? `${t.slice(0, 107).trimEnd()}…` : t; const size = text.length <= 28 ? 84 : text.length <= 48 ? 68 : text.length <= 72 ? 56 : 46; return { text, size }; } export async function ogImage(props: OgProps): Promise { const [fonts, mark] = await Promise.all([loadFonts(), loadMark()]); const { text, size } = clampTitle(props.title); const facts = (props.facts ?? []).slice(0, 4); return new ImageResponse( (
{/* top row: brand */}
Cancer Index
The global index of cancer
{props.kicker}
{/* title block */}
{text}
{props.subtitle ?
{props.subtitle.length > 140 ? `${props.subtitle.slice(0, 137).trimEnd()}…` : props.subtitle}
: null}
{/* facts */} {facts.length ? (
{facts.map((f) => (
{f.value}
{f.label}
))}
) : null} {/* footer */}
www.cancerindex.io · a source, a date and a formula version on every number
{props.sourcesNote ? {props.sourcesNote.length > 90 ? `${props.sourcesNote.slice(0, 87).trimEnd()}…` : props.sourcesNote} : null}
), { ...OG_SIZE, fonts }, ); }