Skip to content

Commit e4eff6b

Browse files
AlemTuzlakautofix-ci[bot]LadyBluenotes
authored
feat(og): dynamic package-themed OG images (#835)
* chore: add satori and resvg-js for dynamic OG image generation * chore: add Inter TTF fonts for Satori OG generation * feat(og): add library accent color map * feat(og): module-scope loader for Satori fonts and island asset * feat(og): Satori JSX template for package OG images * feat(og): PNG generator via Satori and resvg * chore(og): add preview script for local visual verification * feat(og): add doc-title slot between library name and description * feat(og): /api/og/:library.png resource route with CDN cache headers * test(smoke): cover /api/og endpoint with status + content-type assertions * feat(og): ogImageUrl helper for SEO call sites * feat(og): use generated OG image on library landing pages * feat(og): use generated OG image on docs pages * chore(og): remove unused static ogImage fields from libraries * fix(og): clamp title length, drop unused color export, include fonts in function bundle * ci: apply automated fixes * feat(og): separate pitch slot from description, fix slot order * feat(og): show pitch only on landing pages (no doc title or description) * fix(og): add empty alt to decorative island img for lint * refactor(og): migrate satori + resvg to takumi Swap the two-stage satori (JSX→SVG) + resvg-js (SVG→PNG) pipeline for takumi's single-stage Rust renderer. Smaller PNGs (~22%) and faster cold start on the Netlify function. The island PNG moves from an inline base64 data URL to takumi's persistent-image cache, referenced by key. Route handler now returns the ImageResponse directly instead of wrapping a Buffer. * fix(og): use deploy origin for og:image so previews resolve canonicalUrl always returns the production hostname, so on Netlify preview/branch deploys the og:image meta tag pointed at https://tanstack.com/api/og/<id>.png — which 404s if production hasn't shipped the endpoint yet, making validators report the image as invalid/unreachable. og:image now prefers DEPLOY_PRIME_URL → DEPLOY_URL → URL → SITE_URL on SSR, so each deploy emits a same-origin og:image URL. Production behavior is unchanged (DEPLOY_PRIME_URL === URL on production builds). Canonical link tags continue to use canonicalUrl and point to prod. * Merge branch 'main' into feat/dynamic-og-images * fix(og): address coderabbit review - Inter-Regular registered with weight 400 (was 700) — matches the font file and pairs cleanly with Inter-ExtraBold at 800 so template fontWeight 500 resolves to the closer regular face. - Tighten LIBRARY_ACCENT_COLORS to Partial<Record<LibraryId, string>> so removing or renaming a library id is a TS error rather than a silent fallback to the default. getAccentColor takes LibraryId now. - Share MAX_OG_TITLE_LENGTH / MAX_OG_DESCRIPTION_LENGTH / clampOgText via src/utils/og-limits.ts and clamp client-side in ogImageUrl so the og:image URL stays bounded and CDN cache keys stay stable. - Hoist duplicated title/description strings in the examples route head() so seo() and ogImageUrl() can't drift. - Smoke tests: add a 404 assertion for unknown library ids, and report HTML / OG / Total counts separately so a mid-run "X passed" isn't misleading. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Sarah Gerrard <gerrardsarah@gmail.com>
1 parent e51f9b0 commit e4eff6b

39 files changed

Lines changed: 736 additions & 45 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ test-results
3535
.eslintcache
3636
.tsbuildinfo
3737
src/routeTree.gen.ts
38+
.og-preview/

netlify.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ publish = "dist/client"
77

88
[functions]
99
directory = "netlify/functions"
10+
included_files = [
11+
"public/fonts/Inter-Regular.ttf",
12+
"public/fonts/Inter-ExtraBold.ttf",
13+
"public/images/logos/splash-dark.png",
14+
]
1015

1116
[[headers]]
1217
for = "/*"

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@
4747
"@shopify/hydrogen-react": "^2026.4.0",
4848
"@tailwindcss/typography": "^0.5.19",
4949
"@tailwindcss/vite": "^4.2.2",
50+
"@takumi-rs/core": "^1.1.2",
51+
"@takumi-rs/helpers": "^1.1.2",
52+
"@takumi-rs/image-response": "^1.1.2",
5053
"@tanstack/ai": "^0.10.2",
5154
"@tanstack/ai-anthropic": "^0.7.3",
5255
"@tanstack/ai-client": "^0.7.9",

pnpm-lock.yaml

Lines changed: 153 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/fonts/Inter-ExtraBold.ttf

320 KB
Binary file not shown.

public/fonts/Inter-Regular.ttf

317 KB
Binary file not shown.

scripts/og-preview.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Renders one OG image per library to `.og-preview/`.
3+
* Run with: pnpm exec tsx scripts/og-preview.ts
4+
*/
5+
import { mkdirSync, writeFileSync } from 'node:fs'
6+
import { resolve } from 'node:path'
7+
import { libraries } from '../src/libraries/libraries'
8+
import { generateOgImageResponse } from '../src/server/og/generate.server'
9+
10+
const OUT_DIR = resolve(process.cwd(), '.og-preview')
11+
12+
async function renderToFile(
13+
outPath: string,
14+
input: Parameters<typeof generateOgImageResponse>[0],
15+
) {
16+
const result = generateOgImageResponse(input)
17+
if ('kind' in result) {
18+
console.warn(`[skip] ${input.libraryId}: ${result.kind}`)
19+
return
20+
}
21+
const buf = Buffer.from(await result.arrayBuffer())
22+
writeFileSync(outPath, buf)
23+
console.log(`[ok] ${outPath}`)
24+
}
25+
26+
async function main() {
27+
mkdirSync(OUT_DIR, { recursive: true })
28+
29+
for (const lib of libraries) {
30+
if (!lib.to) continue // skip entries without a landing page (react-charts, create-tsrouter-app)
31+
32+
await renderToFile(resolve(OUT_DIR, `${lib.id}.png`), { libraryId: lib.id })
33+
await renderToFile(resolve(OUT_DIR, `${lib.id}-docs.png`), {
34+
libraryId: lib.id,
35+
title: 'Overview',
36+
description: `${lib.tagline} Guides, API reference and examples in one place.`,
37+
})
38+
}
39+
}
40+
41+
main().catch((err) => {
42+
console.error(err)
43+
process.exit(1)
44+
})

src/libraries/ai.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ const textStyles = `text-pink-600 dark:text-pink-500`
99
export const aiProject = {
1010
...ai,
1111
description: `A powerful, open-source AI SDK with a unified interface across multiple providers. No vendor lock-in, no proprietary formats, just clean TypeScript and honest open source.`,
12-
ogImage: 'https://github.com/tanstack/ai/raw/main/media/repo-header.png',
1312
latestBranch: 'main',
1413
bgRadial: 'from-pink-500 via-pink-700/50 to-transparent',
1514
textColor: `text-pink-700`,

src/libraries/config.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ const textStyles = 'text-black dark:text-gray-100'
88
export const configProject = {
99
...config,
1010
description: `Opinionated tooling to lint, build, test, version, and publish JS/TS packages — minimal config, consistent results.`,
11-
ogImage: 'https://github.com/tanstack/config/raw/main/media/repo-header.png',
1211
latestBranch: 'main',
1312
featureHighlights: [
1413
{

src/libraries/db.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ const textStyles = `text-orange-600 dark:text-orange-500`
88
export const dbProject = {
99
...db,
1010
description: `TanStack DB gives you a reactive, client-first store for your API data with collections, live queries and optimistic mutations that keep your UI reactive, consistent and blazing fast 🔥`,
11-
ogImage: 'https://github.com/tanstack/db/raw/main/media/repo-header.png',
1211
latestBranch: 'main',
1312
bgRadial: 'from-orange-500 via-orange-700/50 to-transparent',
1413
textColor: `text-orange-700`,

0 commit comments

Comments
 (0)