From 0b6b6cf4f51ad1d3524d3d6f686cf9c90b81dd22 Mon Sep 17 00:00:00 2001
From: Manuel Schiller <manuel.schiller@caligano.de>
Date: Sat, 11 Apr 2026 19:31:07 +0200
Subject: [PATCH 1/3] fix: reduce start SSR manifest asset duplication

---
 .changeset/cyan-camels-dance.md               |   9 +
 e2e/react-start/start-manifest/package.json   |  35 ++
 .../start-manifest/playwright.config.ts       |  30 ++
 .../src/components/AppShell.tsx               |  33 ++
 .../src/components/SharedNestedLayout.tsx     |  29 ++
 .../start-manifest/src/routeTree.gen.ts       | 261 +++++++++++++++
 e2e/react-start/start-manifest/src/router.tsx |  18 +
 .../start-manifest/src/routes/__root.tsx      |  30 ++
 .../start-manifest/src/routes/index.tsx       |  16 +
 .../start-manifest/src/routes/r1.tsx          |  17 +
 .../start-manifest/src/routes/r2.tsx          |  17 +
 .../start-manifest/src/routes/r3.tsx          |  14 +
 .../start-manifest/src/routes/r4.tsx          |  14 +
 .../start-manifest/src/routes/r5.tsx          |  14 +
 .../start-manifest/src/routes/r6.tsx          |  14 +
 .../start-manifest/src/routes/shared-a.tsx    |  14 +
 .../start-manifest/src/routes/shared-b.tsx    |  14 +
 .../start-manifest/src/routes/shared-c.tsx    |  14 +
 .../src/styles/root-shell.module.css          |  37 ++
 .../src/styles/route-one.module.css           |   7 +
 .../src/styles/route-two.module.css           |   7 +
 .../src/styles/shared-layout.module.css       |  16 +
 .../tests/start-manifest.spec.ts              | 315 ++++++++++++++++++
 e2e/react-start/start-manifest/tsconfig.json  |  21 ++
 e2e/react-start/start-manifest/vite.config.ts |  14 +
 packages/router-core/src/ssr/ssr-server.ts    |   3 +
 .../tests/ssr-server-manifest.test.ts         |  98 ++++++
 packages/start-plugin-core/package.json       |   1 +
 .../start-manifest-plugin/manifestBuilder.ts  |  12 +-
 .../src/vite/start-manifest-plugin/plugin.ts  |   7 +-
 .../manifestBuilder.test.ts                   |  91 +++++
 .../src/createStartHandler.ts                 |   1 +
 pnpm-lock.yaml                                |  46 +++
 33 files changed, 1266 insertions(+), 3 deletions(-)
 create mode 100644 .changeset/cyan-camels-dance.md
 create mode 100644 e2e/react-start/start-manifest/package.json
 create mode 100644 e2e/react-start/start-manifest/playwright.config.ts
 create mode 100644 e2e/react-start/start-manifest/src/components/AppShell.tsx
 create mode 100644 e2e/react-start/start-manifest/src/components/SharedNestedLayout.tsx
 create mode 100644 e2e/react-start/start-manifest/src/routeTree.gen.ts
 create mode 100644 e2e/react-start/start-manifest/src/router.tsx
 create mode 100644 e2e/react-start/start-manifest/src/routes/__root.tsx
 create mode 100644 e2e/react-start/start-manifest/src/routes/index.tsx
 create mode 100644 e2e/react-start/start-manifest/src/routes/r1.tsx
 create mode 100644 e2e/react-start/start-manifest/src/routes/r2.tsx
 create mode 100644 e2e/react-start/start-manifest/src/routes/r3.tsx
 create mode 100644 e2e/react-start/start-manifest/src/routes/r4.tsx
 create mode 100644 e2e/react-start/start-manifest/src/routes/r5.tsx
 create mode 100644 e2e/react-start/start-manifest/src/routes/r6.tsx
 create mode 100644 e2e/react-start/start-manifest/src/routes/shared-a.tsx
 create mode 100644 e2e/react-start/start-manifest/src/routes/shared-b.tsx
 create mode 100644 e2e/react-start/start-manifest/src/routes/shared-c.tsx
 create mode 100644 e2e/react-start/start-manifest/src/styles/root-shell.module.css
 create mode 100644 e2e/react-start/start-manifest/src/styles/route-one.module.css
 create mode 100644 e2e/react-start/start-manifest/src/styles/route-two.module.css
 create mode 100644 e2e/react-start/start-manifest/src/styles/shared-layout.module.css
 create mode 100644 e2e/react-start/start-manifest/tests/start-manifest.spec.ts
 create mode 100644 e2e/react-start/start-manifest/tsconfig.json
 create mode 100644 e2e/react-start/start-manifest/vite.config.ts
 create mode 100644 packages/router-core/tests/ssr-server-manifest.test.ts

