feat(og): dynamic package-themed OG images#835
feat(og): dynamic package-themed OG images#835AlemTuzlak wants to merge 19 commits intoTanStack:mainfrom
Conversation
…in function bundle
👷 Deploy request for tanstack pending review.Visit the deploys page to approve it
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds dynamic Open Graph image generation: removes static Changes
Sequence DiagramsequenceDiagram
participant Client as Client (Browser)
participant Route as API Route<br/>/api/og/$library.png
participant Generate as generateOgPng()
participant Assets as loadOgAssets()
participant Colors as getAccentColor()
participant Template as buildOgTree()
participant Satori as satori
participant Resvg as Resvg
Client->>Route: GET /api/og/query.png?title=Overview
activate Route
Route->>Generate: generateOgPng({ libraryId: 'query', title: 'Overview' })
activate Generate
Generate->>Generate: findLibrary('query')
Generate->>Assets: loadOgAssets()
activate Assets
Assets-->>Generate: fonts + islandDataUrl
deactivate Assets
Generate->>Colors: getAccentColor('query')
Colors-->>Generate: accentColor
Generate->>Generate: clampText(title/description)
Generate->>Template: buildOgTree({ libraryName, accentColor, ... })
activate Template
Template-->>Generate: ReactElement
deactivate Template
Generate->>Satori: satori(ReactElement, { width:1200, height:630, fonts })
activate Satori
Satori-->>Generate: SVG
deactivate Satori
Generate->>Resvg: Resvg(svg).render()
activate Resvg
Resvg-->>Generate: PNG buffer
deactivate Resvg
Generate-->>Route: PNG buffer
deactivate Generate
Route->>Client: PNG image + Content-Type/Cache-Control headers
deactivate Route
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~55 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (9)
src/server/og/colors.ts (1)
6-24: Optional: tighten typing toPartial<Record<LibraryId, string>>.Using
Record<string, string>forgoes the compile-time check that keys correspond to realLibraryIds — so a typo or a renamed/removed library id silently falls back to the default without a TS error.Proposed refactor
-const LIBRARY_ACCENT_COLORS: Record<string, string> = { +const LIBRARY_ACCENT_COLORS: Partial<Record<LibraryId, string>> = {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/server/og/colors.ts` around lines 6 - 24, Replace the overly-broad type of LIBRARY_ACCENT_COLORS with a type keyed by LibraryId to catch typos/removed ids: change its declaration from Record<string,string> to Partial<Record<LibraryId,string>> (or Readonly<...> if immutability is desired), and import or reference the existing LibraryId type so the compiler enforces allowed keys; keep existing keys/values unchanged and ensure any missing keys fall back to current runtime defaults.src/server/og/template.tsx (1)
117-123:hexToRgbasilently producesNaNfor non-6-digit hex.All current callers pass controlled 6-digit values from
colors.ts, so this is defensive only, but a stray#fffor invalid string would producergba(NaN, NaN, NaN, ...)and break the background gradient without warning. Consider either validating and falling back to the default, or normalizing 3-digit hex.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/server/og/template.tsx` around lines 117 - 123, The hexToRgba function can produce NaN for non-6-digit inputs; update hexToRgba to validate and normalize input: strip '#', if length === 3 expand shorthand (e.g. 'abc' -> 'aabbcc'), if length === 6 parse as before, otherwise return a safe fallback (e.g. defaultColor or a hardcoded rgba black/transparent) or throw/log an error; reference the hexToRgba function to locate the fix and ensure callers keep the same signature (hexToRgba(hex: string, alpha: number): string).src/routes/$libraryId/$version.docs.framework.$framework.examples.$.tsx (1)
131-134: Minor: duplicated title/description string construction.The title and description strings are built identically for
seo(...)above (lines 125–130) and again forogImageUrl. Consider hoisting them into localconsts to avoid drift if one side is later edited.♻️ Suggested refactor
head: ({ params }) => { const library = getLibrary(params.libraryId) + const exampleName = slugToTitle(params._splat || '') + const frameworkName = capitalize(params.framework) + const ogTitle = `${frameworkName} ${library.name} ${exampleName} Example` + const ogDescription = `An example showing how to implement ${exampleName} in ${frameworkName} using ${library.name}.` return { meta: seo({ - title: `${capitalize(params.framework)} ${library.name} ${slugToTitle( - params._splat || '', - )} Example | ${library.name} Docs`, - description: `An example showing how to implement ${slugToTitle( - params._splat || '', - )} in ${capitalize(params.framework)} using ${library.name}.`, - image: ogImageUrl(library.id, { - title: `${capitalize(params.framework)} ${library.name} ${slugToTitle(params._splat || '')} Example`, - description: `An example showing how to implement ${slugToTitle(params._splat || '')} in ${capitalize(params.framework)} using ${library.name}.`, - }), + title: `${ogTitle} | ${library.name} Docs`, + description: ogDescription, + image: ogImageUrl(library.id, { title: ogTitle, description: ogDescription }), noindex: library.visible === false, }), } },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/`$libraryId/$version.docs.framework.$framework.examples.$.tsx around lines 131 - 134, The title/description are duplicated between the seo(...) call and the ogImageUrl(...) call; hoist them into local constants (e.g., const title = ..., const description = ...) computed once using params, capitalize, slugToTitle and library.name, then replace the inline templates passed to seo(...) and ogImageUrl(...) with those constants to ensure a single source of truth and avoid drift (update occurrences around the seo and ogImageUrl calls).tests/smoke.ts (2)
60-66: Consider adding a 404 case to the OG smoke tests.The PR test plan calls out "404 for unknown libraries in prod". Adding one case that hits e.g.
/api/og/not-a-library.pngand assertsresponse.status === 404would guard the error branch in the route handler against regressions (currently only happy paths are tested).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/smoke.ts` around lines 60 - 66, Add a 404 smoke test to the OG tests by extending the ogTests array (ImageTestCase) with a case that targets a non-existent library path, e.g. { name: 'OG image · unknown library (404)', path: '/api/og/not-a-library.png' }, and update the test runner assertion for that case to expect response.status === 404 (instead of image content checks used for happy paths) so the route handler's error branch is exercised.
170-171: Minor: shared pass/fail counters make the intermediate totals misleading.
passed/failedare not reset between the HTML block and the OG block, so the "X passed, Y failed" printed at line 224 is actually a cumulative total but reads as if it's just the OG results. Consider separate counters per block (or only printing a single final summary) to avoid confusion when a failure occurs.♻️ Suggested change
- console.log(`\n${passed} passed, ${failed} failed\n`) + console.log(`\nHTML: ${passed} passed, ${failed} failed\n`) ... - console.log(`\n${passed} passed, ${failed} failed\n`) + console.log(`\nTotal: ${passed} passed, ${failed} failed\n`)Also applies to: 184-184, 224-224
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/smoke.ts` around lines 170 - 171, The shared counters passed and failed are reused for both the HTML block and the OG block, making the intermediate "X passed, Y failed" message misleading; update the test to use separate counters (e.g., passedHtml/failedHtml and passedOg/failedOg) or reset passed/failed before starting the OG block so the printed totals reflect only that block's results; locate and modify the variables passed and failed and the summary prints that reference them near the HTML and OG test sections to ensure each block reports its own counts.src/utils/og.ts (1)
14-25: Consider clampingtitle/descriptionclient-side to match server limits.
generateOgPngclamps to 80/160 chars server-side, but callers pass raw loader values intoogImageUrl, so the URL embedded in<meta property="og:image">can be arbitrarily long. Two small downsides:
- Inflates the rendered HTML head (every docs page carries this meta tag).
- Every unique pre-clamp variant produces a distinct cache key at the CDN even though many render to the same PNG, undermining the intent of the server-side clamp "to limit cache keys" mentioned in the PR description.
Clamping here (to the same 80/160 constants exported from
generate.server.ts, or duplicating them in a shared module) keeps URLs bounded and makes CDN cache keys stable.♻️ Suggested change
+const MAX_TITLE = 80 +const MAX_DESCRIPTION = 160 + +function clamp(text: string, max: number): string { + const t = text.trim() + if (t.length <= max) return t + return t.slice(0, max - 1).trimEnd() + '…' +} + export function ogImageUrl( libraryId: LibraryId, options: OgImageOptions = {}, ): string { const params = new URLSearchParams() - if (options.title) params.set('title', options.title) - if (options.description) params.set('description', options.description) + if (options.title) params.set('title', clamp(options.title, MAX_TITLE)) + if (options.description) + params.set('description', clamp(options.description, MAX_DESCRIPTION))Ideally the
MAX_*constants live in one place and are imported by both sides.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/og.ts` around lines 14 - 25, The ogImageUrl helper currently forwards raw options which can produce arbitrarily long query strings; clamp the title and description client-side to the same limits used by the server-side generator (e.g., the MAX_* limits from generateOgPng in generate.server.ts) before building the URL so the meta og:image query is bounded and CDN cache keys remain stable. Update ogImageUrl to import or duplicate the MAX_TITLE/MAX_DESCRIPTION constants (or call a shared clamp utility) and truncate options.title and options.description to those lengths prior to creating URLSearchParams; reference the ogImageUrl function and the generateOgPng/MAX constants in generate.server.ts to keep both sides consistent.scripts/og-preview.ts (1)
29-37: Minor: inconsistent error reporting between variants.The landing variant logs
[skip] ${lib.id}: ${kind}on error, but the docs variant silentlycontinues. For a dev-facing preview script, logging both helps diagnose misconfiguration.♻️ Suggested change
- if ('kind' in (docs as Record<string, unknown>)) continue + if ('kind' in (docs as Record<string, unknown>)) { + console.warn(`[skip docs] ${lib.id}: ${(docs as any).kind}`) + continue + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/og-preview.ts` around lines 29 - 37, The docs branch silently skips when generateOgPng returns an error-like object; update the docs handling to log the same diagnostic as the landing variant instead of just continuing: after calling generateOgPng (the docs constant) check for the error shape ('kind' in docs as Record<string,unknown>) and log a message like `[skip] ${lib.id}: ${kind}` (or include the returned kind/value) before continuing, referencing generateOgPng, docs, docsPath and lib.id so the script reports failures consistently.src/server/og/generate.server.ts (1)
74-77: Clamp boundary nit.
slice(0, max - 1) + '…'yields exactlymaxchars in the common case, buttrimEnd()can shorten it further so the final length is ≤max. This is fine for cache-key bounding, just note that the truncation point can land mid-word (e.g., "quick bro…"). If you prefer word-boundary truncation, you could break on the last space beforemax. Optional.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/server/og/generate.server.ts` around lines 74 - 77, clampText currently slices to max-1 and appends an ellipsis which can cut mid-word and, because of trimEnd(), may produce a string shorter than max; update clampText to prefer truncation at the last whitespace before the max boundary: in function clampText find the initial slice (text.slice(0, max - 1)), trimEnd it, then if the original text length > max search for the last space in that slice and, if found, slice to that space instead before appending '…' so truncation lands on a word boundary while still ensuring the result is ≤ max characters.src/routes/api/og/$library[.png].ts (1)
35-35: Unnecessarynew Uint8Array(result)copy.
resultis a NodeBuffer, which is already aUint8Arraysubclass and a validBodyInit. Wrapping it innew Uint8Array(result)allocates a copy on every request on the hot path.♻️ Proposed simplification
- return new Response(new Uint8Array(result), { + return new Response(result, {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/api/og/`$library[.png].ts at line 35, The Response creation unnecessarily copies the Node Buffer by wrapping it in new Uint8Array(result); change the Response construction to pass the existing Buffer/Uint8Array directly (i.e., use result as the BodyInit) so no heap-copy occurs, ensuring the variable result (from the image generation flow) is used directly when constructing the Response.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/routes/api/og/`$library[.png].ts:
- Around line 29-40: Wrap the call to generateOgPng in a try/catch inside the
route handler so any exceptions from satori/resvg are caught; on catch return a
500 Response with headers that prevent caching (e.g., Cache-Control: no-store or
a very short TTL) instead of letting the error propagate, and preserve the
existing successful response path that uses CACHE_HEADERS; reference
generateOgPng and CACHE_HEADERS so you locate the call and replace it with a
try/catch that returns a non-cacheable 500 on error.
In `@src/server/og/generate.server.ts`:
- Around line 52-65: The Inter font weight is mis-registered and the template
uses a mismatched weight: in the fonts array in generate.server.ts change the
registration for Inter-Regular from weight 700 to weight 400, and then update
the description text style in src/server/og/template.tsx (the element using
fontWeight: 700 around line ~96) to use fontWeight: 400 (or adjust both files to
the intended consistent weight if 700 was desired); ensure the two places (fonts
registration in generate.server.ts and the description fontWeight in
template.tsx) match so Satori finds the correct weight (references: fonts array
in generate.server.ts and the description node in template.tsx).
In `@src/server/og/template.tsx`:
- Around line 47-56: Add an empty alt attribute to the decorative island image
so the JSX linter passes: update the <img> that uses props.islandDataUrl (with
ISLAND_SIZE, WIDTH, HEIGHT) in template.tsx to include alt="" (empty string) to
mark it as decorative; no other behavior changes are needed since Satori ignores
alt text.
---
Nitpick comments:
In `@scripts/og-preview.ts`:
- Around line 29-37: The docs branch silently skips when generateOgPng returns
an error-like object; update the docs handling to log the same diagnostic as the
landing variant instead of just continuing: after calling generateOgPng (the
docs constant) check for the error shape ('kind' in docs as
Record<string,unknown>) and log a message like `[skip] ${lib.id}: ${kind}` (or
include the returned kind/value) before continuing, referencing generateOgPng,
docs, docsPath and lib.id so the script reports failures consistently.
In `@src/routes/`$libraryId/$version.docs.framework.$framework.examples.$.tsx:
- Around line 131-134: The title/description are duplicated between the seo(...)
call and the ogImageUrl(...) call; hoist them into local constants (e.g., const
title = ..., const description = ...) computed once using params, capitalize,
slugToTitle and library.name, then replace the inline templates passed to
seo(...) and ogImageUrl(...) with those constants to ensure a single source of
truth and avoid drift (update occurrences around the seo and ogImageUrl calls).
In `@src/routes/api/og/`$library[.png].ts:
- Line 35: The Response creation unnecessarily copies the Node Buffer by
wrapping it in new Uint8Array(result); change the Response construction to pass
the existing Buffer/Uint8Array directly (i.e., use result as the BodyInit) so no
heap-copy occurs, ensuring the variable result (from the image generation flow)
is used directly when constructing the Response.
In `@src/server/og/colors.ts`:
- Around line 6-24: Replace the overly-broad type of LIBRARY_ACCENT_COLORS with
a type keyed by LibraryId to catch typos/removed ids: change its declaration
from Record<string,string> to Partial<Record<LibraryId,string>> (or
Readonly<...> if immutability is desired), and import or reference the existing
LibraryId type so the compiler enforces allowed keys; keep existing keys/values
unchanged and ensure any missing keys fall back to current runtime defaults.
In `@src/server/og/generate.server.ts`:
- Around line 74-77: clampText currently slices to max-1 and appends an ellipsis
which can cut mid-word and, because of trimEnd(), may produce a string shorter
than max; update clampText to prefer truncation at the last whitespace before
the max boundary: in function clampText find the initial slice (text.slice(0,
max - 1)), trimEnd it, then if the original text length > max search for the
last space in that slice and, if found, slice to that space instead before
appending '…' so truncation lands on a word boundary while still ensuring the
result is ≤ max characters.
In `@src/server/og/template.tsx`:
- Around line 117-123: The hexToRgba function can produce NaN for non-6-digit
inputs; update hexToRgba to validate and normalize input: strip '#', if length
=== 3 expand shorthand (e.g. 'abc' -> 'aabbcc'), if length === 6 parse as
before, otherwise return a safe fallback (e.g. defaultColor or a hardcoded rgba
black/transparent) or throw/log an error; reference the hexToRgba function to
locate the fix and ensure callers keep the same signature (hexToRgba(hex:
string, alpha: number): string).
In `@src/utils/og.ts`:
- Around line 14-25: The ogImageUrl helper currently forwards raw options which
can produce arbitrarily long query strings; clamp the title and description
client-side to the same limits used by the server-side generator (e.g., the
MAX_* limits from generateOgPng in generate.server.ts) before building the URL
so the meta og:image query is bounded and CDN cache keys remain stable. Update
ogImageUrl to import or duplicate the MAX_TITLE/MAX_DESCRIPTION constants (or
call a shared clamp utility) and truncate options.title and options.description
to those lengths prior to creating URLSearchParams; reference the ogImageUrl
function and the generateOgPng/MAX constants in generate.server.ts to keep both
sides consistent.
In `@tests/smoke.ts`:
- Around line 60-66: Add a 404 smoke test to the OG tests by extending the
ogTests array (ImageTestCase) with a case that targets a non-existent library
path, e.g. { name: 'OG image · unknown library (404)', path:
'/api/og/not-a-library.png' }, and update the test runner assertion for that
case to expect response.status === 404 (instead of image content checks used for
happy paths) so the route handler's error branch is exercised.
- Around line 170-171: The shared counters passed and failed are reused for both
the HTML block and the OG block, making the intermediate "X passed, Y failed"
message misleading; update the test to use separate counters (e.g.,
passedHtml/failedHtml and passedOg/failedOg) or reset passed/failed before
starting the OG block so the printed totals reflect only that block's results;
locate and modify the variables passed and failed and the summary prints that
reference them near the HTML and OG test sections to ensure each block reports
its own counts.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 10f12b71-be03-4d11-9abc-04ae073406a3
⛔ Files ignored due to path filters (3)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlpublic/fonts/Inter-ExtraBold.ttfis excluded by!**/*.ttfpublic/fonts/Inter-Regular.ttfis excluded by!**/*.ttf
📒 Files selected for processing (35)
.gitignorenetlify.tomlpackage.jsonscripts/og-preview.tssrc/libraries/ai.tsxsrc/libraries/config.tsxsrc/libraries/db.tsxsrc/libraries/devtools.tsxsrc/libraries/form.tsxsrc/libraries/hotkeys.tsxsrc/libraries/libraries.tssrc/libraries/pacer.tsxsrc/libraries/query.tsxsrc/libraries/ranger.tsxsrc/libraries/router.tsxsrc/libraries/store.tsxsrc/libraries/table.tsxsrc/libraries/types.tssrc/libraries/virtual.tsxsrc/routeTree.gen.tssrc/routes/$libraryId/$version.docs.$.tsxsrc/routes/$libraryId/$version.docs.community-resources.tsxsrc/routes/$libraryId/$version.docs.framework.$framework.$.tsxsrc/routes/$libraryId/$version.docs.framework.$framework.examples.$.tsxsrc/routes/$libraryId/$version.index.tsxsrc/routes/$libraryId/route.tsxsrc/routes/-library-landing.tsxsrc/routes/api/og/$library[.png].tssrc/server/og/assets.server.tssrc/server/og/colors.tssrc/server/og/generate.server.tssrc/server/og/template.tsxsrc/utils/og.tstests/smoke.tsvite.config.ts
💤 Files with no reviewable changes (15)
- src/libraries/form.tsx
- src/libraries/table.tsx
- src/libraries/router.tsx
- src/libraries/db.tsx
- src/libraries/hotkeys.tsx
- src/libraries/virtual.tsx
- src/libraries/ranger.tsx
- src/libraries/query.tsx
- src/libraries/store.tsx
- src/libraries/types.ts
- src/libraries/devtools.tsx
- src/libraries/config.tsx
- src/libraries/ai.tsx
- src/libraries/pacer.tsx
- src/libraries/libraries.ts
| const result = await generateOgPng({ libraryId, title, description }) | ||
|
|
||
| if ('kind' in result) { | ||
| return new Response(`Unknown library: ${libraryId}`, { status: 404 }) | ||
| } | ||
|
|
||
| return new Response(new Uint8Array(result), { | ||
| headers: { | ||
| 'Content-Type': 'image/png', | ||
| ...CACHE_HEADERS, | ||
| }, | ||
| }) |
There was a problem hiding this comment.
Consider wrapping generator call in try/catch.
generateOgPng can throw from satori/resvg on unexpected input (malformed characters, font glyph issues, etc.), resulting in an uncaught 500 that may still be cached by intermediaries if headers aren't set. Consider catching and returning a 500 with Cache-Control: no-store (or a short TTL) so transient failures don't poison the CDN. The happy-path cache headers are only applied on success, which is good — but an explicit error branch would harden this public endpoint.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/routes/api/og/`$library[.png].ts around lines 29 - 40, Wrap the call to
generateOgPng in a try/catch inside the route handler so any exceptions from
satori/resvg are caught; on catch return a 500 Response with headers that
prevent caching (e.g., Cache-Control: no-store or a very short TTL) instead of
letting the error propagate, and preserve the existing successful response path
that uses CACHE_HEADERS; reference generateOgPng and CACHE_HEADERS so you locate
the call and replace it with a try/catch that returns a non-cacheable 500 on
error.
| fonts: [ | ||
| { | ||
| name: 'Inter', | ||
| data: assets.interRegular, | ||
| weight: 700, | ||
| style: 'normal', | ||
| }, | ||
| { | ||
| name: 'Inter', | ||
| data: assets.interExtraBold, | ||
| weight: 800, | ||
| style: 'normal', | ||
| }, | ||
| ], |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm which fontWeight values the template uses
fd -e tsx -e ts template src/server/og --exec cat {}Repository: TanStack/tanstack.com
Length of output: 3471
🏁 Script executed:
#!/bin/bash
# Find where the font assets are defined
rg -l "interRegular|interExtraBold" --type ts --type tsxRepository: TanStack/tanstack.com
Length of output: 92
🏁 Script executed:
#!/bin/bash
# Check the generate.server.ts file to see the full context around fonts
head -n 100 src/server/og/generate.server.ts | tail -n 60Repository: TanStack/tanstack.com
Length of output: 1541
🏁 Script executed:
#!/bin/bash
# Search for loadOgAssets or where interRegular and interExtraBold are defined
rg "loadOgAssets|interRegular|interExtraBold" --type tsRepository: TanStack/tanstack.com
Length of output: 813
🏁 Script executed:
#!/bin/bash
# Look for font files in the repository
find . -type f -name "*Inter*" | head -20Repository: TanStack/tanstack.com
Length of output: 281
Font weight metadata is incorrect, but the proposed fix is incomplete.
Inter-Regular.ttf should be registered with weight: 400, not 700. However, the template at src/server/og/template.tsx uses fontWeight: 700 for the description text (line 96), which would have no matching declared weight after the fix.
With only Inter-Regular.ttf (weight: 400) and Inter-ExtraBold.ttf (weight: 800) available, the complete fix requires:
- Change
Inter-Regularregistration fromweight: 700toweight: 400 - Update the description's
fontWeight: 700tofontWeight: 400in the template (or determine the correct intended weight and adjust accordingly)
Currently, fontWeight: 700 accidentally matches the Regular file because it's wrongly registered as 700. Without updating the template, Satori will fall back to the nearest weight, potentially rendering the description thinner or bolder than intended.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/server/og/generate.server.ts` around lines 52 - 65, The Inter font weight
is mis-registered and the template uses a mismatched weight: in the fonts array
in generate.server.ts change the registration for Inter-Regular from weight 700
to weight 400, and then update the description text style in
src/server/og/template.tsx (the element using fontWeight: 700 around line ~96)
to use fontWeight: 400 (or adjust both files to the intended consistent weight
if 700 was desired); ensure the two places (fonts registration in
generate.server.ts and the description fontWeight in template.tsx) match so
Satori finds the correct weight (references: fonts array in generate.server.ts
and the description node in template.tsx).
Summary
og:imageURL on every TanStack library landing and docs page with a package-themed PNG rendered per-request at/api/og/:library.png.textStyle.titleanddescriptionas query params — landing pages fall back to the library's name +tagline.ogImageGitHub-header URLs from every library definition.Visual
Each OG image:
TanStack(white) /[Package](library color) / doc title if present (library color) / description (library color, smaller).Run
pnpm exec tsx scripts/og-preview.tslocally to render 34 samples into.og-preview/(17 libraries × landing + docs variants).Notable implementation choices
Test plan
/api/og/ai.png,/api/og/query.png,/api/og/devtools.pngsuccessfully (cold start reads font + island bytes from the bundle).curl -sI https://<preview>/api/og/bogus-library.pngreturns HTTP 404 in production (dev-mode wraps 404 in HTML shell; production should pass through).Out of scope
Summary by CodeRabbit
New Features
Chores
Tests