Skip to content

feat(og): dynamic package-themed OG images#835

Open
AlemTuzlak wants to merge 19 commits intoTanStack:mainfrom
AlemTuzlak:feat/dynamic-og-images
Open

feat(og): dynamic package-themed OG images#835
AlemTuzlak wants to merge 19 commits intoTanStack:mainfrom
AlemTuzlak:feat/dynamic-og-images

Conversation

@AlemTuzlak
Copy link
Copy Markdown
Contributor

@AlemTuzlak AlemTuzlak commented Apr 17, 2026

Summary

  • Replaces the static og:image URL on every TanStack library landing and docs page with a package-themed PNG rendered per-request at /api/og/:library.png.
  • Rendered via Satori (JSX → SVG) + @resvg/resvg-js (SVG → PNG). Bundled Inter TTFs, inline splash-dark island asset, library-accent color map keyed off each library's existing textStyle.
  • Docs pages pass the page's title and description as query params — landing pages fall back to the library's name + tagline.
  • CDN cache: 1h browser, 24h edge, 7d stale-while-revalidate.
  • Deletes the hard-coded ogImage GitHub-header URLs from every library definition.

Visual

Each OG image:

  • Island (splash-dark.png) on the left, vertically centered.
  • Right column stacks: TanStack (white) / [Package] (library color) / doc title if present (library color) / description (library color, smaller).
  • Background: dark base + subtle green anchor glow from the left + subtle library-colored glow from the bottom-right.

Run pnpm exec tsx scripts/og-preview.ts locally to render 34 samples into .og-preview/ (17 libraries × landing + docs variants).

Notable implementation choices

  • `src/server/og/` module split: `colors.ts` (library → accent map), `assets.server.ts` (cached font + island loader), `template.tsx` (Satori JSX), `generate.server.ts` (composition + clamping).
  • Route file: `src/routes/api/og/$library[.png].ts` — TanStack Router escapes the literal `.png` in the param name.
  • `@resvg/resvg-js` added to `rscSsrExternals` and `optimizeDeps.exclude` in `vite.config.ts` (ships a native .node binary).
  • `netlify.toml` updated with `included_files` for the two TTFs and the island PNG so the Netlify Function bundle can read them at module scope.
  • Title + description are length-clamped (80 / 160 chars) to avoid unbounded cache keys.

Test plan

  • Netlify preview deploy renders /api/og/ai.png, /api/og/query.png, /api/og/devtools.png successfully (cold start reads font + island bytes from the bundle).
  • curl -sI https://<preview>/api/og/bogus-library.png returns HTTP 404 in production (dev-mode wraps 404 in HTML shell; production should pass through).
  • `` on a library landing and a docs page resolves to the new URL.
  • Paste a landing-page URL and a docs-page URL into https://www.opengraph.xyz and confirm the rendered preview.
  • Existing smoke tests still pass (`pnpm run test:smoke` against local dev).

Out of scope

  • Blog posts keep their custom OG images — no generator fallback for `/blog/*`.
  • Homepage, /explore, /ethos, /brand-guide, /shop still use the existing static `og.png`.

Summary by CodeRabbit

  • New Features

    • Dynamic Open Graph image generation with a new image endpoint, preview generation tool, and pages automatically including generated OG images (optional title/description).
  • Chores

    • Added runtime packages and adjusted build/deploy configuration to support image generation; updated ignore rules and deployment include list.
  • Tests

    • Added smoke tests validating OG image responses (status, content-type, non-empty body).

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 17, 2026

👷 Deploy request for tanstack pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit 240c53f

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 17, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: faad8659-60f8-448b-b11f-312c09e0f7e4

📥 Commits

Reviewing files that changed from the base of the PR and between 7176aa1 and 240c53f.

📒 Files selected for processing (1)
  • src/server/og/template.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/server/og/template.tsx

📝 Walkthrough

Walkthrough

Adds dynamic Open Graph image generation: removes static ogImage fields, introduces server-side PNG generation (satori + resvg), a new /api/og/$library.png route, asset/color helpers, preview/test scripts, and updates route SEO to use the dynamic OG URLs.

Changes