diff --git a/.changeset/cyan-camels-dance.md b/.changeset/cyan-camels-dance.md
new file mode 100644
index 0000000000..9c2fc891c5
--- /dev/null
+++ b/.changeset/cyan-camels-dance.md
@@ -0,0 +1,9 @@
+---
+'@tanstack/router-core': patch
+'@tanstack/start-plugin-core': patch
+'@tanstack/start-server-core': patch
+---
+
+Reduce React Start SSR manifest payload size by omitting unmatched route assets from dehydrated router state while keeping start-manifest asset serialization deduplicated by shared object identity.
+
+This improves SSR HTML size for apps with many routes that share the same CSS assets and adds regression coverage for CSS module hydration, navigation, and start-manifest asset reuse.
diff --git a/e2e/react-start/start-manifest/package.json b/e2e/react-start/start-manifest/package.json
new file mode 100644
index 0000000000..6d7814a688
--- /dev/null
+++ b/e2e/react-start/start-manifest/package.json
@@ -0,0 +1,35 @@
+{
+  "name": "tanstack-react-start-e2e-start-manifest",
+  "private": true,
+  "sideEffects": false,
+  "type": "module",
+  "scripts": {
+    "build": "vite build && tsc --noEmit",
+    "start": "node .output/server/index.mjs",
+    "test:e2e": "playwright test --project=chromium"
+  },
+  "dependencies": {
+    "@tanstack/react-router": "workspace:^",
+    "@tanstack/react-start": "workspace:^",
+    "react": "^19.0.0",
+    "react-dom": "^19.0.0"
+  },
+  "devDependencies": {
+    "@playwright/test": "^1.50.1",
+    "@tanstack/router-e2e-utils": "workspace:^",
+    "@types/node": "^22.10.2",
+    "@types/react": "^19.0.8",
+    "@types/react-dom": "^19.0.3",
+    "@vitejs/plugin-react": "^6.0.1",
+    "nitro": "^3.0.260311-beta",
+    "typescript": "^6.0.2",
+    "vite": "^8.0.0"
+  },
+  "nx": {
+    "targets": {
+      "test:e2e": {
+        "parallelism": false
+      }
+    }
+  }
+}
diff --git a/e2e/react-start/start-manifest/playwright.config.ts b/e2e/react-start/start-manifest/playwright.config.ts
new file mode 100644
index 0000000000..9e72f6160b
--- /dev/null
+++ b/e2e/react-start/start-manifest/playwright.config.ts
@@ -0,0 +1,30 @@
+import { defineConfig, devices } from '@playwright/test'
+import { getTestServerPort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = await getTestServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+
+export default defineConfig({
+  testDir: './tests',
+  workers: 1,
+  reporter: [['line']],
+
+  use: {
+    baseURL,
+  },
+
+  webServer: {
+    command: `PORT=${PORT} pnpm build && PORT=${PORT} pnpm start`,
+    url: baseURL,
+    reuseExistingServer: !process.env.CI,
+    stdout: 'pipe',
+  },
+
+  projects: [
+    {
+      name: 'chromium',
+      use: { ...devices['Desktop Chrome'] },
+    },
+  ],
+})
diff --git a/e2e/react-start/start-manifest/src/components/AppShell.tsx b/e2e/react-start/start-manifest/src/components/AppShell.tsx
new file mode 100644
index 0000000000..99df40387b
--- /dev/null
+++ b/e2e/react-start/start-manifest/src/components/AppShell.tsx
@@ -0,0 +1,33 @@
+import { ClientOnly, Link, Outlet, linkOptions } from '@tanstack/react-router'
+import styles from '~/styles/root-shell.module.css'
+
+const ROUTES = linkOptions([
+  { to: '/', label: 'home' },
+  { to: '/r1', label: '/r1' },
+  { to: '/r2', label: '/r2' },
+  { to: '/shared-a', label: '/shared-a' },
+])
+
+export function AppShell() {
+  return (
+    <div className={styles.shell}>
+      <nav className={styles.nav}>
+        {ROUTES.map((route) => (
+          <Link key={route.to} {...route} data-testid={`nav-${route.label}`}>
+            {route.label}
+          </Link>
+        ))}
+      </nav>
+
+      <main className={styles.content}>
+        <div className={styles.rootBadge} data-testid="root-shell-marker">
+          Start manifest CSS root shell
+        </div>
+        <ClientOnly>
+          <div data-testid="hydration-marker">hydrated</div>
+        </ClientOnly>
+        <Outlet />
+      </main>
+    </div>
+  )
+}
diff --git a/e2e/react-start/start-manifest/src/components/SharedNestedLayout.tsx b/e2e/react-start/start-manifest/src/components/SharedNestedLayout.tsx
new file mode 100644
index 0000000000..5a88a30f41
--- /dev/null
+++ b/e2e/react-start/start-manifest/src/components/SharedNestedLayout.tsx
@@ -0,0 +1,29 @@
+import { Link, linkOptions } from '@tanstack/react-router'
+import type { ReactNode } from 'react'
+import styles from '~/styles/shared-layout.module.css'
+
+const SHARED_ROUTES = linkOptions([
+  { to: '/shared-a', label: '/shared-a' },
+  { to: '/shared-b', label: '/shared-b' },
+  { to: '/shared-c', label: '/shared-c' },
+])
+
+export function SharedNestedLayout({ children }: { children: ReactNode }) {
+  return (
+    <section className={styles.layout} data-testid="shared-layout-shell">
+      <div className={styles.heading} data-testid="shared-layout-heading">
+        Shared nested layout CSS module
+      </div>
+      <nav>
+        {SHARED_ROUTES.map((route) => (
+          <Link key={route.to} {...route} data-testid={`nav-${route.label}`}>
+            {route.label}
+          </Link>
+        ))}
+      </nav>
+      <div className={styles.body} data-testid="shared-layout-copy">
+        {children}
+      </div>
+    </section>
+  )
+}
diff --git a/e2e/react-start/start-manifest/src/routeTree.gen.ts b/e2e/react-start/start-manifest/src/routeTree.gen.ts
new file mode 100644
index 0000000000..0571da3592
--- /dev/null
+++ b/e2e/react-start/start-manifest/src/routeTree.gen.ts
@@ -0,0 +1,261 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as SharedCRouteImport } from './routes/shared-c'
+import { Route as SharedBRouteImport } from './routes/shared-b'
+import { Route as SharedARouteImport } from './routes/shared-a'
+import { Route as R6RouteImport } from './routes/r6'
+import { Route as R5RouteImport } from './routes/r5'
+import { Route as R4RouteImport } from './routes/r4'
+import { Route as R3RouteImport } from './routes/r3'
+import { Route as R2RouteImport } from './routes/r2'
+import { Route as R1RouteImport } from './routes/r1'
+import { Route as IndexRouteImport } from './routes/index'
+
+const SharedCRoute = SharedCRouteImport.update({
+  id: '/shared-c',
+  path: '/shared-c',
+  getParentRoute: () => rootRouteImport,
+} as any)
+const SharedBRoute = SharedBRouteImport.update({
+  id: '/shared-b',
+  path: '/shared-b',
+  getParentRoute: () => rootRouteImport,
+} as any)
+const SharedARoute = SharedARouteImport.update({
+  id: '/shared-a',
+  path: '/shared-a',
+  getParentRoute: () => rootRouteImport,
+} as any)
+const R6Route = R6RouteImport.update({
+  id: '/r6',
+  path: '/r6',
+  getParentRoute: () => rootRouteImport,
+} as any)
+const R5Route = R5RouteImport.update({
+  id: '/r5',
+  path: '/r5',
+  getParentRoute: () => rootRouteImport,
+} as any)
+const R4Route = R4RouteImport.update({
+  id: '/r4',
+  path: '/r4',
+  getParentRoute: () => rootRouteImport,
+} as any)
+const R3Route = R3RouteImport.update({
+  id: '/r3',
+  path: '/r3',
+  getParentRoute: () => rootRouteImport,
+} as any)
+const R2Route = R2RouteImport.update({
+  id: '/r2',
+  path: '/r2',
+  getParentRoute: () => rootRouteImport,
+} as any)
+const R1Route = R1RouteImport.update({
+  id: '/r1',
+  path: '/r1',
+  getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+  id: '/',
+  path: '/',
+  getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+  '/': typeof IndexRoute
+  '/r1': typeof R1Route
+  '/r2': typeof R2Route
+  '/r3': typeof R3Route
+  '/r4': typeof R4Route
+  '/r5': typeof R5Route
+  '/r6': typeof R6Route
+  '/shared-a': typeof SharedARoute
+  '/shared-b': typeof SharedBRoute
+  '/shared-c': typeof SharedCRoute
+}
+export interface FileRoutesByTo {
+  '/': typeof IndexRoute
+  '/r1': typeof R1Route
+  '/r2': typeof R2Route
+  '/r3': typeof R3Route
+  '/r4': typeof R4Route
+  '/r5': typeof R5Route
+  '/r6': typeof R6Route
+  '/shared-a': typeof SharedARoute
+  '/shared-b': typeof SharedBRoute
+  '/shared-c': typeof SharedCRoute
+}
+export interface FileRoutesById {
+  __root__: typeof rootRouteImport
+  '/': typeof IndexRoute
+  '/r1': typeof R1Route
+  '/r2': typeof R2Route
+  '/r3': typeof R3Route
+  '/r4': typeof R4Route
+  '/r5': typeof R5Route
+  '/r6': typeof R6Route
+  '/shared-a': typeof SharedARoute
+  '/shared-b': typeof SharedBRoute
+  '/shared-c': typeof SharedCRoute
+}
+export interface FileRouteTypes {
+  fileRoutesByFullPath: FileRoutesByFullPath
+  fullPaths:
+    | '/'
+    | '/r1'
+    | '/r2'
+    | '/r3'
+    | '/r4'
+    | '/r5'
+    | '/r6'
+    | '/shared-a'
+    | '/shared-b'
+    | '/shared-c'
+  fileRoutesByTo: FileRoutesByTo
+  to:
+    | '/'
+    | '/r1'
+    | '/r2'
+    | '/r3'
+    | '/r4'
+    | '/r5'
+    | '/r6'
+    | '/shared-a'
+    | '/shared-b'
+    | '/shared-c'
+  id:
+    | '__root__'
+    | '/'
+    | '/r1'
+    | '/r2'
+    | '/r3'
+    | '/r4'
+    | '/r5'
+    | '/r6'
+    | '/shared-a'
+    | '/shared-b'
+    | '/shared-c'
+  fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+  IndexRoute: typeof IndexRoute
+  R1Route: typeof R1Route
+  R2Route: typeof R2Route
+  R3Route: typeof R3Route
+  R4Route: typeof R4Route
+  R5Route: typeof R5Route
+  R6Route: typeof R6Route
+  SharedARoute: typeof SharedARoute
+  SharedBRoute: typeof SharedBRoute
+  SharedCRoute: typeof SharedCRoute
+}
+
+declare module '@tanstack/react-router' {
+  interface FileRoutesByPath {
+    '/shared-c': {
+      id: '/shared-c'
+      path: '/shared-c'
+      fullPath: '/shared-c'
+      preLoaderRoute: typeof SharedCRouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/shared-b': {
+      id: '/shared-b'
+      path: '/shared-b'
+      fullPath: '/shared-b'
+      preLoaderRoute: typeof SharedBRouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/shared-a': {
+      id: '/shared-a'
+      path: '/shared-a'
+      fullPath: '/shared-a'
+      preLoaderRoute: typeof SharedARouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/r6': {
+      id: '/r6'
+      path: '/r6'
+      fullPath: '/r6'
+      preLoaderRoute: typeof R6RouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/r5': {
+      id: '/r5'
+      path: '/r5'
+      fullPath: '/r5'
+      preLoaderRoute: typeof R5RouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/r4': {
+      id: '/r4'
+      path: '/r4'
+      fullPath: '/r4'
+      preLoaderRoute: typeof R4RouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/r3': {
+      id: '/r3'
+      path: '/r3'
+      fullPath: '/r3'
+      preLoaderRoute: typeof R3RouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/r2': {
+      id: '/r2'
+      path: '/r2'
+      fullPath: '/r2'
+      preLoaderRoute: typeof R2RouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/r1': {
+      id: '/r1'
+      path: '/r1'
+      fullPath: '/r1'
+      preLoaderRoute: typeof R1RouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/': {
+      id: '/'
+      path: '/'
+      fullPath: '/'
+      preLoaderRoute: typeof IndexRouteImport
+      parentRoute: typeof rootRouteImport
+    }
+  }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+  IndexRoute: IndexRoute,
+  R1Route: R1Route,
+  R2Route: R2Route,
+  R3Route: R3Route,
+  R4Route: R4Route,
+  R5Route: R5Route,
+  R6Route: R6Route,
+  SharedARoute: SharedARoute,
+  SharedBRoute: SharedBRoute,
+  SharedCRoute: SharedCRoute,
+}
+export const routeTree = rootRouteImport
+  ._addFileChildren(rootRouteChildren)
+  ._addFileTypes<FileRouteTypes>()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/react-start'
+declare module '@tanstack/react-start' {
+  interface Register {
+    ssr: true
+    router: Awaited<ReturnType<typeof getRouter>>
+  }
+}
diff --git a/e2e/react-start/start-manifest/src/router.tsx b/e2e/react-start/start-manifest/src/router.tsx
new file mode 100644
index 0000000000..a575758450
--- /dev/null
+++ b/e2e/react-start/start-manifest/src/router.tsx
@@ -0,0 +1,18 @@
+import { createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+
+export const getRouter = () => {
+  const router = createRouter({
+    routeTree,
+    scrollRestoration: true,
+    defaultPreload: 'intent',
+  })
+
+  return router
+}
+
+declare module '@tanstack/react-router' {
+  interface Register {
+    router: ReturnType<typeof getRouter>
+  }
+}
diff --git a/e2e/react-start/start-manifest/src/routes/__root.tsx b/e2e/react-start/start-manifest/src/routes/__root.tsx
new file mode 100644
index 0000000000..af8098eeb5
--- /dev/null
+++ b/e2e/react-start/start-manifest/src/routes/__root.tsx
@@ -0,0 +1,30 @@
+/// <reference types="vite/client" />
+import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
+import { AppShell } from '~/components/AppShell'
+import type { ReactNode } from 'react'
+
+export const Route = createRootRoute({
+  head: () => ({
+    meta: [
+      { charSet: 'utf-8' },
+      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
+      { title: 'TanStack Start Manifest Bloat E2E' },
+    ],
+  }),
+  component: AppShell,
+  shellComponent: RootDocument,
+})
+
+function RootDocument({ children }: { children: ReactNode }) {
+  return (
+    <html lang="en">
+      <head>
+        <HeadContent />
+      </head>
+      <body>
+        {children}
+        <Scripts />
+      </body>
+    </html>
+  )
+}
diff --git a/e2e/react-start/start-manifest/src/routes/index.tsx b/e2e/react-start/start-manifest/src/routes/index.tsx
new file mode 100644
index 0000000000..bac21d3bd2
--- /dev/null
+++ b/e2e/react-start/start-manifest/src/routes/index.tsx
@@ -0,0 +1,16 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+  component: HomeRoute,
+})
+
+function HomeRoute() {
+  return (
+    <section>
+      <h1>Start manifest fixture</h1>
+      <p data-testid="home-copy">
+        Use this page to navigate between CSS module routes.
+      </p>
+    </section>
+  )
+}
diff --git a/e2e/react-start/start-manifest/src/routes/r1.tsx b/e2e/react-start/start-manifest/src/routes/r1.tsx
new file mode 100644
index 0000000000..b74b3d23e6
--- /dev/null
+++ b/e2e/react-start/start-manifest/src/routes/r1.tsx
@@ -0,0 +1,17 @@
+import { createFileRoute } from '@tanstack/react-router'
+import styles from '~/styles/route-one.module.css'
+
+export const Route = createFileRoute('/r1')({
+  component: RouteComponent,
+})
+
+function RouteComponent() {
+  return (
+    <section>
+      <h1>Route /r1</h1>
+      <div className={styles.card} data-testid="route-r1-card">
+        Route /r1 CSS module styling
+      </div>
+    </section>
+  )
+}
diff --git a/e2e/react-start/start-manifest/src/routes/r2.tsx b/e2e/react-start/start-manifest/src/routes/r2.tsx
new file mode 100644
index 0000000000..235879f021
--- /dev/null
+++ b/e2e/react-start/start-manifest/src/routes/r2.tsx
@@ -0,0 +1,17 @@
+import { createFileRoute } from '@tanstack/react-router'
+import styles from '~/styles/route-two.module.css'
+
+export const Route = createFileRoute('/r2')({
+  component: RouteComponent,
+})
+
+function RouteComponent() {
+  return (
+    <section>
+      <h1>Route /r2</h1>
+      <div className={styles.card} data-testid="route-r2-card">
+        Route /r2 CSS module styling
+      </div>
+    </section>
+  )
+}
diff --git a/e2e/react-start/start-manifest/src/routes/r3.tsx b/e2e/react-start/start-manifest/src/routes/r3.tsx
new file mode 100644
index 0000000000..46a2ed2cb4
--- /dev/null
+++ b/e2e/react-start/start-manifest/src/routes/r3.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/r3')({
+  component: RouteComponent,
+})
+
+function RouteComponent() {
+  return (
+    <section>
+      <h1>Route /r3</h1>
+      <p>Auxiliary route.</p>
+    </section>
+  )
+}
diff --git a/e2e/react-start/start-manifest/src/routes/r4.tsx b/e2e/react-start/start-manifest/src/routes/r4.tsx
new file mode 100644
index 0000000000..da398831a6
--- /dev/null
+++ b/e2e/react-start/start-manifest/src/routes/r4.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/r4')({
+  component: RouteComponent,
+})
+
+function RouteComponent() {
+  return (
+    <section>
+      <h1>Route /r4</h1>
+      <p>Auxiliary route.</p>
+    </section>
+  )
+}
diff --git a/e2e/react-start/start-manifest/src/routes/r5.tsx b/e2e/react-start/start-manifest/src/routes/r5.tsx
new file mode 100644
index 0000000000..197ebcff55
--- /dev/null
+++ b/e2e/react-start/start-manifest/src/routes/r5.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/r5')({
+  component: RouteComponent,
+})
+
+function RouteComponent() {
+  return (
+    <section>
+      <h1>Route /r5</h1>
+      <p>Auxiliary route.</p>
+    </section>
+  )
+}
diff --git a/e2e/react-start/start-manifest/src/routes/r6.tsx b/e2e/react-start/start-manifest/src/routes/r6.tsx
new file mode 100644
index 0000000000..88d128fd59
--- /dev/null
+++ b/e2e/react-start/start-manifest/src/routes/r6.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/r6')({
+  component: RouteComponent,
+})
+
+function RouteComponent() {
+  return (
+    <section>
+      <h1>Route /r6</h1>
+      <p>Auxiliary route.</p>
+    </section>
+  )
+}
diff --git a/e2e/react-start/start-manifest/src/routes/shared-a.tsx b/e2e/react-start/start-manifest/src/routes/shared-a.tsx
new file mode 100644
index 0000000000..566362d52a
--- /dev/null
+++ b/e2e/react-start/start-manifest/src/routes/shared-a.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { SharedNestedLayout } from '~/components/SharedNestedLayout'
+
+export const Route = createFileRoute('/shared-a')({
+  component: RouteComponent,
+})
+
+function RouteComponent() {
+  return (
+    <SharedNestedLayout>
+      <div data-testid="shared-a-card">Shared route A</div>
+    </SharedNestedLayout>
+  )
+}
diff --git a/e2e/react-start/start-manifest/src/routes/shared-b.tsx b/e2e/react-start/start-manifest/src/routes/shared-b.tsx
new file mode 100644
index 0000000000..e4441f471f
--- /dev/null
+++ b/e2e/react-start/start-manifest/src/routes/shared-b.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { SharedNestedLayout } from '~/components/SharedNestedLayout'
+
+export const Route = createFileRoute('/shared-b')({
+  component: RouteComponent,
+})
+
+function RouteComponent() {
+  return (
+    <SharedNestedLayout>
+      <div data-testid="shared-b-card">Shared route B</div>
+    </SharedNestedLayout>
+  )
+}
diff --git a/e2e/react-start/start-manifest/src/routes/shared-c.tsx b/e2e/react-start/start-manifest/src/routes/shared-c.tsx
new file mode 100644
index 0000000000..85e5fca0ae
--- /dev/null
+++ b/e2e/react-start/start-manifest/src/routes/shared-c.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { SharedNestedLayout } from '~/components/SharedNestedLayout'
+
+export const Route = createFileRoute('/shared-c')({
+  component: RouteComponent,
+})
+
+function RouteComponent() {
+  return (
+    <SharedNestedLayout>
+      <div data-testid="shared-c-card">Shared route C</div>
+    </SharedNestedLayout>
+  )
+}
diff --git a/e2e/react-start/start-manifest/src/styles/root-shell.module.css b/e2e/react-start/start-manifest/src/styles/root-shell.module.css
new file mode 100644
index 0000000000..e7bece8664
--- /dev/null
+++ b/e2e/react-start/start-manifest/src/styles/root-shell.module.css
@@ -0,0 +1,37 @@
+.shell {
+  font-family:
+    -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+  min-height: 100vh;
+  background: rgb(248, 250, 252);
+  color: rgb(15, 23, 42);
+}
+
+.nav {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 0.75rem;
+  padding: 1rem;
+  border-bottom: 1px solid rgb(226, 232, 240);
+  background: white;
+}
+
+.nav a {
+  color: rgb(37, 99, 235);
+  text-decoration: none;
+}
+
+.content {
+  max-width: 760px;
+  margin: 0 auto;
+  padding: 1.5rem 1rem 3rem;
+}
+
+.rootBadge {
+  display: inline-block;
+  margin-bottom: 1rem;
+  padding: 0.375rem 0.75rem;
+  border-radius: 9999px;
+  background: rgb(220, 252, 231);
+  color: rgb(22, 101, 52);
+  font-weight: 700;
+}
diff --git a/e2e/react-start/start-manifest/src/styles/route-one.module.css b/e2e/react-start/start-manifest/src/styles/route-one.module.css
new file mode 100644
index 0000000000..623d03e6a3
--- /dev/null
+++ b/e2e/react-start/start-manifest/src/styles/route-one.module.css
@@ -0,0 +1,7 @@
+.card {
+  padding: 1rem;
+  border-radius: 0.75rem;
+  background: rgb(239, 246, 255);
+  color: rgb(30, 64, 175);
+  border: 2px solid rgb(96, 165, 250);
+}
diff --git a/e2e/react-start/start-manifest/src/styles/route-two.module.css b/e2e/react-start/start-manifest/src/styles/route-two.module.css
new file mode 100644
index 0000000000..66ed355711
--- /dev/null
+++ b/e2e/react-start/start-manifest/src/styles/route-two.module.css
@@ -0,0 +1,7 @@
+.card {
+  padding: 1rem;
+  border-radius: 0.75rem;
+  background: rgb(250, 245, 255);
+  color: rgb(126, 34, 206);
+  border: 2px solid rgb(192, 132, 252);
+}
diff --git a/e2e/react-start/start-manifest/src/styles/shared-layout.module.css b/e2e/react-start/start-manifest/src/styles/shared-layout.module.css
new file mode 100644
index 0000000000..6025a1da06
--- /dev/null
+++ b/e2e/react-start/start-manifest/src/styles/shared-layout.module.css
@@ -0,0 +1,16 @@
+.layout {
+  padding: 1rem;
+  border-radius: 0.75rem;
+  background: rgb(255, 251, 235);
+  border: 2px solid rgb(245, 158, 11);
+}
+
+.heading {
+  color: rgb(180, 83, 9);
+  font-weight: 700;
+  margin-bottom: 0.5rem;
+}
+
+.body {
+  color: rgb(146, 64, 14);
+}
diff --git a/e2e/react-start/start-manifest/tests/start-manifest.spec.ts b/e2e/react-start/start-manifest/tests/start-manifest.spec.ts
new file mode 100644
index 0000000000..d04d42b2db
--- /dev/null
+++ b/e2e/react-start/start-manifest/tests/start-manifest.spec.ts
@@ -0,0 +1,315 @@
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+import type { Page } from '@playwright/test'
+import { readdir } from 'node:fs/promises'
+import path from 'node:path'
+import { pathToFileURL } from 'node:url'
+
+test.skip(process.env.MODE === 'dev', 'Prod-only manifest dehydration coverage')
+
+const ROOT_SHELL_COLOR = 'rgb(22, 101, 52)'
+const ROUTE_ONE_COLOR = 'rgb(30, 64, 175)'
+const ROUTE_TWO_COLOR = 'rgb(126, 34, 206)'
+
+const buildUrl = (baseURL: string, pathname: string) =>
+  baseURL.replace(/\/$/, '') + pathname
+
+async function getColor(testId: string, page: Page) {
+  return page
+    .getByTestId(testId)
+    .evaluate((element) => getComputedStyle(element).color)
+}
+
+function getStylesheetHrefsFromHtml(html: string) {
+  return Array.from(
+    html.matchAll(/<link[^>]+rel="stylesheet"[^>]+href="([^"]+)"/g),
+    (match) => match[1]!,
+  )
+}
+
+async function getHeadStylesheetHrefs(page: Page) {
+  return page.locator('head link[rel="stylesheet"]').evaluateAll((links) => {
+    return links.map(
+      (link) => (link as HTMLLinkElement).getAttribute('href') || '',
+    )
+  })
+}
+
+function countMatchingStylesheetHrefs(hrefs: Array<string>, pattern: string) {
+  return hrefs.filter((href) => href.includes(pattern)).length
+}
+
+async function loadBuiltStartManifest() {
+  const serverDir = path.resolve(import.meta.dirname, '../.output/server')
+  const entries = await readdir(serverDir)
+  const manifestFile = entries.find(
+    (entry) =>
+      entry.startsWith('_tanstack-start-manifest_v-') && entry.endsWith('.mjs'),
+  )
+
+  expect(manifestFile).toBeTruthy()
+
+  const moduleUrl = `${pathToFileURL(path.join(serverDir, manifestFile!)).href}?t=${Date.now()}`
+  const manifestModule = await import(moduleUrl)
+
+  return manifestModule.tsrStartManifest() as {
+    routes: Record<string, { assets?: Array<unknown> }>
+  }
+}
+
+async function expectDirectEntry({
+  page,
+  request,
+  baseURL,
+  pathname,
+  expectedVisibleTestId,
+  expectedAbsentTestId,
+  expectedStylesheetPattern,
+  unexpectedStylesheetPattern,
+  expectedColorTestId,
+  expectedColor,
+}: {
+  page: Page
+  request: {
+    get: (url: string) => Promise<{ ok(): boolean; text(): Promise<string> }>
+  }
+  baseURL: string
+  pathname: string
+  expectedVisibleTestId: string
+  expectedAbsentTestId: string
+  expectedStylesheetPattern: string
+  unexpectedStylesheetPattern: string
+  expectedColorTestId: string
+  expectedColor: string
+}) {
+  const url = buildUrl(baseURL, pathname)
+  const response = await request.get(url)
+  const ssrHtml = await response.text()
+
+  expect(response.ok()).toBe(true)
+  expect(ssrHtml).toContain('root-shell-marker')
+  expect(ssrHtml).toContain(expectedVisibleTestId)
+  expect(ssrHtml).not.toContain(expectedAbsentTestId)
+
+  const stylesheetLinks = getStylesheetHrefsFromHtml(ssrHtml)
+
+  expect(
+    countMatchingStylesheetHrefs(stylesheetLinks, expectedStylesheetPattern),
+  ).toBe(1)
+  expect(
+    countMatchingStylesheetHrefs(stylesheetLinks, unexpectedStylesheetPattern),
+  ).toBe(0)
+  expect(stylesheetLinks).toHaveLength(2)
+
+  await page.goto(url)
+  await expect(page.getByTestId(expectedVisibleTestId)).toBeVisible()
+  await expect(page.getByTestId('hydration-marker')).toBeVisible()
+
+  await expect
+    .poll(async () => {
+      const hrefs = await getHeadStylesheetHrefs(page)
+      return countMatchingStylesheetHrefs(hrefs, expectedStylesheetPattern)
+    })
+    .toBe(1)
+  await expect
+    .poll(async () => {
+      const hrefs = await getHeadStylesheetHrefs(page)
+      return countMatchingStylesheetHrefs(hrefs, unexpectedStylesheetPattern)
+    })
+    .toBe(0)
+
+  await expect
+    .poll(() => getColor('root-shell-marker', page))
+    .toBe(ROOT_SHELL_COLOR)
+  await expect
+    .poll(() => getColor(expectedColorTestId, page))
+    .toBe(expectedColor)
+}
+
+test('SSR and client navigation keep CSS module styles correct without hydration errors', async ({
+  page,
+  baseURL,
+  request,
+}) => {
+  const routeOneUrl = buildUrl(baseURL!, '/r1')
+  const response = await request.get(routeOneUrl)
+  const ssrHtml = await response.text()
+
+  expect(response.ok()).toBe(true)
+  expect(ssrHtml).toContain('root-shell-marker')
+  expect(ssrHtml).toContain('route-r1-card')
+  expect(ssrHtml).not.toContain('route-r2-card')
+
+  const stylesheetLinks = getStylesheetHrefsFromHtml(ssrHtml)
+
+  expect(countMatchingStylesheetHrefs(stylesheetLinks, 'r1-')).toBe(1)
+  expect(countMatchingStylesheetHrefs(stylesheetLinks, 'r2-')).toBe(0)
+  expect(stylesheetLinks).toHaveLength(2)
+
+  await page.goto(routeOneUrl)
+
+  await expect(page.getByTestId('route-r1-card')).toBeVisible()
+  await expect(page.getByTestId('hydration-marker')).toBeVisible()
+
+  await expect
+    .poll(async () => {
+      const hrefs = await getHeadStylesheetHrefs(page)
+      return countMatchingStylesheetHrefs(hrefs, 'r1-')
+    })
+    .toBe(1)
+  await expect
+    .poll(async () => {
+      const hrefs = await getHeadStylesheetHrefs(page)
+      return countMatchingStylesheetHrefs(hrefs, 'r2-')
+    })
+    .toBe(0)
+
+  await expect
+    .poll(() => getColor('root-shell-marker', page))
+    .toBe(ROOT_SHELL_COLOR)
+  await expect.poll(() => getColor('route-r1-card', page)).toBe(ROUTE_ONE_COLOR)
+
+  await page.getByTestId('nav-/r2').click()
+  await page.waitForURL('**/r2')
+  await expect(page.getByTestId('route-r2-card')).toBeVisible()
+
+  await expect
+    .poll(async () => {
+      const hrefs = await getHeadStylesheetHrefs(page)
+      return countMatchingStylesheetHrefs(hrefs, 'r2-')
+    })
+    .toBe(1)
+
+  await expect
+    .poll(() => getColor('root-shell-marker', page))
+    .toBe(ROOT_SHELL_COLOR)
+  await expect.poll(() => getColor('route-r2-card', page)).toBe(ROUTE_TWO_COLOR)
+
+  await page.getByTestId('nav-/r1').click()
+  await page.waitForURL('**/r1')
+  await expect(page.getByTestId('route-r1-card')).toBeVisible()
+
+  await expect
+    .poll(async () => {
+      const hrefs = await getHeadStylesheetHrefs(page)
+      return countMatchingStylesheetHrefs(hrefs, 'r1-')
+    })
+    .toBe(1)
+
+  await expect
+    .poll(() => getColor('root-shell-marker', page))
+    .toBe(ROOT_SHELL_COLOR)
+  await expect.poll(() => getColor('route-r1-card', page)).toBe(ROUTE_ONE_COLOR)
+})
+
+test('direct SSR entry on /r2 only includes its stylesheet and hydrates cleanly', async ({
+  page,
+  baseURL,
+  request,
+}) => {
+  await expectDirectEntry({
+    page,
+    request,
+    baseURL: baseURL!,
+    pathname: '/r2',
+    expectedVisibleTestId: 'route-r2-card',
+    expectedAbsentTestId: 'route-r1-card',
+    expectedStylesheetPattern: 'r2-',
+    unexpectedStylesheetPattern: 'r1-',
+    expectedColorTestId: 'route-r2-card',
+    expectedColor: ROUTE_TWO_COLOR,
+  })
+})
+
+test('direct SSR entries on different route-css pages stay isolated', async ({
+  page,
+  baseURL,
+  request,
+}) => {
+  await expectDirectEntry({
+    page,
+    request,
+    baseURL: baseURL!,
+    pathname: '/r2',
+    expectedVisibleTestId: 'route-r2-card',
+    expectedAbsentTestId: 'route-r1-card',
+    expectedStylesheetPattern: 'r2-',
+    unexpectedStylesheetPattern: 'r1-',
+    expectedColorTestId: 'route-r2-card',
+    expectedColor: ROUTE_TWO_COLOR,
+  })
+
+  await expectDirectEntry({
+    page,
+    request,
+    baseURL: baseURL!,
+    pathname: '/r1',
+    expectedVisibleTestId: 'route-r1-card',
+    expectedAbsentTestId: 'route-r2-card',
+    expectedStylesheetPattern: 'r1-',
+    unexpectedStylesheetPattern: 'r2-',
+    expectedColorTestId: 'route-r1-card',
+    expectedColor: ROUTE_ONE_COLOR,
+  })
+})
+
+test('home route only renders the root stylesheet and no route-specific CSS', async ({
+  page,
+  baseURL,
+  request,
+}) => {
+  const homeUrl = buildUrl(baseURL!, '/')
+  const response = await request.get(homeUrl)
+  const ssrHtml = await response.text()
+
+  expect(response.ok()).toBe(true)
+  expect(ssrHtml).toContain('root-shell-marker')
+  expect(ssrHtml).toContain('home-copy')
+  expect(ssrHtml).not.toContain('route-r1-card')
+  expect(ssrHtml).not.toContain('route-r2-card')
+
+  const stylesheetLinks = getStylesheetHrefsFromHtml(ssrHtml)
+
+  expect(countMatchingStylesheetHrefs(stylesheetLinks, 'r1-')).toBe(0)
+  expect(countMatchingStylesheetHrefs(stylesheetLinks, 'r2-')).toBe(0)
+  expect(stylesheetLinks).toHaveLength(1)
+
+  await page.goto(homeUrl)
+  await expect(page.getByTestId('home-copy')).toBeVisible()
+  await expect(page.getByTestId('hydration-marker')).toBeVisible()
+
+  await expect
+    .poll(async () => {
+      const hrefs = await getHeadStylesheetHrefs(page)
+      return countMatchingStylesheetHrefs(hrefs, 'r1-')
+    })
+    .toBe(0)
+  await expect
+    .poll(async () => {
+      const hrefs = await getHeadStylesheetHrefs(page)
+      return countMatchingStylesheetHrefs(hrefs, 'r2-')
+    })
+    .toBe(0)
+
+  await expect
+    .poll(() => getColor('root-shell-marker', page))
+    .toBe(ROOT_SHELL_COLOR)
+})
+
+test('built start manifest preserves shared layout asset identity across sibling routes', async ({
+  page,
+  baseURL,
+}) => {
+  await page.goto(buildUrl(baseURL!, '/shared-a'))
+  await expect(page.getByTestId('shared-a-card')).toBeVisible()
+
+  const manifest = await loadBuiltStartManifest()
+
+  const sharedAAsset = manifest.routes['/shared-a']?.assets?.[0]
+  const sharedBAsset = manifest.routes['/shared-b']?.assets?.[0]
+  const sharedCAsset = manifest.routes['/shared-c']?.assets?.[0]
+
+  expect(sharedAAsset).toBeTruthy()
+  expect(sharedAAsset).toBe(sharedBAsset)
+  expect(sharedBAsset).toBe(sharedCAsset)
+})
diff --git a/e2e/react-start/start-manifest/tsconfig.json b/e2e/react-start/start-manifest/tsconfig.json
new file mode 100644
index 0000000000..cc479423a1
--- /dev/null
+++ b/e2e/react-start/start-manifest/tsconfig.json
@@ -0,0 +1,21 @@
+{
+  "include": ["**/*.ts", "**/*.tsx", "**/*.d.ts"],
+  "compilerOptions": {
+    "strict": true,
+    "esModuleInterop": true,
+    "jsx": "react-jsx",
+    "module": "ESNext",
+    "moduleResolution": "Bundler",
+    "lib": ["DOM", "DOM.Iterable", "ES2024"],
+    "isolatedModules": true,
+    "resolveJsonModule": true,
+    "skipLibCheck": true,
+    "target": "ES2024",
+    "allowJs": true,
+    "forceConsistentCasingInFileNames": true,
+    "paths": {
+      "~/*": ["./src/*"]
+    },
+    "noEmit": true
+  }
+}
diff --git a/e2e/react-start/start-manifest/vite.config.ts b/e2e/react-start/start-manifest/vite.config.ts
new file mode 100644
index 0000000000..b386068693
--- /dev/null
+++ b/e2e/react-start/start-manifest/vite.config.ts
@@ -0,0 +1,14 @@
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import { defineConfig } from 'vite'
+import viteReact from '@vitejs/plugin-react'
+import { nitro } from 'nitro/vite'
+
+export default defineConfig({
+  server: {
+    port: 3000,
+  },
+  resolve: {
+    tsconfigPaths: true,
+  },
+  plugins: [tanstackStart({ srcDirectory: 'src' }), viteReact(), nitro()],
+})
diff --git a/packages/router-core/src/ssr/ssr-server.ts b/packages/router-core/src/ssr/ssr-server.ts
index 412fe5730e..ec60544bda 100644
--- a/packages/router-core/src/ssr/ssr-server.ts
+++ b/packages/router-core/src/ssr/ssr-server.ts
@@ -173,10 +173,12 @@ export function attachRouterServerSsrUtils({
   router,
   manifest,
   getRequestAssets,
+  includeUnmatchedRouteAssets = true,
 }: {
   router: AnyRouter
   manifest: Manifest | undefined
   getRequestAssets?: () => Array<RouterManagedTag> | undefined
+  includeUnmatchedRouteAssets?: boolean
 }) {
   router.ssr = {
     get manifest() {
@@ -258,6 +260,7 @@ export function attachRouterServerSsrUtils({
             if (currentRouteIds.has(routeId)) {
               nextFilteredRoutes[routeId] = routeManifest
             } else if (
+              includeUnmatchedRouteAssets &&
               routeManifest.assets &&
               routeManifest.assets.length > 0
             ) {
diff --git a/packages/router-core/tests/ssr-server-manifest.test.ts b/packages/router-core/tests/ssr-server-manifest.test.ts
new file mode 100644
index 0000000000..62582e646b
--- /dev/null
+++ b/packages/router-core/tests/ssr-server-manifest.test.ts
@@ -0,0 +1,98 @@
+import { createMemoryHistory } from '@tanstack/history'
+import { BaseRootRoute, BaseRoute } from '../src'
+import { attachRouterServerSsrUtils } from '../src/ssr/ssr-server'
+import { createTestRouter } from './routerTestUtils'
+import { describe, expect, test } from 'vitest'
+import type { Manifest } from '../src/manifest'
+
+function buildRouter() {
+  const rootRoute = new BaseRootRoute({})
+  const indexRoute = new BaseRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    component: () => null,
+  })
+  const postsRoute = new BaseRoute({
+    getParentRoute: () => rootRoute,
+    path: '/posts',
+    component: () => null,
+  })
+
+  const routeTree = rootRoute.addChildren([indexRoute, postsRoute])
+
+  return createTestRouter({
+    routeTree,
+    history: createMemoryHistory({ initialEntries: ['/'] }),
+    isServer: true,
+  })
+}
+
+function buildManifest(): Manifest {
+  const sharedAsset = {
+    tag: 'link' as const,
+    attrs: {
+      rel: 'stylesheet',
+      href: '/assets/shared.css',
+      type: 'text/css',
+    },
+  }
+
+  return {
+    routes: {
+      __root__: {
+        assets: [sharedAsset],
+        preloads: ['/assets/root.js'],
+      },
+      '/': {
+        assets: [sharedAsset],
+        preloads: ['/assets/index.js'],
+      },
+      '/posts': {
+        assets: [sharedAsset],
+        preloads: ['/assets/posts.js'],
+      },
+    },
+  }
+}
+
+async function dehydrateManifest(options?: {
+  includeUnmatchedRouteAssets?: boolean
+}) {
+  const router = buildRouter()
+  const manifest = buildManifest()
+
+  attachRouterServerSsrUtils({
+    router,
+    manifest,
+    includeUnmatchedRouteAssets: options?.includeUnmatchedRouteAssets,
+  })
+
+  await router.load()
+  await router.serverSsr!.dehydrate()
+
+  const script = router.serverSsr!.takeBufferedScripts()
+  expect(script?.tag).toBe('script')
+  expect(script?.children).toContain('router=')
+
+  return script!.children!
+}
+
+describe('attachRouterServerSsrUtils manifest dehydration', () => {
+  test('includes unmatched route assets by default', async () => {
+    const serialized = await dehydrateManifest()
+
+    expect(serialized).toContain('"/posts":$R[')
+    expect(serialized).toContain('assets:$R[')
+    expect(serialized).not.toContain('"/assets/posts.js"')
+  })
+
+  test('omits unmatched route assets when disabled', async () => {
+    const serialized = await dehydrateManifest({
+      includeUnmatchedRouteAssets: false,
+    })
+
+    expect(serialized).not.toContain('"/posts":$R[')
+    expect(serialized).toContain('"/":$R[')
+    expect(serialized).toContain('"/assets/index.js"')
+  })
+})
diff --git a/packages/start-plugin-core/package.json b/packages/start-plugin-core/package.json
index bb45ac6924..e4a2665782 100644
--- a/packages/start-plugin-core/package.json
+++ b/packages/start-plugin-core/package.json
@@ -82,6 +82,7 @@
     "@tanstack/router-utils": "workspace:*",
     "@tanstack/start-client-core": "workspace:*",
     "@tanstack/start-server-core": "workspace:*",
+    "seroval": "^1.5.0",
     "cheerio": "^1.0.0",
     "exsolve": "^1.0.7",
     "pathe": "^2.0.3",
diff --git a/packages/start-plugin-core/src/start-manifest-plugin/manifestBuilder.ts b/packages/start-plugin-core/src/start-manifest-plugin/manifestBuilder.ts
index c85e050149..4a7be7e570 100644
--- a/packages/start-plugin-core/src/start-manifest-plugin/manifestBuilder.ts
+++ b/packages/start-plugin-core/src/start-manifest-plugin/manifestBuilder.ts
@@ -1,4 +1,5 @@
 /* eslint-disable @typescript-eslint/prefer-for-of */
+import { serialize } from 'seroval'
 import { joinURL } from 'ufo'
 import { resolveManifestAssetLink, rootRouteId } from '@tanstack/router-core'
 import {
@@ -41,6 +42,11 @@ type DedupeRoute = {
   children?: Array<string>
 }
 
+export interface StartManifest {
+  routes: Record<string, RouteTreeRoute>
+  clientEntry: string
+}
+
 export function appendUniqueStrings(
   target: Array<string> | undefined,
   source: Array<string>,
@@ -143,7 +149,7 @@ export function buildStartManifest(options: {
   clientBuild: NormalizedClientBuild
   routeTreeRoutes: RouteTreeRoutes
   basePath: string
-}) {
+}): StartManifest {
   const scannedChunks = scanClientChunks(options.clientBuild)
   const hashedCssFiles = collectDynamicImportCss(
     scannedChunks.routeEntryChunks,
@@ -181,6 +187,10 @@ export function buildStartManifest(options: {
   }
 }
 
+export function serializeStartManifest(startManifest: StartManifest) {
+  return serialize(startManifest)
+}
+
 export function scanClientChunks(
   clientBuild: NormalizedClientBuild,
 ): ScannedClientChunks {
diff --git a/packages/start-plugin-core/src/vite/start-manifest-plugin/plugin.ts b/packages/start-plugin-core/src/vite/start-manifest-plugin/plugin.ts
index 9b443fb073..a52414e359 100644
--- a/packages/start-plugin-core/src/vite/start-manifest-plugin/plugin.ts
+++ b/packages/start-plugin-core/src/vite/start-manifest-plugin/plugin.ts
@@ -2,7 +2,10 @@ import { joinURL } from 'ufo'
 import { VIRTUAL_MODULES } from '@tanstack/start-server-core'
 import { resolveViteId } from '../../utils'
 import { ENTRY_POINTS, START_ENVIRONMENT_NAMES } from '../../constants'
-import { buildStartManifest } from '../../start-manifest-plugin/manifestBuilder'
+import {
+  buildStartManifest,
+  serializeStartManifest,
+} from '../../start-manifest-plugin/manifestBuilder'
 import type { GetConfigFn, NormalizedClientBuild } from '../../types'
 import type { PluginOption } from 'vite'
 
@@ -59,7 +62,7 @@ export function startManifestPlugin(opts: {
             basePath: resolvedStartConfig.basePaths.publicBase,
           })
 
-          return `export const tsrStartManifest = () => (${JSON.stringify(startManifest)})`
+          return `export const tsrStartManifest = () => (${serializeStartManifest(startManifest)})`
         }
 
         return undefined
diff --git a/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts b/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts
index 66d04e4b03..613646d604 100644
--- a/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts
+++ b/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts
@@ -1,3 +1,4 @@
+import { deserialize } from 'seroval'
 import { describe, expect, test } from 'vitest'
 import {
   appendUniqueAssets,
@@ -9,7 +10,9 @@ import {
   getRouteFilePathsFromModuleIds,
   normalizeViteClientBuild,
   normalizeViteClientChunk,
+  serializeStartManifest,
   scanClientChunks,
+  type StartManifest,
 } from '../../src/start-manifest-plugin/manifestBuilder'
 import type { Rollup } from 'vite'
 
@@ -21,6 +24,10 @@ function normalizeTestChunk(chunk: Rollup.OutputChunk) {
   return normalizeViteClientChunk(chunk)
 }
 
+function deserializeSerializedManifest(serialized: string): StartManifest {
+  return deserialize(serialized) as StartManifest
+}
+
 function makeChunk(options: {
   fileName: string
   imports?: Array<string>
@@ -670,6 +677,90 @@ describe('buildStartManifest', () => {
       },
     ])
   })
+
+  test('serializeStartManifest preserves shared asset identity across routes', () => {
+    const sharedAsset = {
+      tag: 'link' as const,
+      attrs: {
+        rel: 'stylesheet',
+        href: '/assets/shared.css',
+        type: 'text/css',
+      },
+    }
+    const manifest: StartManifest = {
+      routes: {
+        __root__: {
+          children: ['/a', '/b', '/c'],
+        },
+        '/a': { assets: [sharedAsset], preloads: ['/assets/a.js'] },
+        '/b': { assets: [sharedAsset], preloads: ['/assets/b.js'] },
+        '/c': { assets: [sharedAsset], preloads: ['/assets/c.js'] },
+      },
+      clientEntry: '/assets/entry.js',
+    }
+
+    const evaluated = deserializeSerializedManifest(
+      serializeStartManifest(manifest),
+    )
+
+    expect(evaluated.routes['/a'].assets[0]).toBe(
+      evaluated.routes['/b'].assets[0],
+    )
+    expect(evaluated.routes['/b'].assets[0]).toBe(
+      evaluated.routes['/c'].assets[0],
+    )
+  })
+
+  test('serializeStartManifest preserves non-asset fields unchanged', () => {
+    const manifest: StartManifest = {
+      routes: {
+        __root__: {
+          children: ['/posts'],
+          preloads: ['/assets/root.js'],
+        },
+        '/posts': {
+          filePath: '/routes/posts.tsx',
+          children: ['/posts/$postId'],
+          preloads: ['/assets/posts.js'],
+          assets: [
+            {
+              tag: 'script' as const,
+              attrs: {
+                src: '/assets/posts.js',
+                type: 'module',
+              },
+              children: 'console.log("posts")',
+            },
+          ],
+        },
+      },
+      clientEntry: '/assets/entry.js',
+    }
+
+    expect(
+      deserializeSerializedManifest(serializeStartManifest(manifest)),
+    ).toEqual(manifest)
+  })
+
+  test('serializeStartManifest handles manifests without route assets', () => {
+    const manifest: StartManifest = {
+      routes: {
+        __root__: {
+          children: ['/posts'],
+          preloads: ['/assets/root.js'],
+        },
+        '/posts': {
+          filePath: '/routes/posts.tsx',
+          preloads: ['/assets/posts.js'],
+        },
+      },
+      clientEntry: '/assets/entry.js',
+    }
+
+    expect(
+      deserializeSerializedManifest(serializeStartManifest(manifest)),
+    ).toEqual(manifest)
+  })
 })
 
 describe('route tree dedupe in buildStartManifest', () => {
diff --git a/packages/start-server-core/src/createStartHandler.ts b/packages/start-server-core/src/createStartHandler.ts
index 9fd39d3d23..a666ed87c1 100644
--- a/packages/start-server-core/src/createStartHandler.ts
+++ b/packages/start-server-core/src/createStartHandler.ts
@@ -700,6 +700,7 @@ export function createStartHandler<TRegister = Register>(
           manifest,
           getRequestAssets: () =>
             getStartContext({ throwIfNotFound: false })?.requestAssets,
+          includeUnmatchedRouteAssets: false,
         })
 
         routerInstance.update({ additionalContext: { serverContext } })
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fb40791600..228528eda4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2857,6 +2857,49 @@ importers:
         specifier: ^8.0.0
         version: 8.0.0(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.6.1)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
 
+  e2e/react-start/start-manifest:
+    dependencies:
+      '@tanstack/react-router':
+        specifier: workspace:*
+        version: link:../../../packages/react-router
+      '@tanstack/react-start':
+        specifier: workspace:*
+        version: link:../../../packages/react-start
+      react:
+        specifier: ^19.2.3
+        version: 19.2.3
+      react-dom:
+        specifier: ^19.2.3
+        version: 19.2.3(react@19.2.3)
+    devDependencies:
+      '@playwright/test':
+        specifier: ^1.57.0
+        version: 1.58.0
+      '@tanstack/router-e2e-utils':
+        specifier: workspace:^
+        version: link:../../e2e-utils
+      '@types/node':
+        specifier: 25.0.9
+        version: 25.0.9
+      '@types/react':
+        specifier: ^19.2.8
+        version: 19.2.9
+      '@types/react-dom':
+        specifier: ^19.2.3
+        version: 19.2.3(@types/react@19.2.9)
+      '@vitejs/plugin-react':
+        specifier: ^6.0.1
+        version: 6.0.1(vite@8.0.0(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.6.1)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+      nitro:
+        specifier: ^3.0.260311-beta
+        version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.2.3)(giget@2.0.0)(jiti@2.6.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.0(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.6.1)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+      typescript:
+        specifier: ^6.0.2
+        version: 6.0.2
+      vite:
+        specifier: ^8.0.0
+        version: 8.0.0(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.6.1)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+
   e2e/react-start/static-server-functions:
     dependencies:
       '@tanstack/react-router':
@@ -12897,6 +12940,9 @@ importers:
       picomatch:
         specifier: ^4.0.3
         version: 4.0.3
+      seroval:
+        specifier: ^1.5.0
+        version: 1.5.0
       source-map:
         specifier: ^0.7.6
         version: 0.7.6

From 1c7146f412db92f71dc77bf55237251d82fe18a1 Mon Sep 17 00:00:00 2001
From: Manuel Schiller <manuel.schiller@caligano.de>
Date: Sat, 11 Apr 2026 20:01:11 +0200
Subject: [PATCH 2/3] review

---
 packages/router-core/src/ssr/ssr-server.ts                | 8 +++++---
 .../tests/start-manifest-plugin/manifestBuilder.test.ts   | 2 +-
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/packages/router-core/src/ssr/ssr-server.ts b/packages/router-core/src/ssr/ssr-server.ts
index ec60544bda..3851d0339b 100644
--- a/packages/router-core/src/ssr/ssr-server.ts
+++ b/packages/router-core/src/ssr/ssr-server.ts
@@ -238,12 +238,14 @@ export function attachRouterServerSsrUtils({
       const matches = matchesToDehydrate.map(dehydrateMatch)
 
       let manifestToDehydrate: Manifest | undefined = undefined
-      // For currently matched routes, send full manifest (preloads + assets)
-      // For all other routes, only send assets (no preloads as they are handled via dynamic imports)
+      // For currently matched routes, send full manifest (preloads + assets).
+      // For unmatched routes, include assets only when includeUnmatchedRouteAssets
+      // is true; otherwise omit them entirely. Preloads for unmatched routes are
+      // still excluded because they are handled via dynamic imports.
       if (manifest) {
         // Prod-only caching; in dev manifests may be replaced/updated (HMR)
         const currentRouteIdsList = matchesToDehydrate.map((m) => m.routeId)
-        const manifestCacheKey = currentRouteIdsList.join('\0')
+        const manifestCacheKey = `${currentRouteIdsList.join('\0')}\0includeUnmatchedRouteAssets=${includeUnmatchedRouteAssets}`
 
         let filteredRoutes: FilteredRoutes | undefined
 
diff --git a/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts b/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts
index 613646d604..9cdb00d8e2 100644
--- a/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts
+++ b/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts
@@ -1,5 +1,5 @@
-import { deserialize } from 'seroval'
 import { describe, expect, test } from 'vitest'
+import { deserialize } from 'seroval'
 import {
   appendUniqueAssets,
   appendUniqueStrings,

From 8f5b695fb1d14da1f084ab2c8253b448ad04537f Mon Sep 17 00:00:00 2001
From: "nx-cloud[bot]" <71083854+nx-cloud[bot]@users.noreply.github.com>
Date: Sat, 11 Apr 2026 18:12:24 +0000
Subject: [PATCH 3/3] fix(start-plugin-core): guard optional assets access in
 type tests

Co-authored-by: schiller-manuel <schiller-manuel@users.noreply.github.com>
---
 .../start-manifest-plugin/manifestBuilder.test.ts   | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts b/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts
index 9cdb00d8e2..d736a43698 100644
--- a/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts
+++ b/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts
@@ -703,12 +703,13 @@ describe('buildStartManifest', () => {
       serializeStartManifest(manifest),
     )
 
-    expect(evaluated.routes['/a'].assets[0]).toBe(
-      evaluated.routes['/b'].assets[0],
-    )
-    expect(evaluated.routes['/b'].assets[0]).toBe(
-      evaluated.routes['/c'].assets[0],
-    )
+    const aAsset = evaluated.routes['/a']?.assets?.[0]
+    const bAsset = evaluated.routes['/b']?.assets?.[0]
+    const cAsset = evaluated.routes['/c']?.assets?.[0]
+
+    expect(aAsset).toBeDefined()
+    expect(aAsset).toBe(bAsset)
+    expect(bAsset).toBe(cAsset)
   })
 
   test('serializeStartManifest preserves non-asset fields unchanged', () => {
