Skip to content

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

Merged
LadyBluenotes merged 25 commits into
TanStack:mainfrom
AlemTuzlak:feat/dynamic-og-images
May 6, 2026
Merged

feat(og): dynamic package-themed OG images#835
LadyBluenotes merged 25 commits into
TanStack: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 an endpoint and a preview generator; pages now include generated OG images (optional title/description).
  • Chores

    • Added runtime image-generation packages, adjusted build/deploy inclusions, and updated ignore rules.
  • Tests

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

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 17, 2026

Deploy Preview for tanstack ready!

Name Link
🔨 Latest commit 521dc7e
🔍 Latest deploy log https://app.netlify.com/projects/tanstack/deploys/69fb3d297d07bf00087c0d7a
😎 Deploy Preview https://deploy-preview-835--tanstack.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 37 (🔴 down 6 from production)
Accessibility: 90 (no change from production)
Best Practices: 83 (🔴 down 9 from production)
SEO: 97 (no change from production)
PWA: 70 (no change from production)
View the detailed breakdown and full score reports
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Removes static ogImage metadata and adds dynamic Open Graph PNG generation: new server OG generator, assets/colors/template, API route /api/og/$library.png, URL helper, preview script, smoke-test coverage, build/config updates, and type/metadata edits removing ogImage fields.

Changes

Dynamic OG image generation

Layer / File(s) Summary
Type / Metadata
src/libraries/types.ts, src/libraries/.../*.tsx, src/libraries/libraries.ts
Removed optional ogImage?: string from LibrarySlim and deleted ogImage properties from many library project objects.
Build / Config
.gitignore, netlify.toml, package.json, vite.config.ts
Ignored .og-preview/; added included_files for Netlify functions (font/image assets); added Takumi packages to dependencies and to SSR externals / optimizeDeps excludes.
Asset Loader
src/server/og/assets.server.ts
New lazy-loading and module-level caching for Inter font files and splash image; exports loadOgAssets() returning Buffers.
Accent Colors
src/server/og/colors.ts
New per-library accent color map and exported getAccentColor(libraryId) with fallback.
Template
src/server/og/template.tsx
New buildOgTree(props) React template that composes the 1200×630 OG render tree (title split, layered background, island image, pitch/doc blocks).
Generator
src/server/og/generate.server.ts
New generateOgImageResponse(input, init?) that finds a library, clamps text, loads assets, computes color, builds the render tree, and returns an ImageResponse or OgLibraryNotFoundError.
API Route & Routing
src/routes/api/og/$library[.png].ts, src/routeTree.gen.ts
New file route /api/og/$library.png that normalizes params, forwards optional title/description, calls generator, returns PNG with cache headers, and registers the route in generated route maps.
URL Helper
src/utils/og.ts
New ogImageUrl(libraryId, { title?, description? }) builds canonical absolute /api/og/... .png URL with SSR-aware origin handling.
Route SEO Wiring
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 stored library.ogImage with ogImageUrl(...), adding image to seo(...) metadata across listing, docs, framework, examples, and community pages.
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 OG endpoints, assert image/png and non-zero body.
Minor edits
src/libraries/* (many files)
Removed former ogImage fields from individual project files (grouped in metadata layer above).

Sequence Diagram

sequenceDiagram
    participant Client as Client (Browser)
    participant Route as API Route\n/api/og/$library.png
    participant Generator as generateOgImageResponse()
    participant Assets as loadOgAssets()
    participant Colors as getAccentColor()
    participant Template as buildOgTree()
    participant Takumi as Takumi ImageResponse

    Client->>Route: GET /api/og/query.png?title=Overview
    activate Route
    Route->>Generator: generateOgImageResponse({libraryId:'query', title:'Overview'})
    activate Generator
    Generator->>Generator: findLibrary('query')
    Generator->>Assets: loadOgAssets()
    activate Assets
    Assets-->>Generator: fonts + island Buffer
    deactivate Assets
    Generator->>Colors: getAccentColor('query')
    Colors-->>Generator: accentColor
    Generator->>Template: buildOgTree({libraryName, accentColor, ...})
    activate Template
    Template-->>Generator: ReactElement
    deactivate Template
    Generator->>Takumi: ImageResponse(ReactElement, { fonts, size:1200x630 })
    activate Takumi
    Takumi-->>Generator: PNG response
    deactivate Takumi
    Generator-->>Route: PNG response
    deactivate Generator
    Route->>Client: PNG + Content-Type/Cache-Control
    deactivate Route
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Poem

🐰 I hopped through fonts and glowing light,

I painted islands, trimmed titles right,
From assets fetched to images spun,
Each library shines like morning sun. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: introducing dynamic, per-package Open Graph images generated server-side and served via a new API endpoint, replacing static hard-coded URLs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 thread src/routes/api/og/$library[.png].ts Outdated
Comment thread src/server/og/generate.server.ts
Comment thread src/server/og/template.tsx
AlemTuzlak and others added 9 commits April 17, 2026 15:22
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.
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.
- 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.
@LadyBluenotes LadyBluenotes merged commit e4eff6b into TanStack:main May 6, 2026
8 checks passed
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.

2 participants