Cohort / File(s) Summary
Build Configuration
/.gitignore, netlify.toml, package.json, vite.config.ts
Ignored .og-preview/; included font/image assets in Netlify functions; added satori and @resvg/resvg-js deps; mark @resvg/resvg-js as SSR external and excluded from optimizeDeps.
Library Metadata & Types
src/libraries/.../*.tsx, src/libraries/libraries.ts, src/libraries/types.ts
Removed ogImage property from many library project objects and from LibrarySlim type, deleting hard-coded OG URLs across library metadata.
OG Asset Loader
src/server/og/assets.server.ts
New lazy-loading/caching of Inter font files and splash image; exports loadOgAssets() returning font buffers and a data URL.
OG Color Map
src/server/og/colors.ts
New per-library accent color mapping and exported getAccentColor() with a fallback.
OG Generation Engine
src/server/og/generate.server.ts, src/server/og/template.tsx
New generateOgPng() pipeline: find library, clamp text, build React render tree (buildOgTree), render SVG via satori, convert to PNG with Resvg, return Buffer or a library-not-found error.
API Route & Routing
src/routes/api/og/$library[.png].ts, src/routeTree.gen.ts
Added file route /api/og/$library.png that returns generated PNG with caching headers; registered route in generated route tree.
OG URL Helper
src/utils/og.ts
Added ogImageUrl() to construct canonical absolute URLs for the OG endpoint with optional title/description query params.
Route Head Metadata Updates
src/routes/-library-landing.tsx, src/routes/$libraryId/route.tsx, src/routes/$libraryId/$version.index.tsx, src/routes/$libraryId/$version.docs.*.tsx
Replaced uses of library.ogImage with ogImageUrl(...) (including docs, framework, examples, community pages) to populate SEO image meta.
Preview & Tests
scripts/og-preview.ts, tests/smoke.ts
Added scripts/og-preview.ts to pre-generate PNG previews into .og-preview; extended smoke tests to request and validate OG image endpoints.
Misc small edits
src/libraries/*Project.tsx (many files)
Multiple small edits removing ogImage property from individual project objects (grouped above).

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Poem

🐰 I hopped through fonts and glowing light,

Built tiny islands for each sight,
From React to SVG then PNG,
Each library sparkles — just you see! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(og): dynamic package-themed OG images' accurately and clearly summarizes the main change: introducing dynamic, per-package Open Graph images to replace static hard-coded URLs.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (9)
src/server/og/colors.ts (1)

6-24: Optional: tighten typing to Partial<Record<LibraryId, string>>.

Using Record<string, string> forgoes the compile-time check that keys correspond to real LibraryIds — 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: hexToRgba silently produces NaN for non-6-digit hex.

All current callers pass controlled 6-digit values from colors.ts, so this is defensive only, but a stray #fff or invalid string would produce rgba(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 for ogImageUrl. Consider hoisting them into local consts 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.png and asserts response.status === 404 would 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/failed are 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 clamping title/description client-side to match server limits.

generateOgPng clamps to 80/160 chars server-side, but callers pass raw loader values into ogImageUrl, 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 silently continues. 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 exactly max chars in the common case, but trimEnd() 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 before max. 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: Unnecessary new Uint8Array(result) copy.

result is a Node Buffer, which is already a Uint8Array subclass and a valid BodyInit. Wrapping it in new 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0d0fdb0 and 336e8fa.

⛔ Files ignored due to path filters (3)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • public/fonts/Inter-ExtraBold.ttf is excluded by !**/*.ttf
  • public/fonts/Inter-Regular.ttf is excluded by !**/*.ttf
📒 Files selected for processing (35)
  • .gitignore
  • netlify.toml
  • package.json
  • scripts/og-preview.ts
  • src/libraries/ai.tsx
  • src/libraries/config.tsx
  • src/libraries/db.tsx
  • src/libraries/devtools.tsx
  • src/libraries/form.tsx
  • src/libraries/hotkeys.tsx
  • src/libraries/libraries.ts
  • src/libraries/pacer.tsx
  • src/libraries/query.tsx
  • src/libraries/ranger.tsx
  • src/libraries/router.tsx
  • src/libraries/store.tsx
  • src/libraries/table.tsx
  • src/libraries/types.ts
  • src/libraries/virtual.tsx
  • src/routeTree.gen.ts
  • src/routes/$libraryId/$version.docs.$.tsx
  • src/routes/$libraryId/$version.docs.community-resources.tsx
  • src/routes/$libraryId/$version.docs.framework.$framework.$.tsx
  • src/routes/$libraryId/$version.docs.framework.$framework.examples.$.tsx
  • src/routes/$libraryId/$version.index.tsx
  • src/routes/$libraryId/route.tsx
  • src/routes/-library-landing.tsx
  • src/routes/api/og/$library[.png].ts
  • src/server/og/assets.server.ts
  • src/server/og/colors.ts
  • src/server/og/generate.server.ts
  • src/server/og/template.tsx
  • src/utils/og.ts
  • tests/smoke.ts
  • vite.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

Comment on lines +29 to +40
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,
},
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +52 to +65
fonts: [
{
name: 'Inter',
data: assets.interRegular,
weight: 700,
style: 'normal',
},
{
name: 'Inter',
data: assets.interExtraBold,
weight: 800,
style: 'normal',
},
],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 tsx

Repository: 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 60

Repository: 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 ts

Repository: TanStack/tanstack.com

Length of output: 813


🏁 Script executed:

#!/bin/bash
# Look for font files in the repository
find . -type f -name "*Inter*" | head -20

Repository: 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:

  1. Change Inter-Regular registration from weight: 700 to weight: 400
  2. Update the description's fontWeight: 700 to fontWeight: 400 in 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).

Comment thread src/server/og/template.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant