diff --git a/apps/docs/fonts/Nunito-Regular.ttf b/apps/docs/fonts/Nunito-Regular.ttf new file mode 100644 index 0000000..3bf5ddd Binary files /dev/null and b/apps/docs/fonts/Nunito-Regular.ttf differ diff --git a/apps/docs/fonts/README.md b/apps/docs/fonts/README.md index 0e52a63..41d6027 100644 --- a/apps/docs/fonts/README.md +++ b/apps/docs/fonts/README.md @@ -10,6 +10,9 @@ self-hosted, so documentation builds and visitors do not need Google Fonts reque `monorepo/packages/og/assets/` for Open Graph rendering (weights 700 and 900). They share the adjacent `Nunito-OFL.txt` license; the image renderer needs TTF rather than the site's WOFF2 font. +- **Nunito Regular**: a static weight-400 TTF instanced from `Nunito-Latin.woff2` + with fontTools (`instancer.instantiateVariableFont(font, {"wght": 400})`) for the + Open Graph descriptions. Same Latin subset and `Nunito-OFL.txt` license. - **Space Grotesk Bold**: copied with its OFL license from `monorepo/workers/www/public/fonts/`. - **Maple Mono**: variable subset, weights 100–800, copied with its OFL license from diff --git a/apps/docs/lib/og-card.tsx b/apps/docs/lib/og-card.tsx index 0a7cb69..c06449f 100644 --- a/apps/docs/lib/og-card.tsx +++ b/apps/docs/lib/og-card.tsx @@ -3,65 +3,95 @@ import { join } from 'node:path'; import { ImageResponse } from 'next/og'; import { socialImageSize } from './social'; -// Match monorepo/packages/og: Nunito headlines, Space Grotesk labels, a drawn -// wordmark, black canvas, and a 24px rail from the Bloxwap sRGB brand palette. +// A GitHub-style card: white canvas, title and description top left, the +// Bloxwap mark where GitHub puts the owner avatar, package facts along the +// bottom, and the brand palette as the language bar. const palette = ['#00ff3f', '#35b5ff', '#b300ff', '#ff479c', '#fffb38']; +const ink = '#1f2328'; +const muted = '#59636e'; const assets = Promise.all([ + readFile(join(process.cwd(), 'fonts/Nunito-Regular.ttf')), readFile(join(process.cwd(), 'fonts/Nunito-Bold.ttf')), - readFile(join(process.cwd(), 'fonts/Nunito-Black.ttf')), - readFile(join(process.cwd(), 'fonts/SpaceGrotesk-Bold.ttf')), - readFile(join(process.cwd(), 'public/logos/bloxwap-wordmark-white.svg')), + readFile(join(process.cwd(), 'public/logos/bloxwap-icon-green.svg'), 'utf8'), + readFile(join(process.cwd(), '../../packages/chart/package.json'), 'utf8'), ]); -function accentFor(title: string): string { - let hash = 0x811c9dc5; - for (let i = 0; i < title.length; i++) hash = Math.imul(hash ^ title.charCodeAt(i), 0x01000193); - return palette[(hash >>> 0) % palette.length]; -} - function clamp(text: string, max: number): string { const points = [...text.trim()]; return points.length > max ? `${points.slice(0, max - 1).join('')}…` : points.join(''); } +/** The mark as a plain SVG data URI; the renderer ignores the file's C2PA metadata and P3 styles. */ +function markUri(svg: string): string { + const plain = svg.replace(/[\s\S]*?<\/metadata>/, '').replace(/\sstyle="[^"]*"/g, ''); + return `data:image/svg+xml;base64,${Buffer.from(plain).toString('base64')}`; +} + +/** Sentence-cases the route's category label, e.g. `API REFERENCE` → `API reference`. */ +function categoryLabel(category: string): string { + return category.startsWith('API') ? `API${category.slice(3).toLowerCase()}` : category.charAt(0) + category.slice(1).toLowerCase(); +} + export async function renderSocialCard(options: { title: string; description: string; category: string; home?: boolean; }): Promise { - const [bold, black, display, wordmark] = await assets; - const title = clamp(options.title, 90); - const description = clamp(options.description, 180); - const accent = options.home ? palette[0] : accentFor(title); - const fontSize = title.length <= 14 ? 148 : title.length <= 28 ? 116 : title.length <= 46 ? 92 : title.length <= 66 ? 72 : 58; + const [regular, bold, mark, manifest] = await assets; + const pkg = JSON.parse(manifest) as { version: string; license: string; dependencies?: Record }; + const facts: [string, string][] = [ + [`v${pkg.version}`, 'Latest'], + [pkg.license, 'License'], + [String(Object.keys(pkg.dependencies ?? {}).length), 'Dependencies'], + ]; + const title = clamp(options.title, 80); + const description = clamp(options.description, 150); + const fontSize = title.length <= 24 ? 72 : title.length <= 48 ? 60 : 52; return new ImageResponse( -
-
-
-
-
- Bloxwap - · - CHART +
+
+
+
+ {options.home ? ( +
+ bloxwap/ + chart +
+ ) : ( +
+
+ bloxwap/ + chart + · + {categoryLabel(options.category)} +
+
{title}
+
+ )} +
{description}
- {options.category} + Bloxwap
-
-
= 116 ? -4 : -2, lineHeight: 1 }}> - {options.home ? ['Financial charts.', 'Your interface.'].map((line) =>
{line}
) : title} -
-
{description}
+
+ {facts.map(([value, label]) => ( +
+ {value} + {label} +
+ ))}
+
+ {palette.map((color) =>
)} +
, { ...socialImageSize, fonts: [ + { name: 'Nunito', data: regular, weight: 400, style: 'normal' }, { name: 'Nunito', data: bold, weight: 700, style: 'normal' }, - { name: 'Nunito', data: black, weight: 900, style: 'normal' }, - { name: 'Space Grotesk', data: display, weight: 700, style: 'normal' }, ], }, );