diff --git a/apps/docs/README.md b/apps/docs/README.md index 74e6e57..9d14172 100644 --- a/apps/docs/README.md +++ b/apps/docs/README.md @@ -47,9 +47,14 @@ monorepo checkout. ## Social cards The homepage and every documentation page have a 1200×630 PNG for Open Graph -and Twitter/X large-image previews. The design follows `monorepo/packages/og`: -black surfaces, a colored edge, the supplied Bloxwap wordmark, Nunito headlines, -and Space Grotesk labels. Fonts and artwork are local assets. +and Twitter/X large-image previews. They are laid out like GitHub's repository +cards, in the site's dark theme and matching the bloxwap/sfx cards: a Bloxwap +black (`#0a0a0a`) canvas, Nunito Bold and Black text in the `--foreground` and +`--muted-foreground` grays, and the green Bloxwap mark. The home card shows a +stats row (built-in indicator and drawing-tool counts from the library, plus the +dependency count and version from `packages/chart/package.json`); page cards +show the section instead. A weighted brand-color bar runs along the bottom. +Fonts are local assets. `lib/og-card.tsx` renders the cards with Next's `ImageResponse`. The static route in `app/og/[...slug]/route.tsx` generates `/og/home.png` and diff --git a/apps/docs/fonts/Nunito-Regular.ttf b/apps/docs/fonts/Nunito-Regular.ttf deleted file mode 100644 index 3bf5ddd..0000000 Binary files a/apps/docs/fonts/Nunito-Regular.ttf and /dev/null differ diff --git a/apps/docs/fonts/README.md b/apps/docs/fonts/README.md index 41d6027..0e52a63 100644 --- a/apps/docs/fonts/README.md +++ b/apps/docs/fonts/README.md @@ -10,9 +10,6 @@ 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 c06449f..7c7457a 100644 --- a/apps/docs/lib/og-card.tsx +++ b/apps/docs/lib/og-card.tsx @@ -1,32 +1,46 @@ import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { ImageResponse } from 'next/og'; +import { BUILTIN_DRAWINGS } from '@bloxwap/chart/drawings'; +import { BUILTIN_INDICATORS } from '@bloxwap/chart/indicators'; import { socialImageSize } from './social'; -// 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'; +// Modeled on GitHub's repository cards, in the site's dark theme (the same card as bloxwap/sfx): the Bloxwap black +// canvas, owner/repo title, muted description, the logo tile top right, a stats row, and a color bar along the +// bottom (the Bloxwap brand palette). +const ink = '#fafafa'; // --foreground +const muted = '#a1a1a1'; // --muted-foreground +const bar: [color: string, share: number][] = [['#00ff3f', 62], ['#35b5ff', 14], ['#b300ff', 10], ['#ff479c', 8], ['#fffb38', 6]]; const assets = Promise.all([ - readFile(join(process.cwd(), 'fonts/Nunito-Regular.ttf')), readFile(join(process.cwd(), 'fonts/Nunito-Bold.ttf')), - readFile(join(process.cwd(), 'public/logos/bloxwap-icon-green.svg'), 'utf8'), + readFile(join(process.cwd(), 'fonts/Nunito-Black.ttf')), readFile(join(process.cwd(), '../../packages/chart/package.json'), 'utf8'), ]); +/** Lucide-style 24px stroke icons, inlined so the renderer needs no icon font or component. */ +const icons: Record = { + indicators: ['M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2'], + drawings: [ + 'M13 7 8.7 2.7a2.41 2.41 0 0 0-3.4 0L2.7 5.3a2.41 2.41 0 0 0 0 3.4L7 13', 'm8 6 2-2', 'm18 16 2-2', + 'm17 11 4.3 4.3c.94.94.94 2.46 0 3.4l-2.6 2.6c-.94.94-2.46.94-3.4 0L11 17', + 'M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z', 'm15 5 4 4', + ], + dependencies: ['M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z', 'm3.3 7 8.7 5 8.7-5', 'M12 22V12'], + version: ['M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z', 'M7.5 7.5h.01'], + docs: ['M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20'], +}; + +function Icon({ name }: { name: string }) { + return + {icons[name].map((d) => )} + ; +} + 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(); @@ -38,60 +52,55 @@ export async function renderSocialCard(options: { category: string; home?: boolean; }): Promise { - 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 [bold, black, manifest] = await assets; + const pkg = JSON.parse(manifest) as { version: string; dependencies?: Record }; + const stats: [icon: string, value: string, label: string][] = [ + ['indicators', String(BUILTIN_INDICATORS.length), 'Indicators'], + ['drawings', String(BUILTIN_DRAWINGS.length), 'Drawing tools'], + ['dependencies', String(Object.keys(pkg.dependencies ?? {}).length), 'Dependencies'], + ['version', `v${pkg.version}`, 'Latest'], ]; - const title = clamp(options.title, 80); + const title = clamp(options.title, 60); const description = clamp(options.description, 150); - const fontSize = title.length <= 24 ? 72 : title.length <= 48 ? 60 : 52; + const fontSize = options.home || title.length <= 20 ? 84 : title.length <= 34 ? 72 : 60; return new ImageResponse( -
-
-
-
- {options.home ? ( -
- bloxwap/ - chart -
- ) : ( -
-
- bloxwap/ - chart - · - {categoryLabel(options.category)} -
-
{title}
-
- )} -
{description}
+
+
+
+
+ {options.home + ? <>bloxwap/chart + : {title}}
- Bloxwap -
-
- {facts.map(([value, label]) => ( -
- {value} - {label} -
- ))} +
{description}
+ + + + + + +
+
+ {options.home + ? stats.map(([icon, value, label]) =>
+
{value}
+
{label}
+
) + :
+ bloxwap/chart·{categoryLabel(options.category)} +
}
-
- {palette.map((color) =>
)} +
+ {bar.map(([color, share]) =>
)}
, { ...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' }, ], }, );