Skip to content

feat: add isBot option to renderRouterToStream for streaming SSR bot detection#7661

Open
anonrig wants to merge 2 commits into
TanStack:mainfrom
anonrig:feat/ssr-isbot-option
Open

feat: add isBot option to renderRouterToStream for streaming SSR bot detection#7661
anonrig wants to merge 2 commits into
TanStack:mainfrom
anonrig:feat/ssr-isbot-option

Conversation

@anonrig

@anonrig anonrig commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Closes #7660

Summary

Streaming SSR waits for the entire document (React's allReady) for requests that isbot flags by their User-Agent, and streams the shell first for everyone else. It's a reasonable default, but it isn't configurable — and isbot classifies synthetic performance tools (Lighthouse, PageSpeed Insights, WebPageTest, Pingdom, …) as bots, so lab audits get measured on the buffered path instead of the streaming path real users actually receive.

This PR adds an isBot option to renderRouterToStream so the decision is made in the server request handler, defaulting to the current behavior (no breaking change):

import {
  createRequestHandler,
  renderRouterToStream,
  RouterServer,
} from '@tanstack/react-router/ssr/server'

createRequestHandler({ request, createRouter })(
  ({ request, router, responseHeaders }) =>
    renderRouterToStream({
      request,
      router,
      responseHeaders,
      children: <RouterServer router={router} />,
      // undefined (default): use the built-in isbot(User-Agent) check
      // boolean: force the bot (true) / non-bot (false) path
      // (request) => boolean: custom predicate
      isBot: false,
    }),
)

Why the request handler, not createRouter

An earlier revision put this on the router's ssr options. That's the wrong home: createRouter runs isomorphically, so an isBot predicate (and anything it imports) would be bundled into the client. renderRouterToStream lives in ssr/server and only ever runs on the server, so the option — and the user's predicate — stay server-side. It's also available in the Start path, whose defaultStreamHandler calls renderRouterToStream too.

Changes

  • @tanstack/react-router, @tanstack/solid-router, @tanstack/vue-router: renderRouterToStream accepts isBot?: boolean | ((request: Request) => boolean), falling back to isbot(request.headers.get('User-Agent')) when unset.
  • Tests: new cases in react-router/tests/renderRouterToStream.test.tsx covering the default, true, false, and predicate forms.
  • Changeset (minor) for the three adapter packages.

Verification

  • build — router-core + all three adapters
  • test:unit — react/solid/vue
  • test:types — ts5.5 → ts6.0 (all three adapters)
  • test:eslint — 0 errors

Summary by CodeRabbit

New Features

  • Added new isBot configuration option for streaming server-side rendering to customize how bot detection works
  • Override default automatic User-Agent detection with a boolean flag to force specific behavior globally
  • Alternatively, supply a custom request-scoped predicate function for flexible per-request bot determination
  • Existing default behavior remains fully backward compatible with all applications

Streaming SSR hardcoded the `isbot` User-Agent check that decides whether to wait for the full document (React `allReady`) before responding or to stream the shell first. Add an `ssr.isBot` router option (boolean | (request) => boolean) to override it; the default (undefined) keeps the current `isbot` behavior. Implemented across the react, solid, and vue adapters.
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4b2411b4-3b5b-4c8f-83a1-5e65db5b6c9a

📥 Commits

Reviewing files that changed from the base of the PR and between ce89206 and 0184ecc.

📒 Files selected for processing (5)
  • .changeset/ssr-isbot-option.md
  • packages/react-router/src/ssr/renderRouterToStream.tsx
  • packages/react-router/tests/renderRouterToStream.test.tsx
  • packages/solid-router/src/ssr/renderRouterToStream.tsx
  • packages/vue-router/src/ssr/renderRouterToStream.tsx
✅ Files skipped from review due to trivial changes (1)
  • .changeset/ssr-isbot-option.md

📝 Walkthrough

Walkthrough

Adds an optional isBot parameter (boolean | (request: Request) => boolean) to renderRouterToStream in react-router, solid-router, and vue-router. Each package computes a local isBotRequest value from the override or falls back to the existing isbot User-Agent check, replacing inline call sites. A changeset file and new react-router tests are included.

Changes

isBot configurable bot detection for SSR streaming

Layer / File(s) Summary
Changeset documentation and usage example
.changeset/ssr-isbot-option.md
Documents the three isBot forms (undefined, boolean, request predicate), the unchanged default behavior, and provides a usage example passing isBot: false from a server handler.
isBot option and isBotRequest computation
packages/react-router/src/ssr/renderRouterToStream.tsx, packages/solid-router/src/ssr/renderRouterToStream.tsx, packages/vue-router/src/ssr/renderRouterToStream.tsx
Extends each package's renderRouterToStream parameter type with optional isBot, computes isBotRequest from the override or falls back to the built-in isbot(User-Agent) check, and replaces all inline isbot() branch conditions with isBotRequest.
react-router tests for isBot option
packages/react-router/tests/renderRouterToStream.test.tsx
Adds a getStreamMode helper intercepting renderToPipeableStream callbacks plus five test cases: default UA detection for Googlebot and human UA, isBot: false overriding Googlebot detection, isBot: true overriding human UA, and a predicate function receiving the Request.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested labels

package: react-router

🐇 A little isBot flag, now you decide,
Who buffers the shell and who streams with pride!
Googlebot, Lighthouse, each gets their way —
No more UA hacks to brighten your day.
With a boolean or function, the choice is all yours,
As the rabbit hops through the streaming SSR doors! 🌊

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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 clearly summarizes the main change: adding an isBot option to renderRouterToStream for configurable bot detection in streaming SSR.
Linked Issues check ✅ Passed The PR fully implements all coding objectives from #7660: adds configurable isBot option (undefined/boolean/function) across react/solid/vue routers, maintains backward-compatible defaults, and includes comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the isBot option feature across routing packages, test files, and changeset documentation with no extraneous modifications.

✏️ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
packages/solid-router/src/ssr/renderRouterToStream.tsx (1)

18-19: ⚡ Quick win

Use braces for if statements in resolveIsBot.

This currently breaks the repository control-statement style rule.

As per coding guidelines, **/*.{ts,tsx,js,jsx} must always use curly braces for if, else, loops, and similar control statements.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/solid-router/src/ssr/renderRouterToStream.tsx` around lines 18 - 19,
The if statements in the resolveIsBot function are missing curly braces, which
violates the repository's control-statement style guidelines. Add curly braces
around the return statements for both if conditions that check typeof isBot ===
'function' and typeof isBot === 'boolean' to ensure consistent code style across
the repository.

Source: Coding guidelines

packages/react-router/src/ssr/renderRouterToStream.tsx (1)

48-49: ⚡ Quick win

Wrap if branches in braces in resolveIsBot.

This violates the project control-statement style rule for TS/TSX files.

Suggested fix
 const resolveIsBot = (router: AnyRouter, request: Request): boolean => {
   const isBot = router.options.ssr?.isBot
-  if (typeof isBot === 'function') return isBot(request)
-  if (typeof isBot === 'boolean') return isBot
+  if (typeof isBot === 'function') {
+    return isBot(request)
+  }
+  if (typeof isBot === 'boolean') {
+    return isBot
+  }
   return isbot(request.headers.get('User-Agent'))
 }

As per coding guidelines, **/*.{ts,tsx,js,jsx} must always use curly braces for if, else, loops, and similar control statements.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react-router/src/ssr/renderRouterToStream.tsx` around lines 48 - 49,
The `if` statements in the `resolveIsBot` function are missing curly braces
around their bodies, which violates the project's control-statement style rule.
Add curly braces around both the `if (typeof isBot === 'function')` and `if
(typeof isBot === 'boolean')` conditional bodies so that the return statements
are wrapped within braces.

Source: Coding guidelines

packages/vue-router/src/ssr/renderRouterToStream.tsx (1)

21-22: ⚡ Quick win

Add braces around resolveIsBot if branches.

Current form violates the project JS/TS control-statement style requirement.

As per coding guidelines, **/*.{ts,tsx,js,jsx} must always use curly braces for if, else, loops, and similar control statements.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vue-router/src/ssr/renderRouterToStream.tsx` around lines 21 - 22,
The two if statements in the resolveIsBot function that check the type of isBot
are missing required curly braces around their bodies. Add curly braces around
the return statement in the condition checking typeof isBot === 'function' and
also add curly braces around the return statement in the condition checking
typeof isBot === 'boolean' to comply with the project's control statement style
requirements.

Source: Coding guidelines

packages/router-core/src/router.ts (1)

515-517: ⚡ Quick win

Use framework-neutral wording in core ssr.isBot docs.

This API is shared across React/Solid/Vue adapters, but the comment currently names React-specific allReady, which can mislead non-React users.

Suggested doc tweak
-     * document to render (React's `allReady`) before responding, so crawlers
+     * document to render before responding, so crawlers
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/router-core/src/router.ts` around lines 515 - 517, The documentation
comment for the ssr.isBot API contains React-specific terminology (React's
`allReady`) that is not framework-neutral. Since this API is shared across
React, Solid, and Vue adapters, replace the React-specific reference with
framework-agnostic language that describes the same behavior (waiting for the
entire document to render before responding to bot requests) without naming
React APIs. Ensure the reworded comment clearly explains the bot detection
behavior in terms that apply to all supported frameworks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/react-router/src/ssr/renderRouterToStream.tsx`:
- Around line 48-49: The `if` statements in the `resolveIsBot` function are
missing curly braces around their bodies, which violates the project's
control-statement style rule. Add curly braces around both the `if (typeof isBot
=== 'function')` and `if (typeof isBot === 'boolean')` conditional bodies so
that the return statements are wrapped within braces.

In `@packages/router-core/src/router.ts`:
- Around line 515-517: The documentation comment for the ssr.isBot API contains
React-specific terminology (React's `allReady`) that is not framework-neutral.
Since this API is shared across React, Solid, and Vue adapters, replace the
React-specific reference with framework-agnostic language that describes the
same behavior (waiting for the entire document to render before responding to
bot requests) without naming React APIs. Ensure the reworded comment clearly
explains the bot detection behavior in terms that apply to all supported
frameworks.

In `@packages/solid-router/src/ssr/renderRouterToStream.tsx`:
- Around line 18-19: The if statements in the resolveIsBot function are missing
curly braces, which violates the repository's control-statement style
guidelines. Add curly braces around the return statements for both if conditions
that check typeof isBot === 'function' and typeof isBot === 'boolean' to ensure
consistent code style across the repository.

In `@packages/vue-router/src/ssr/renderRouterToStream.tsx`:
- Around line 21-22: The two if statements in the resolveIsBot function that
check the type of isBot are missing required curly braces around their bodies.
Add curly braces around the return statement in the condition checking typeof
isBot === 'function' and also add curly braces around the return statement in
the condition checking typeof isBot === 'boolean' to comply with the project's
control statement style requirements.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bd458c22-3db0-4906-ba99-ec2eb9016b26

📥 Commits

Reviewing files that changed from the base of the PR and between 279a849 and ce89206.

📒 Files selected for processing (6)
  • .changeset/ssr-isbot-option.md
  • packages/react-router/src/ssr/renderRouterToStream.tsx
  • packages/react-router/tests/renderRouterToStream.test.tsx
  • packages/router-core/src/router.ts
  • packages/solid-router/src/ssr/renderRouterToStream.tsx
  • packages/vue-router/src/ssr/renderRouterToStream.tsx

createRouter runs isomorphically, so an `ssr.isBot` predicate (and anything it imports) would be bundled into the client. Move the option to `renderRouterToStream` (in ssr/server, server-only) so the bot decision is made in the request handler instead. Default behavior is unchanged. Reverts the router-core RouterOptions change.
@anonrig anonrig changed the title feat: add ssr.isBot router option for streaming SSR bot detection feat: add isBot option to renderRouterToStream for streaming SSR bot detection Jun 21, 2026
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.

Streaming SSR: make the isbot "buffer full HTML for bots" behavior configurable (opt-in / opt-out)

1 participant