Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added apps/docs/fonts/Nunito-Regular.ttf
Binary file not shown.
3 changes: 3 additions & 0 deletions apps/docs/fonts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
94 changes: 62 additions & 32 deletions apps/docs/lib/og-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(/<metadata>[\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<ImageResponse> {
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<string, string> };
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(
<div style={{ display: 'flex', position: 'relative', width: '100%', height: '100%', background: '#0a0a0a', color: '#f5f7fa', fontFamily: 'Nunito', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 0, left: 0, width: 24, height: '100%', background: accent }} />
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100%', padding: '56px 72px 54px 78px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 18 }}>
<img src={`data:image/svg+xml;base64,${wordmark.toString('base64')}`} width={190} height={52} alt="Bloxwap" />
<span style={{ fontFamily: 'Space Grotesk', fontSize: 26, color: '#636363' }}>·</span>
<span style={{ fontFamily: 'Space Grotesk', fontSize: 26, fontWeight: 700, letterSpacing: 5, color: accent }}>CHART</span>
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100%', background: '#ffffff', color: ink, fontFamily: 'Nunito' }}>
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, padding: '80px 80px 56px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 64 }}>
<div style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
{options.home ? (
<div style={{ display: 'flex', fontSize: 72, lineHeight: 1.15 }}>
<span style={{ fontWeight: 400 }}>bloxwap/</span>
<span style={{ fontWeight: 700 }}>chart</span>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', fontSize: 30, color: muted }}>
<span style={{ fontWeight: 400 }}>bloxwap/</span>
<span style={{ fontWeight: 700 }}>chart</span>
<span style={{ fontWeight: 400, margin: '0 12px' }}>·</span>
<span style={{ fontWeight: 400 }}>{categoryLabel(options.category)}</span>
</div>
<div style={{ display: 'flex', marginTop: 16, fontSize, fontWeight: 700, lineHeight: 1.15 }}>{title}</div>
</div>
)}
<div style={{ display: 'flex', marginTop: 28, fontSize: 30, fontWeight: 400, lineHeight: 1.4, color: muted }}>{description}</div>
</div>
<span style={{ fontSize: 18, fontWeight: 700, letterSpacing: 2, color: '#a1a1a1' }}>{options.category}</span>
<img src={markUri(mark)} width={128} height={128} alt="Bloxwap" />
</div>
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, justifyContent: 'flex-end' }}>
<div style={{ display: 'flex', flexDirection: 'column', fontSize, fontWeight: 900, letterSpacing: fontSize >= 116 ? -4 : -2, lineHeight: 1 }}>
{options.home ? ['Financial charts.', 'Your interface.'].map((line) => <div key={line} style={{ display: 'flex' }}>{line}</div>) : title}
</div>
<div style={{ display: 'flex', maxWidth: 960, marginTop: 34, fontSize: 28, fontWeight: 700, lineHeight: 1.3, color: '#a1a1a1' }}>{description}</div>
<div style={{ display: 'flex', marginTop: 'auto', gap: 48, fontSize: 28 }}>
{facts.map(([value, label]) => (
<div key={label} style={{ display: 'flex', gap: 10 }}>
<span style={{ fontWeight: 700 }}>{value}</span>
<span style={{ fontWeight: 400, color: muted }}>{label}</span>
</div>
))}
</div>
</div>
<div style={{ display: 'flex', height: 16 }}>
{palette.map((color) => <div key={color} style={{ display: 'flex', flex: 1, background: color }} />)}
</div>
</div>,
{
...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' },
],
},
);
Expand Down
Loading