Skip to content

SSR dehydrated match IDs are URL-shaped strings — Google crawls them as phantom URLs #6739

@agry-nejo

Description

@agry-nejo

Which project does this relate to?

Router

Describe the bug

Disclaimer: I'm relatively new to TanStack Start/Router and it's possible I'm missing a configuration option that addresses this. If so, I'd appreciate being pointed in the right direction. I did search existing issues and docs before filing.

TanStack Router constructs match IDs by concatenating route.id + interpolatedPath + loaderDepsHash (router.ts:1630). These match IDs are then serialized into SSR HTML via dehydrateMatch() (ssr-server.ts:40) and injected into inline <script> tags through crossSerializeStream.

The resulting match IDs look like relative URLs. For example, a route src/routes/$orgId/projects/$projectId/index.tsx visited at /acme/projects/dashboard produces:

/$orgId/projects/$projectId//acme/projects/dashboard/{}

Google's crawler parses the full SSR HTML source — including <script> content — extracts these path-like strings, and attempts to crawl them. This produces phantom entries in Google Search Console reported as "Redirect Error" or "Not Found (404)".

This is the same class of issue as Next.js's __NEXT_DATA__ problem (vercel/next.js#39377, vercel/next.js#40143).

Your Example Website or App

https://stackblitz.com/edit/github-4qzy4wjc?file=src%2Froutes%2Fabout.tsx

Steps to Reproduce the Bug or Issue

  1. Create a TanStack Start app with a dynamic route (e.g., src/routes/$orgId/projects/$projectId/index.tsx)
  2. Start the dev server
  3. Visit a page that resolves the route (e.g., /acme/projects/dashboard)
  4. View the HTML source
  5. Search for $_TSR in the source — the dehydrated <script> tags contain match IDs like:
    /$orgId/projects/$projectId//acme/projects/dashboard/
    
    This string starts with /, contains path separators, and is indistinguishable from a relative URL.

Expected behavior

Match IDs serialized into SSR HTML should not be extractable by crawlers as valid URLs. They are internal router identifiers and ideally they use a format that cannot be mistaken for relative URL paths.

Screenshots or Videos

No response

Platform

  • Router / Start Version: 1.157.18
  • OS: Windows
  • Browser: Chrome
  • Browser Version: 133
  • Bundler: vite
  • Bundler Version: 7.3.1

Additional context

The match ID at router.ts:1630 is a simple string concatenation:

const matchId =
  route.id +           // "/$orgId/projects/$projectId/"  (route pattern)
  interpolatedPath +   // "/acme/projects/dashboard/"     (resolved path)
  loaderDepsHash       // "{}"                            (JSON.stringify of deps)

Result: /$orgId/projects/$projectId//acme/projects/dashboard/{} — a string that:

  • Starts with / (relative URL)
  • Contains valid path separators throughout
  • Is serialized as a plain string property i: match.id in dehydrateMatch()
  • Is injected as-is into inline <script> tags via crossSerializeStream from seroval

Google's crawler is known to extract path-like strings from HTML source — including inline script content — and attempt to fetch them.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions