Repository: TanStack/router
Pull request #2996 (2024-12-12 22:28:01)

feat(react-router): implement hash change "Scroll Into View" option
I am working on an API documentation page that has a number of anchor links. The sidebar contains `Link` elements that expand/collapse sections based on if its hash is active. It works like a table of contents. I also update the hash based on the user's scroll position. If they've scrolled past an anchor, it updates the hash. The issue I was hitting is that updating the hash with `navigate` or a number of other methods actually runs `scrollIntoView`, which takes over the user's scrolling in these cases.

The change I'm proposing is to add an option to `navigate` and `Link` that will allow the consumer to choose to opt out of the default `scrollIntoView` behavior, or even allow them to customize the scroll behavior if they would like to (e.g., for smooth scrolling instead of instant).

Discussed in Discord here: https://discord.com/channels/719702312431386674/1316559407919796334

Another relevant discussion: https://github.com/TanStack/router/discussions/1155


Patch:
diff --git a/docs/framework/react/api/router/NavigateOptionsType.md b/docs/framework/react/api/router/NavigateOptionsType.md
index f832d650b..ce917ffdc 100644
--- a/docs/framework/react/api/router/NavigateOptionsType.md
+++ b/docs/framework/react/api/router/NavigateOptionsType.md
@@ -9,8 +9,49 @@ The `NavigateOptions` type is used to describe the options that can be used when
 type NavigateOptions = ToOptions & {
   replace?: boolean
   resetScroll?: boolean
+  hashScrollIntoView?: boolean | ScrollIntoViewOptions
   ignoreBlocker?: boolean
+  reloadDocument?: boolean
+  href?: string
 }
 ```
 
+## NavigateOptions properties
+
+The `NavigateOptions` object accepts the following properties:
+
+### `replace`
+
+- Type: `boolean`
+- Optional
+- Defaults to `false`.
+- If `true`, the location will be committed to the browser history using `history.replace` instead of `history.push`.
+
+### `resetScroll`
+
+- Type: `boolean`
+- Optional
+- Defaults to `true` so that the scroll position will be reset to 0,0 after the location is committed to the browser history.
+- If `false`, the scroll position will not be reset to 0,0 after the location is committed to history.
+
+### `ignoreBlocker`
+
+- Type: `boolean`
+- Optional
+- Defaults to `false`.
+- If `true`, navigation will ignore any blockers that might prevent it.
+
+### `reloadDocument`
+
+- Type: `boolean`
+- Optional
+- Defaults to `false`.
+- If `true`, navigation to a route inside of router will trigger a full page load instead of the traditional SPA navigation.
+
+### `href`
+
+- Type: `string`
+- Optional
+- This can be used instead of `to` to navigate to a fully built href, e.g. pointing to an external target.
+
 - [`ToOptions`](./ToOptionsType.md)
diff --git a/docs/framework/react/api/router/RedirectType.md b/docs/framework/react/api/router/RedirectType.md
index b8570dd2b..b1727a068 100644
--- a/docs/framework/react/api/router/RedirectType.md
+++ b/docs/framework/react/api/router/RedirectType.md
@@ -7,8 +7,9 @@ The `Redirect` type is used to represent a redirect action in TanStack Router.
 
 ```tsx
 export type Redirect = {
-  code?: number
+  statusCode?: number
   throw?: any
+  headers?: HeadersInit
 } & NavigateOptions
 ```
 
@@ -18,7 +19,7 @@ export type Redirect = {
 
 The `Redirect` object accepts/contains the following properties:
 
-### `code` property
+### `statusCode` property
 
 - Type: `number`
 - Optional
@@ -29,3 +30,9 @@ The `Redirect` object accepts/contains the following properties:
 - Type: `any`
 - Optional
 - If provided, will throw the redirect object instead of returning it. This can be useful in places where `throwing` in a function might cause it to have a return type of `never`. In that case, you can use `redirect({ throw: true })` to throw the redirect object instead of returning it.
+
+### `headers` property
+
+- Type: `HeadersInit`
+- Optional
+- The HTTP headers to use when redirecting.
diff --git a/docs/framework/react/api/router/RouterOptionsType.md b/docs/framework/react/api/router/RouterOptionsType.md
index 1244a0b74..d3c6f4ad9 100644
--- a/docs/framework/react/api/router/RouterOptionsType.md
+++ b/docs/framework/react/api/router/RouterOptionsType.md
@@ -148,6 +148,15 @@ The `RouterOptions` type accepts an object with the following properties and met
 - See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition) for more information on how this function works.
 - See [Google](https://developer.chrome.com/docs/web-platform/view-transitions/same-document#view-transition-types) for more informations on viewTransition types
 
+### `defaultHashScrollIntoView` property
+
+- Type: `boolean | ScrollIntoViewOptions`
+- Optional
+- Defaults to `true` so the element with an id matching the hash will be scrolled into view after the location is committed to history.
+- If `false`, the element with an id matching the hash will not be scrolled into view after the location is committed to history.
+- If an object is provided, it will be passed to the `scrollIntoView` method as options.
+- See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) for more information on `ScrollIntoViewOptions`.
+
 ### `caseSensitive` property
 
 - Type: `boolean`
diff --git a/docs/framework/react/api/router/RouterType.md b/docs/framework/react/api/router/RouterType.md
index 22c22d38f..b99e9fb79 100644
--- a/docs/framework/react/api/router/RouterType.md
+++ b/docs/framework/react/api/router/RouterType.md
@@ -93,6 +93,7 @@ Commits a new location object to the browser history.
     location: ParsedLocation & {
       replace?: boolean
       resetScroll?: boolean
+      hashScrollIntoView?: boolean | ScrollIntoViewOptions
       ignoreBlocker?: boolean
     },
   ) => Promise<void>
@@ -112,6 +113,13 @@ Commits a new location object to the browser history.
     - Optional
     - Defaults to `true` so that the scroll position will be reset to 0,0 after the location is committed to the browser history.
     - If `false`, the scroll position will not be reset to 0,0 after the location is committed to history.
+  - `hashScrollIntoView`
+    - Type: `boolean | ScrollIntoViewOptions`
+    - Optional
+    - Defaults to `true` so the element with an id matching the hash will be scrolled into view after the location is committed to history.
+    - If `false`, the element with an id matching the hash will not be scrolled into view after the location is committed to history.
+    - If an object is provided, it will be passed to the `scrollIntoView` method as options.
+    - See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) for more information on `ScrollIntoViewOptions`.
   - `ignoreBlocker`
     - Type: `boolean`
     - Optional
diff --git a/docs/framework/react/comparison.md b/docs/framework/react/comparison.md
index b3b358ef6..72c4b2769 100644
--- a/docs/framework/react/comparison.md
+++ b/docs/framework/react/comparison.md
@@ -72,10 +72,10 @@ Feature/Capability Key:
 [stars-tanstack-router]: https://img.shields.io/github/stars/tanstack/router?label=%F0%9F%8C%9F
 [_]: _
 [router]: https://github.com/remix-run/react-router
-[bp-router]: https://badgen.net/bundlephobia/minzip/react-router-dom
+[bp-router]: https://badgen.net/bundlephobia/minzip/react-router
 [gh-router]: https://github.com/remix-run/react-router
 [stars-router]: https://img.shields.io/github/stars/remix-run/react-router?label=%F0%9F%8C%9F
-[bpl-router]: https://bundlephobia.com/result?p=react-router-dom
+[bpl-router]: https://bundlephobia.com/result?p=react-router
 [bpl-history]: https://bundlephobia.com/result?p=history
 [_]: _
 [nextjs]: https://nextjs.org/docs/routing/introduction
diff --git a/docs/framework/react/start/server-functions.md b/docs/framework/react/start/server-functions.md
index d6106ac3a..63281e4d0 100644
--- a/docs/framework/react/start/server-functions.md
+++ b/docs/framework/react/start/server-functions.md
@@ -578,6 +578,20 @@ export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
 })
 ```
 
+You can also redirect to an external target using `href`:
+
+```tsx
+import { redirect } from '@tanstack/react-router'
+import { createServerFn } from '@tanstack/start'
+
+export const auth = createServerFn({ method: 'GET' }).handler(async () => {
+  // Redirect the user to the auth provider
+  throw redirect({
+    href: 'https://authprovider.com/login',
+  })
+})
+```
+
 > ⚠️ Do not use Vinxi's `sendRedirect` function to send soft redirects from within server functions. This will send the redirect using the `Location` header and will force a full page hard navigation on the client.
 
 ## Redirect Headers
diff --git a/examples/react/authenticated-routes/package.json b/examples/react/authenticated-routes/package.json
index 72e4faff8..293db8005 100644
--- a/examples/react/authenticated-routes/package.json
+++ b/examples/react/authenticated-routes/package.json
@@ -9,9 +9,9 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1",
diff --git a/examples/react/basic-default-search-params/package.json b/examples/react/basic-default-search-params/package.json
index 42c7c6428..ad4ba9a5b 100644
--- a/examples/react/basic-default-search-params/package.json
+++ b/examples/react/basic-default-search-params/package.json
@@ -10,8 +10,8 @@
   },
   "dependencies": {
     "@tanstack/react-query": "^5.62.3",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
     "redaxios": "^0.5.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
diff --git a/examples/react/basic-file-based-codesplitting/package.json b/examples/react/basic-file-based-codesplitting/package.json
index df61b6dcf..5752a911d 100644
--- a/examples/react/basic-file-based-codesplitting/package.json
+++ b/examples/react/basic-file-based-codesplitting/package.json
@@ -9,9 +9,9 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1",
diff --git a/examples/react/basic-file-based/package.json b/examples/react/basic-file-based/package.json
index dc59e7d88..0f34d8e3c 100644
--- a/examples/react/basic-file-based/package.json
+++ b/examples/react/basic-file-based/package.json
@@ -9,9 +9,9 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1",
diff --git a/examples/react/basic-file-based/src/routeTree.gen.ts b/examples/react/basic-file-based/src/routeTree.gen.ts
index 375fe8dc9..0c13b4bff 100644
--- a/examples/react/basic-file-based/src/routeTree.gen.ts
+++ b/examples/react/basic-file-based/src/routeTree.gen.ts
@@ -12,6 +12,7 @@
 
 import { Route as rootRoute } from './routes/__root'
 import { Route as PostsImport } from './routes/posts'
+import { Route as AnchorImport } from './routes/anchor'
 import { Route as LayoutImport } from './routes/_layout'
 import { Route as IndexImport } from './routes/index'
 import { Route as PostsIndexImport } from './routes/posts.index'
@@ -28,6 +29,12 @@ const PostsRoute = PostsImport.update({
   getParentRoute: () => rootRoute,
 } as any)
 
+const AnchorRoute = AnchorImport.update({
+  id: '/anchor',
+  path: '/anchor',
+  getParentRoute: () => rootRoute,
+} as any)
+
 const LayoutRoute = LayoutImport.update({
   id: '/_layout',
   getParentRoute: () => rootRoute,
@@ -86,6 +93,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof LayoutImport
       parentRoute: typeof rootRoute
     }
+    '/anchor': {
+      id: '/anchor'
+      path: '/anchor'
+      fullPath: '/anchor'
+      preLoaderRoute: typeof AnchorImport
+      parentRoute: typeof rootRoute
+    }
     '/posts': {
       id: '/posts'
       path: '/posts'
@@ -173,6 +187,7 @@ const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren)
 export interface FileRoutesByFullPath {
   '/': typeof IndexRoute
   '': typeof LayoutLayout2RouteWithChildren
+  '/anchor': typeof AnchorRoute
   '/posts': typeof PostsRouteWithChildren
   '/posts/$postId': typeof PostsPostIdRoute
   '/posts/': typeof PostsIndexRoute
@@ -183,6 +198,7 @@ export interface FileRoutesByFullPath {
 export interface FileRoutesByTo {
   '/': typeof IndexRoute
   '': typeof LayoutLayout2RouteWithChildren
+  '/anchor': typeof AnchorRoute
   '/posts/$postId': typeof PostsPostIdRoute
   '/posts': typeof PostsIndexRoute
   '/layout-a': typeof LayoutLayout2LayoutARoute
@@ -193,6 +209,7 @@ export interface FileRoutesById {
   __root__: typeof rootRoute
   '/': typeof IndexRoute
   '/_layout': typeof LayoutRouteWithChildren
+  '/anchor': typeof AnchorRoute
   '/posts': typeof PostsRouteWithChildren
   '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
   '/posts/$postId': typeof PostsPostIdRoute
@@ -206,17 +223,26 @@ export interface FileRouteTypes {
   fullPaths:
     | '/'
     | ''
+    | '/anchor'
     | '/posts'
     | '/posts/$postId'
     | '/posts/'
     | '/layout-a'
     | '/layout-b'
   fileRoutesByTo: FileRoutesByTo
-  to: '/' | '' | '/posts/$postId' | '/posts' | '/layout-a' | '/layout-b'
+  to:
+    | '/'
+    | ''
+    | '/anchor'
+    | '/posts/$postId'
+    | '/posts'
+    | '/layout-a'
+    | '/layout-b'
   id:
     | '__root__'
     | '/'
     | '/_layout'
+    | '/anchor'
     | '/posts'
     | '/_layout/_layout-2'
     | '/posts/$postId'
@@ -229,12 +255,14 @@ export interface FileRouteTypes {
 export interface RootRouteChildren {
   IndexRoute: typeof IndexRoute
   LayoutRoute: typeof LayoutRouteWithChildren
+  AnchorRoute: typeof AnchorRoute
   PostsRoute: typeof PostsRouteWithChildren
 }
 
 const rootRouteChildren: RootRouteChildren = {
   IndexRoute: IndexRoute,
   LayoutRoute: LayoutRouteWithChildren,
+  AnchorRoute: AnchorRoute,
   PostsRoute: PostsRouteWithChildren,
 }
 
@@ -250,6 +278,7 @@ export const routeTree = rootRoute
       "children": [
         "/",
         "/_layout",
+        "/anchor",
         "/posts"
       ]
     },
@@ -262,6 +291,9 @@ export const routeTree = rootRoute
         "/_layout/_layout-2"
       ]
     },
+    "/anchor": {
+      "filePath": "anchor.tsx"
+    },
     "/posts": {
       "filePath": "posts.tsx",
       "children": [
diff --git a/examples/react/basic-file-based/src/routes/__root.tsx b/examples/react/basic-file-based/src/routes/__root.tsx
index 6b57d1e23..eaa4ec9ea 100644
--- a/examples/react/basic-file-based/src/routes/__root.tsx
+++ b/examples/react/basic-file-based/src/routes/__root.tsx
@@ -43,6 +43,14 @@ function RootComponent() {
         >
           Layout
         </Link>{' '}
+        <Link
+          to="/anchor"
+          activeProps={{
+            className: 'font-bold',
+          }}
+        >
+          Anchor
+        </Link>{' '}
         <Link
           // @ts-expect-error
           to="/this-route-does-not-exist"
diff --git a/examples/react/basic-file-based/src/routes/anchor.tsx b/examples/react/basic-file-based/src/routes/anchor.tsx
new file mode 100644
index 000000000..2faa82f25
--- /dev/null
+++ b/examples/react/basic-file-based/src/routes/anchor.tsx
@@ -0,0 +1,210 @@
+import { useLayoutEffect, useRef, useState } from 'react'
+import {
+  Link,
+  createFileRoute,
+  useLocation,
+  useNavigate,
+} from '@tanstack/react-router'
+
+export const Route = createFileRoute('/anchor')({
+  component: AnchorComponent,
+})
+
+const anchors: Array<{
+  id: string
+  title: string
+  hashScrollIntoView?: boolean | ScrollIntoViewOptions
+}> = [
+  {
+    id: 'default-anchor',
+    title: 'Default Anchor',
+  },
+  {
+    id: 'false-anchor',
+    title: 'No Scroll Into View',
+    hashScrollIntoView: false,
+  },
+  {
+    id: 'smooth-scroll',
+    title: 'Smooth Scroll',
+    hashScrollIntoView: { behavior: 'smooth' },
+  },
+] as const
+
+function AnchorSection({ id, title }: { id: string; title: string }) {
+  const [hasShown, setHasShown] = useState(false)
+  const elementRef = useRef<HTMLHeadingElement>(null)
+
+  useLayoutEffect(() => {
+    const observer = new IntersectionObserver(
+      ([entry]) => {
+        if (!hasShown && entry.isIntersecting) {
+          setHasShown(true)
+        }
+      },
+      { threshold: 0.01 },
+    )
+
+    const currentRef = elementRef.current
+    if (currentRef) {
+      observer.observe(currentRef)
+    }
+
+    return () => {
+      if (currentRef) {
+        observer.unobserve(currentRef)
+      }
+    }
+  }, [hasShown])
+
+  return (
+    <div id={id} className="p-2 min-h-dvh">
+      <h1 className="font-bold text-xl pt-10" ref={elementRef}>
+        {title}
+        {hasShown ? ' (shown)' : ''}
+      </h1>
+    </div>
+  )
+}
+
+function AnchorComponent() {
+  const navigate = useNavigate()
+  const location = useLocation()
+  const [withScroll, setWithScroll] = useState(true)
+
+  return (
+    <div className="flex flex-col w-full">
+      <nav className="sticky top-0 z-10 p-2 bg-gray-50 dark:bg-gray-900 border-b">
+        <ul className="inline-flex gap-2">
+          {anchors.map((anchor) => (
+            <li key={anchor.id}>
+              <Link
+                hash={anchor.id}
+                activeOptions={{ includeHash: true }}
+                activeProps={{
+                  className: 'font-bold active',
+                }}
+                hashScrollIntoView={anchor.hashScrollIntoView}
+              >
+                {anchor.title}
+              </Link>
+            </li>
+          ))}
+        </ul>
+      </nav>
+      <main className="overflow-auto">
+        <form
+          className="p-2 space-y-2 min-h-dvh"
+          onSubmit={(event) => {
+            event.preventDefault()
+            event.stopPropagation()
+            const formData = new FormData(event.target as HTMLFormElement)
+
+            const toHash = formData.get('hash') as string
+
+            if (!toHash) {
+              return
+            }
+
+            const hashScrollIntoView = withScroll
+              ? ({
+                  behavior: formData.get('scrollBehavior') as ScrollBehavior,
+                  block: formData.get('scrollBlock') as ScrollLogicalPosition,
+                  inline: formData.get('scrollInline') as ScrollLogicalPosition,
+                } satisfies ScrollIntoViewOptions)
+              : false
+
+            navigate({ hash: toHash, hashScrollIntoView })
+          }}
+        >
+          <h1 className="font-bold text-xl">Scroll with navigate</h1>
+          <div className="space-y-2">
+            <label>
+              <span>Target Anchor</span>
+              <select
+                className="border border-opacity-50 rounded p-2 w-full"
+                defaultValue={location.hash || anchors[0].id}
+                name="hash"
+              >
+                {anchors.map((anchor) => (
+                  <option key={anchor.id} value={anchor.id}>
+                    {anchor.title}
+                  </option>
+                ))}
+              </select>
+            </label>
+            <div>
+              <label>
+                <input
+                  checked={withScroll}
+                  onChange={(e) => setWithScroll(e.target.checked)}
+                  type="checkbox"
+                />{' '}
+                Scroll Into View
+              </label>
+            </div>
+          </div>
+          {withScroll ? (
+            <>
+              <div className="space-y-2">
+                <label>
+                  <span>Behavior</span>
+                  <select
+                    className="border border-opacity-50 rounded p-2 w-full"
+                    defaultValue="instant"
+                    name="scrollBehavior"
+                  >
+                    <option value="instant">instant</option>
+                    <option value="smooth">smooth</option>
+                    <option value="auto">auto</option>
+                  </select>
+                </label>
+              </div>
+
+              <div className="space-y-2">
+                <label>
+                  <span>Block</span>
+                  <select
+                    className="border border-opacity-50 rounded p-2 w-full"
+                    defaultValue="start"
+                    name="scrollBlock"
+                  >
+                    <option value="start">start</option>
+                    <option value="center">center</option>
+                    <option value="end">end</option>
+                    <option value="nearest">nearest</option>
+                  </select>
+                </label>
+              </div>
+
+              <div className="space-y-2">
+                <label>
+                  <span>Inline</span>
+                  <select
+                    className="border border-opacity-50 rounded p-2 w-full"
+                    defaultValue="nearest"
+                    name="scrollInline"
+                  >
+                    <option value="start">start</option>
+                    <option value="center">center</option>
+                    <option value="end">end</option>
+                    <option value="nearest">nearest</option>
+                  </select>
+                </label>
+              </div>
+            </>
+          ) : null}
+          <div>
+            <button className="bg-blue-500 rounded p-2 uppercase text-white font-black disabled:opacity-50">
+              Navigate
+            </button>
+          </div>
+        </form>
+
+        {anchors.map((anchor) => (
+          <AnchorSection key={anchor.id} id={anchor.id} title={anchor.title} />
+        ))}
+      </main>
+    </div>
+  )
+}
diff --git a/examples/react/basic-react-query-file-based/package.json b/examples/react/basic-react-query-file-based/package.json
index a9ff5b494..3ee162dd5 100644
--- a/examples/react/basic-react-query-file-based/package.json
+++ b/examples/react/basic-react-query-file-based/package.json
@@ -11,9 +11,9 @@
   "dependencies": {
     "@tanstack/react-query": "^5.62.3",
     "@tanstack/react-query-devtools": "^5.62.3",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1",
diff --git a/examples/react/basic-react-query/package.json b/examples/react/basic-react-query/package.json
index c6e3d8833..71ef56240 100644
--- a/examples/react/basic-react-query/package.json
+++ b/examples/react/basic-react-query/package.json
@@ -11,8 +11,8 @@
   "dependencies": {
     "@tanstack/react-query": "^5.62.3",
     "@tanstack/react-query-devtools": "^5.62.3",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1"
diff --git a/examples/react/basic-ssr-file-based/package.json b/examples/react/basic-ssr-file-based/package.json
index 88621be0c..bf5f93447 100644
--- a/examples/react/basic-ssr-file-based/package.json
+++ b/examples/react/basic-ssr-file-based/package.json
@@ -11,10 +11,10 @@
     "debug": "node --inspect-brk server"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
-    "@tanstack/start": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
+    "@tanstack/start": "^1.88.0",
     "get-port": "^7.1.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
diff --git a/examples/react/basic-ssr-streaming-file-based/package.json b/examples/react/basic-ssr-streaming-file-based/package.json
index 645e4ef15..bc1a8a27d 100644
--- a/examples/react/basic-ssr-streaming-file-based/package.json
+++ b/examples/react/basic-ssr-streaming-file-based/package.json
@@ -11,10 +11,10 @@
     "debug": "node --inspect-brk server"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
-    "@tanstack/start": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
+    "@tanstack/start": "^1.88.0",
     "get-port": "^7.1.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
diff --git a/examples/react/basic-virtual-file-based/package.json b/examples/react/basic-virtual-file-based/package.json
index aab5d20c8..7b2a51697 100644
--- a/examples/react/basic-virtual-file-based/package.json
+++ b/examples/react/basic-virtual-file-based/package.json
@@ -9,9 +9,9 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
     "@tanstack/virtual-file-routes": "^1.87.6",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
diff --git a/examples/react/basic-virtual-inside-file-based/package.json b/examples/react/basic-virtual-inside-file-based/package.json
index 7f7307478..26bc3d007 100644
--- a/examples/react/basic-virtual-inside-file-based/package.json
+++ b/examples/react/basic-virtual-inside-file-based/package.json
@@ -9,9 +9,9 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
     "@tanstack/virtual-file-routes": "^1.87.6",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
diff --git a/examples/react/basic/package.json b/examples/react/basic/package.json
index 3e583dda5..8da9524f8 100644
--- a/examples/react/basic/package.json
+++ b/examples/react/basic/package.json
@@ -9,8 +9,8 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1"
diff --git a/examples/react/deferred-data/package.json b/examples/react/deferred-data/package.json
index fee389970..21de1228b 100644
--- a/examples/react/deferred-data/package.json
+++ b/examples/react/deferred-data/package.json
@@ -9,8 +9,8 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
     "redaxios": "^0.5.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
diff --git a/examples/react/kitchen-sink-file-based/package.json b/examples/react/kitchen-sink-file-based/package.json
index 778e10a7d..95d4d68e6 100644
--- a/examples/react/kitchen-sink-file-based/package.json
+++ b/examples/react/kitchen-sink-file-based/package.json
@@ -9,9 +9,9 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
     "immer": "^10.1.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
diff --git a/examples/react/kitchen-sink-react-query-file-based/package.json b/examples/react/kitchen-sink-react-query-file-based/package.json
index 238036e6c..8be224d51 100644
--- a/examples/react/kitchen-sink-react-query-file-based/package.json
+++ b/examples/react/kitchen-sink-react-query-file-based/package.json
@@ -11,9 +11,9 @@
   "dependencies": {
     "@tanstack/react-query": "^5.62.3",
     "@tanstack/react-query-devtools": "^5.62.3",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
     "immer": "^10.1.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
diff --git a/examples/react/kitchen-sink-react-query/package.json b/examples/react/kitchen-sink-react-query/package.json
index b5de75724..57ab5e8fa 100644
--- a/examples/react/kitchen-sink-react-query/package.json
+++ b/examples/react/kitchen-sink-react-query/package.json
@@ -11,8 +11,8 @@
   "dependencies": {
     "@tanstack/react-query": "^5.62.3",
     "@tanstack/react-query-devtools": "^5.62.3",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
     "redaxios": "^0.5.1",
     "immer": "^10.1.1",
     "react": "^18.2.0",
diff --git a/examples/react/kitchen-sink/package.json b/examples/react/kitchen-sink/package.json
index a5631efa4..d3060530c 100644
--- a/examples/react/kitchen-sink/package.json
+++ b/examples/react/kitchen-sink/package.json
@@ -9,8 +9,8 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
     "redaxios": "^0.5.1",
     "immer": "^10.1.1",
     "react": "^18.2.0",
diff --git a/examples/react/large-file-based/package.json b/examples/react/large-file-based/package.json
index 628d8e07f..e67b3afd0 100644
--- a/examples/react/large-file-based/package.json
+++ b/examples/react/large-file-based/package.json
@@ -12,9 +12,9 @@
   },
   "dependencies": {
     "@tanstack/react-query": "^5.62.3",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1",
diff --git a/examples/react/location-masking/package.json b/examples/react/location-masking/package.json
index 723ac1053..e4092f7e6 100644
--- a/examples/react/location-masking/package.json
+++ b/examples/react/location-masking/package.json
@@ -11,8 +11,8 @@
   "dependencies": {
     "@radix-ui/react-dialog": "^1.1.2",
     "@tanstack/react-query": "^5.62.3",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
     "redaxios": "^0.5.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
diff --git a/examples/react/navigation-blocking/package.json b/examples/react/navigation-blocking/package.json
index 753a780c0..5bca3b50e 100644
--- a/examples/react/navigation-blocking/package.json
+++ b/examples/react/navigation-blocking/package.json
@@ -10,8 +10,8 @@
   },
   "dependencies": {
     "@tanstack/react-query": "^5.62.3",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
     "redaxios": "^0.5.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
diff --git a/examples/react/quickstart-esbuild-file-based/package.json b/examples/react/quickstart-esbuild-file-based/package.json
index 8686594db..9e3d8fb49 100644
--- a/examples/react/quickstart-esbuild-file-based/package.json
+++ b/examples/react/quickstart-esbuild-file-based/package.json
@@ -9,9 +9,9 @@
     "start": "dev"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1",
diff --git a/examples/react/quickstart-file-based/package.json b/examples/react/quickstart-file-based/package.json
index 92b2be0be..ea343a6c6 100644
--- a/examples/react/quickstart-file-based/package.json
+++ b/examples/react/quickstart-file-based/package.json
@@ -9,9 +9,9 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1",
diff --git a/examples/react/quickstart-rspack-file-based/package.json b/examples/react/quickstart-rspack-file-based/package.json
index de088e494..98bd0ca10 100644
--- a/examples/react/quickstart-rspack-file-based/package.json
+++ b/examples/react/quickstart-rspack-file-based/package.json
@@ -8,15 +8,15 @@
     "preview": "rsbuild preview"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
     "react": "^18.3.1",
     "react-dom": "^18.3.1"
   },
   "devDependencies": {
     "@rsbuild/core": "1.1.9",
     "@rsbuild/plugin-react": "1.1.0",
-    "@tanstack/router-plugin": "^1.87.11",
+    "@tanstack/router-plugin": "^1.87.13",
     "@types/react": "^18.3.3",
     "@types/react-dom": "^18.3.0",
     "typescript": "^5.6.2"
diff --git a/examples/react/quickstart-webpack-file-based/package.json b/examples/react/quickstart-webpack-file-based/package.json
index 3804f5bd5..27ee965ea 100644
--- a/examples/react/quickstart-webpack-file-based/package.json
+++ b/examples/react/quickstart-webpack-file-based/package.json
@@ -7,14 +7,14 @@
     "build": "webpack build && tsc --noEmit"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
     "react": "^18.3.1",
     "react-dom": "^18.3.1"
   },
   "devDependencies": {
     "@swc/core": "^1.10.1",
-    "@tanstack/router-plugin": "^1.87.11",
+    "@tanstack/router-plugin": "^1.87.13",
     "@types/react": "^18.3.3",
     "@types/react-dom": "^18.3.0",
     "html-webpack-plugin": "^5.6.3",
diff --git a/examples/react/quickstart/package.json b/examples/react/quickstart/package.json
index c789e981c..6c02e6f15 100644
--- a/examples/react/quickstart/package.json
+++ b/examples/react/quickstart/package.json
@@ -9,8 +9,8 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
   },
diff --git a/examples/react/router-monorepo-react-query/package.json b/examples/react/router-monorepo-react-query/package.json
index 498980ba8..7c294e41e 100644
--- a/examples/react/router-monorepo-react-query/package.json
+++ b/examples/react/router-monorepo-react-query/package.json
@@ -12,9 +12,9 @@
   "dependencies": {
     "@tanstack/react-query": "^5.62.3",
     "@tanstack/react-query-devtools": "^5.62.3",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1"
diff --git a/examples/react/router-monorepo-react-query/packages/app/package.json b/examples/react/router-monorepo-react-query/packages/app/package.json
index a9599eaaa..5764b5817 100644
--- a/examples/react/router-monorepo-react-query/packages/app/package.json
+++ b/examples/react/router-monorepo-react-query/packages/app/package.json
@@ -20,7 +20,7 @@
     "@types/react-dom": "^18.2.18",
     "@vitejs/plugin-react": "^4.3.4",
     "typescript": "^5.7.2",
-    "@tanstack/router-devtools": "^1.87.12",
+    "@tanstack/router-devtools": "^1.88.0",
     "vite": "^6.0.3",
     "vite-plugin-dts": "^4.3.0"
   },
diff --git a/examples/react/router-monorepo-react-query/packages/router/package.json b/examples/react/router-monorepo-react-query/packages/router/package.json
index 96271250b..e888a7212 100644
--- a/examples/react/router-monorepo-react-query/packages/router/package.json
+++ b/examples/react/router-monorepo-react-query/packages/router/package.json
@@ -10,8 +10,8 @@
   "dependencies": {
     "@tanstack/history": "^1.87.6",
     "@tanstack/react-query": "^5.62.3",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
     "@router-mono-react-query/post-query": "workspace:*",
     "redaxios": "^0.5.1",
     "zod": "^3.23.8",
diff --git a/examples/react/router-monorepo-simple/package.json b/examples/react/router-monorepo-simple/package.json
index a70e761f0..10bb45b9e 100644
--- a/examples/react/router-monorepo-simple/package.json
+++ b/examples/react/router-monorepo-simple/package.json
@@ -8,9 +8,9 @@
     "dev": "pnpm router build && pnpm post-feature build && pnpm app dev"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1"
diff --git a/examples/react/router-monorepo-simple/packages/app/package.json b/examples/react/router-monorepo-simple/packages/app/package.json
index 43d4b4b8b..69ea8a043 100644
--- a/examples/react/router-monorepo-simple/packages/app/package.json
+++ b/examples/react/router-monorepo-simple/packages/app/package.json
@@ -19,7 +19,7 @@
     "@types/react-dom": "^18.2.18",
     "@vitejs/plugin-react": "^4.3.4",
     "typescript": "^5.7.2",
-    "@tanstack/router-devtools": "^1.87.12",
+    "@tanstack/router-devtools": "^1.88.0",
     "vite": "^6.0.3",
     "vite-plugin-dts": "^4.3.0"
   },
diff --git a/examples/react/router-monorepo-simple/packages/router/package.json b/examples/react/router-monorepo-simple/packages/router/package.json
index 90945efad..b4edacafd 100644
--- a/examples/react/router-monorepo-simple/packages/router/package.json
+++ b/examples/react/router-monorepo-simple/packages/router/package.json
@@ -9,8 +9,8 @@
   "types": "./dist/index.d.ts",
   "dependencies": {
     "@tanstack/history": "^1.87.6",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
     "redaxios": "^0.5.1",
     "zod": "^3.23.8",
     "react": "^18.2.0",
diff --git a/examples/react/scroll-restoration/package.json b/examples/react/scroll-restoration/package.json
index b1aaf9bb1..e0f7629aa 100644
--- a/examples/react/scroll-restoration/package.json
+++ b/examples/react/scroll-restoration/package.json
@@ -9,9 +9,9 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
     "@tanstack/react-virtual": "^3.11.1",
-    "@tanstack/router-devtools": "^1.87.12",
+    "@tanstack/router-devtools": "^1.88.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
   },
diff --git a/examples/react/search-validator-adapters/package.json b/examples/react/search-validator-adapters/package.json
index d92967a2f..9397dd8b9 100644
--- a/examples/react/search-validator-adapters/package.json
+++ b/examples/react/search-validator-adapters/package.json
@@ -11,12 +11,12 @@
   },
   "dependencies": {
     "@tanstack/react-query": "^5.62.3",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/arktype-adapter": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
-    "@tanstack/valibot-adapter": "^1.87.12",
-    "@tanstack/zod-adapter": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/arktype-adapter": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
+    "@tanstack/valibot-adapter": "^1.88.0",
+    "@tanstack/zod-adapter": "^1.88.0",
     "arktype": "2.0.0-rc.26",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
diff --git a/examples/react/start-basic-auth/package.json b/examples/react/start-basic-auth/package.json
index 80a54fbc8..ea0aba13d 100644
--- a/examples/react/start-basic-auth/package.json
+++ b/examples/react/start-basic-auth/package.json
@@ -11,9 +11,9 @@
   },
   "dependencies": {
     "@prisma/client": "5.22.0",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/start": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/start": "^1.88.0",
     "dotenv": "^16.4.7",
     "prisma": "^5.22.0",
     "react": "^18.3.1",
diff --git a/examples/react/start-basic-react-query/package.json b/examples/react/start-basic-react-query/package.json
index 93a7e0323..e30ee2011 100644
--- a/examples/react/start-basic-react-query/package.json
+++ b/examples/react/start-basic-react-query/package.json
@@ -11,10 +11,10 @@
   "dependencies": {
     "@tanstack/react-query": "^5.62.3",
     "@tanstack/react-query-devtools": "^5.62.3",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/react-router-with-query": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/start": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/react-router-with-query": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/start": "^1.88.0",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
     "redaxios": "^0.5.1",
diff --git a/examples/react/start-basic-rsc/package.json b/examples/react/start-basic-rsc/package.json
index 401862517..6584ff194 100644
--- a/examples/react/start-basic-rsc/package.json
+++ b/examples/react/start-basic-rsc/package.json
@@ -10,9 +10,9 @@
   },
   "dependencies": {
     "@babel/plugin-syntax-typescript": "^7.25.9",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/start": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/start": "^1.88.0",
     "redaxios": "^0.5.1",
     "tailwind-merge": "^2.5.5",
     "vinxi": "0.5.1"
diff --git a/examples/react/start-basic/package.json b/examples/react/start-basic/package.json
index 19f5edcbd..7aade4876 100644
--- a/examples/react/start-basic/package.json
+++ b/examples/react/start-basic/package.json
@@ -9,9 +9,9 @@
     "start": "vinxi start"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/start": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/start": "^1.88.0",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
     "redaxios": "^0.5.1",
diff --git a/examples/react/start-clerk-basic/package.json b/examples/react/start-clerk-basic/package.json
index 151882382..5b8d0524f 100644
--- a/examples/react/start-clerk-basic/package.json
+++ b/examples/react/start-clerk-basic/package.json
@@ -10,9 +10,9 @@
   },
   "dependencies": {
     "@clerk/tanstack-start": "0.6.5",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/start": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/start": "^1.88.0",
     "dotenv": "^16.4.7",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
diff --git a/examples/react/start-convex-trellaux/package.json b/examples/react/start-convex-trellaux/package.json
index b6488aa7e..5ba78e5b2 100644
--- a/examples/react/start-convex-trellaux/package.json
+++ b/examples/react/start-convex-trellaux/package.json
@@ -13,10 +13,10 @@
   "dependencies": {
     "@tanstack/react-query": "^5.62.3",
     "@tanstack/react-query-devtools": "^5.62.3",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/react-router-with-query": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/start": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/react-router-with-query": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/start": "^1.88.0",
     "@convex-dev/react-query": "0.0.0-alpha.8",
     "concurrently": "^8.2.2",
     "convex": "^1.17.3",
diff --git a/examples/react/start-counter/package.json b/examples/react/start-counter/package.json
index 11bee87e4..21d951b21 100644
--- a/examples/react/start-counter/package.json
+++ b/examples/react/start-counter/package.json
@@ -9,8 +9,8 @@
     "start": "vinxi start"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/start": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/start": "^1.88.0",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
     "vinxi": "0.5.1"
diff --git a/examples/react/start-large/package.json b/examples/react/start-large/package.json
index b752f6a4d..ed764b2b4 100644
--- a/examples/react/start-large/package.json
+++ b/examples/react/start-large/package.json
@@ -12,9 +12,9 @@
   },
   "dependencies": {
     "@tanstack/react-query": "^5.62.3",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/start": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/start": "^1.88.0",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
     "redaxios": "^0.5.1",
diff --git a/examples/react/start-supabase-basic/package.json b/examples/react/start-supabase-basic/package.json
index e53b46fb5..4e2a411a9 100644
--- a/examples/react/start-supabase-basic/package.json
+++ b/examples/react/start-supabase-basic/package.json
@@ -15,9 +15,9 @@
   "dependencies": {
     "@supabase/ssr": "^0.5.2",
     "@supabase/supabase-js": "^2.47.3",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/start": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/start": "^1.88.0",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
     "vinxi": "0.5.1"
diff --git a/examples/react/start-trellaux/package.json b/examples/react/start-trellaux/package.json
index 81d753716..cfb0af41c 100644
--- a/examples/react/start-trellaux/package.json
+++ b/examples/react/start-trellaux/package.json
@@ -11,10 +11,10 @@
   "dependencies": {
     "@tanstack/react-query": "^5.62.3",
     "@tanstack/react-query-devtools": "^5.62.3",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/react-router-with-query": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/start": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/react-router-with-query": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/start": "^1.88.0",
     "ky": "^1.7.2",
     "msw": "^2.6.8",
     "react": "^18.3.1",
diff --git a/examples/react/with-framer-motion/package.json b/examples/react/with-framer-motion/package.json
index 1105559fb..d42f5a5ed 100644
--- a/examples/react/with-framer-motion/package.json
+++ b/examples/react/with-framer-motion/package.json
@@ -9,8 +9,8 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
     "redaxios": "^0.5.1",
     "framer-motion": "^11.13.3",
     "react": "^18.2.0",
diff --git a/examples/react/with-trpc-react-query/package.json b/examples/react/with-trpc-react-query/package.json
index 4a96d50f1..af83550f4 100644
--- a/examples/react/with-trpc-react-query/package.json
+++ b/examples/react/with-trpc-react-query/package.json
@@ -10,9 +10,9 @@
   "dependencies": {
     "@tanstack/react-query": "^5.62.3",
     "@tanstack/react-query-devtools": "^5.62.3",
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
     "@trpc/client": "11.0.0-rc.660",
     "@trpc/react-query": "11.0.0-rc.660",
     "@trpc/server": "11.0.0-rc.660",
diff --git a/examples/react/with-trpc/package.json b/examples/react/with-trpc/package.json
index 09238969c..3026b8189 100644
--- a/examples/react/with-trpc/package.json
+++ b/examples/react/with-trpc/package.json
@@ -8,9 +8,9 @@
     "start": "vinxi start"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.87.12",
-    "@tanstack/router-devtools": "^1.87.12",
-    "@tanstack/router-plugin": "^1.87.11",
+    "@tanstack/react-router": "^1.88.0",
+    "@tanstack/router-devtools": "^1.88.0",
+    "@tanstack/router-plugin": "^1.87.13",
     "@trpc/client": "11.0.0-rc.660",
     "@trpc/server": "11.0.0-rc.660",
     "react": "^18.2.0",
diff --git a/package.json b/package.json
index b0d195f24..1c40996f6 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,7 @@
     "test:types": "nx affected --target=test:types --exclude=examples/**",
     "test:e2e": "nx run-many --target=test:e2e",
     "build": "nx affected --target=build --exclude=e2e/** --exclude=examples/**",
-    "build:all": "nx run-many --target=build --exclude=examples/** --exclude=e2e/** ",
+    "build:all": "nx run-many --target=build --exclude=examples/** --exclude=e2e/**",
     "watch": "pnpm run build:all && nx watch --all -- pnpm run build:all",
     "dev": "pnpm run watch",
     "prettier": "prettier --ignore-unknown '**/*'",
diff --git a/packages/arktype-adapter/package.json b/packages/arktype-adapter/package.json
index 402866ac6..88ffbf700 100644
--- a/packages/arktype-adapter/package.json
+++ b/packages/arktype-adapter/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/arktype-adapter",
-  "version": "1.87.12",
+  "version": "1.88.0",
   "description": "Modern and scalable routing for React applications",
   "author": "Tanner Linsley",
   "license": "MIT",
diff --git a/packages/create-router/package.json b/packages/create-router/package.json
index 07698234a..9cd8014c2 100644
--- a/packages/create-router/package.json
+++ b/packages/create-router/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/create-router",
-  "version": "1.87.12",
+  "version": "1.88.0",
   "description": "Modern and scalable routing for React applications",
   "author": "Tanner Linsley",
   "license": "MIT",
diff --git a/packages/react-router-with-query/package.json b/packages/react-router-with-query/package.json
index 7ff9c873c..ae4a01e53 100644
--- a/packages/react-router-with-query/package.json
+++ b/packages/react-router-with-query/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/react-router-with-query",
-  "version": "1.87.12",
+  "version": "1.88.0",
   "description": "Modern and scalable routing for React applications",
   "author": "Tanner Linsley",
   "license": "MIT",
diff --git a/packages/react-router/package.json b/packages/react-router/package.json
index 7d9d59ccb..7547daecc 100644
--- a/packages/react-router/package.json
+++ b/packages/react-router/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/react-router",
-  "version": "1.87.12",
+  "version": "1.88.0",
   "description": "Modern and scalable routing for React applications",
   "author": "Tanner Linsley",
   "license": "MIT",
diff --git a/packages/react-router/src/RouterProvider.tsx b/packages/react-router/src/RouterProvider.tsx
index 67be7b953..e5b40ebaa 100644
--- a/packages/react-router/src/RouterProvider.tsx
+++ b/packages/react-router/src/RouterProvider.tsx
@@ -15,6 +15,7 @@ import type {
 export interface CommitLocationOptions {
   replace?: boolean
   resetScroll?: boolean
+  hashScrollIntoView?: boolean | ScrollIntoViewOptions
   viewTransition?: boolean | ViewTransitionOptions
   /**
    * @deprecated All navigations use React transitions under the hood now
@@ -38,7 +39,7 @@ export type NavigateFn = <
   TMaskTo extends string = '',
 >(
   opts: NavigateOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
-) => Promise<void>
+) => Promise<void> | void
 
 export type BuildLocationFn = <
   TRouter extends RegisteredRouter,
diff --git a/packages/react-router/src/Transitioner.tsx b/packages/react-router/src/Transitioner.tsx
index 63b8ff794..c0539fbb2 100644
--- a/packages/react-router/src/Transitioner.tsx
+++ b/packages/react-router/src/Transitioner.tsx
@@ -143,10 +143,13 @@ export function Transitioner() {
       }))
 
       if (typeof document !== 'undefined' && (document as any).querySelector) {
-        if (router.state.location.hash !== '') {
+        const hashScrollIntoViewOptions =
+          router.state.location.state.__hashScrollIntoViewOptions ?? true
+
+        if (hashScrollIntoViewOptions && router.state.location.hash !== '') {
           const el = document.getElementById(router.state.location.hash)
           if (el) {
-            el.scrollIntoView()
+            el.scrollIntoView(hashScrollIntoViewOptions)
           }
         }
       }
diff --git a/packages/react-router/src/history.ts b/packages/react-router/src/history.ts
index 19a93e097..2271af692 100644
--- a/packages/react-router/src/history.ts
+++ b/packages/react-router/src/history.ts
@@ -4,5 +4,6 @@ declare module '@tanstack/history' {
   interface HistoryState {
     __tempLocation?: HistoryLocation
     __tempKey?: string
+    __hashScrollIntoViewOptions?: boolean | ScrollIntoViewOptions
   }
 }
diff --git a/packages/react-router/src/link.tsx b/packages/react-router/src/link.tsx
index 387926fec..7f86eb6f4 100644
--- a/packages/react-router/src/link.tsx
+++ b/packages/react-router/src/link.tsx
@@ -210,6 +210,10 @@ export type NavigateOptions<
 > = ToOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> & NavigateOptionProps
 
 export interface NavigateOptionProps {
+  // if set to `true`, the router will scroll the element with an id matching the hash into view with default ScrollIntoViewOptions.
+  // if set to `false`, the router will not scroll the element with an id matching the hash into view.
+  // if set to `ScrollIntoViewOptions`, the router will scroll the element with an id matching the hash into view with the provided options.
+  hashScrollIntoView?: boolean | ScrollIntoViewOptions
   // `replace` is a boolean that determines whether the navigation should replace the current history entry or push a new one.
   replace?: boolean
   resetScroll?: boolean
@@ -219,6 +223,8 @@ export interface NavigateOptionProps {
   // if set to `ViewTransitionOptions`, the router will pass the `types` field to document.startViewTransition({update: fn, types: viewTransition.types}) call
   viewTransition?: boolean | ViewTransitionOptions
   ignoreBlocker?: boolean
+  reloadDocument?: boolean
+  href?: string
 }
 
 export type ToOptions<
@@ -607,6 +613,7 @@ export function useLinkProps<
     to,
     preload: userPreload,
     preloadDelay: userPreloadDelay,
+    hashScrollIntoView,
     replace,
     startTransition,
     resetScroll,
@@ -643,6 +650,9 @@ export function useLinkProps<
   // null for LinkUtils
 
   const type: 'internal' | 'external' = React.useMemo(() => {
+    if (rest.reloadDocument) {
+      return 'external'
+    }
     try {
       new URL(`${to}`)
       return 'external'
@@ -801,6 +811,7 @@ export function useLinkProps<
         ...options,
         replace,
         resetScroll,
+        hashScrollIntoView,
         startTransition,
         viewTransition,
         ignoreBlocker,
diff --git a/packages/react-router/src/redirects.ts b/packages/react-router/src/redirects.ts
index b1a29a689..0884f56e5 100644
--- a/packages/react-router/src/redirects.ts
+++ b/packages/react-router/src/redirects.ts
@@ -12,10 +12,10 @@ export type Redirect<
   TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
   TMaskTo extends string = '.',
 > = {
+  href?: string
   /**
    * @deprecated Use `statusCode` instead
    **/
-  href?: string
   code?: number
   statusCode?: number
   throw?: any
@@ -47,6 +47,14 @@ export function redirect<
   ;(opts as any).isRedirect = true
   opts.statusCode = opts.statusCode || opts.code || 307
   opts.headers = opts.headers || {}
+  if (!opts.reloadDocument) {
+    opts.reloadDocument = false
+    try {
+      new URL(`${opts.href}`)
+      opts.reloadDocument = true
+    } catch {}
+  }
+
   if (opts.throw) {
     throw opts
   }
diff --git a/packages/react-router/src/route.ts b/packages/react-router/src/route.ts
index 6498231d7..80a2835c6 100644
--- a/packages/react-router/src/route.ts
+++ b/packages/react-router/src/route.ts
@@ -569,7 +569,7 @@ export interface LoaderFnContext<
   /**
    * @deprecated Use `throw redirect({ to: '/somewhere' })` instead
    **/
-  navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void>
+  navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void> | void
   // root route does not have a parent match
   parentMatchPromise: TId extends RootRouteId
     ? never
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index e7f32c501..b22f61c35 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -313,6 +313,14 @@ export interface RouterOptions<
    * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultviewtransition-property)
    */
   defaultViewTransition?: boolean | ViewTransitionOptions
+  /**
+   * The default `hashScrollIntoView` a route should use if no hashScrollIntoView is provided while navigating
+   *
+   * See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) for more information on `ScrollIntoViewOptions`.
+   *
+   * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaulthashscrollintoview-property)
+   */
+  defaultHashScrollIntoView?: boolean | ScrollIntoViewOptions
   /**
    * @default 'fuzzy'
    * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#notfoundmode-property)
@@ -536,6 +544,7 @@ export interface BuildNextOptions {
   }
   from?: string
   _fromLocation?: ParsedLocation
+  href?: string
 }
 
 export interface MatchedRoutesResult {
@@ -1808,7 +1817,7 @@ export class Router<
       this.load()
     } else {
       // eslint-disable-next-line prefer-const
-      let { maskedLocation, ...nextHistory } = next
+      let { maskedLocation, hashScrollIntoView, ...nextHistory } = next
 
       if (maskedLocation) {
         nextHistory = {
@@ -1838,6 +1847,9 @@ export class Router<
         }
       }
 
+      nextHistory.state.__hashScrollIntoViewOptions =
+        hashScrollIntoView ?? this.options.defaultHashScrollIntoView ?? true
+
       this.shouldViewTransition = viewTransition
 
       this.history[next.replace ? 'replace' : 'push'](
@@ -1859,11 +1871,12 @@ export class Router<
   buildAndCommitLocation = ({
     replace,
     resetScroll,
+    hashScrollIntoView,
     viewTransition,
     ignoreBlocker,
+    href,
     ...rest
   }: BuildNextOptions & CommitLocationOptions = {}) => {
-    const href = (rest as any).href
     if (href) {
       const parsed = parseHref(href, {})
       rest.to = parsed.pathname
@@ -1881,33 +1894,29 @@ export class Router<
       viewTransition,
       replace,
       resetScroll,
+      hashScrollIntoView,
       ignoreBlocker,
     })
   }
 
-  navigate: NavigateFn = ({ to, ...rest }) => {
-    // If this link simply reloads the current route,
-    // make sure it has a new key so it will trigger a data refresh
-
-    // If this `to` is a valid external URL, return
-    // null for LinkUtils
-    const toString = String(to)
-    let isExternal
-
-    try {
-      new URL(`${toString}`)
-      isExternal = true
-    } catch (e) {}
-
-    invariant(
-      !isExternal,
-      'Attempting to navigate to external url with router.navigate!',
-    )
+  navigate: NavigateFn = ({ to, reloadDocument, href, ...rest }) => {
+    if (reloadDocument) {
+      if (!href) {
+        const location = this.buildLocation({ to, ...rest } as any)
+        href = location.href
+      }
+      if (rest.replace) {
+        window.location.replace(href)
+      } else {
+        window.location.href = href
+      }
+      return
+    }
 
     return this.buildAndCommitLocation({
       ...rest,
+      href,
       to: to as string,
-      // to: toString,
     })
   }
 
@@ -2041,7 +2050,7 @@ export class Router<
             redirect = err
             if (!this.isServer) {
               this.navigate({
-                ...err,
+                ...redirect,
                 replace: true,
                 ignoreBlocker: true,
               })
@@ -2194,7 +2203,11 @@ export class Router<
     }
 
     const handleRedirectAndNotFound = (match: AnyRouteMatch, err: any) => {
-      if (isResolvedRedirect(err)) throw err
+      if (isResolvedRedirect(err)) {
+        if (!err.reloadDocument) {
+          throw err
+        }
+      }
 
       if (isRedirect(err) || isNotFound(err)) {
         updateMatch(match.id, (prev) => ({
@@ -2877,6 +2890,9 @@ export class Router<
       return matches
     } catch (err) {
       if (isRedirect(err)) {
+        if (err.reloadDocument) {
+          return undefined
+        }
         return await this.preloadRoute({
           ...(err as any),
           _fromLocation: next,
diff --git a/packages/router-devtools/package.json b/packages/router-devtools/package.json
index 1e8ca4d1e..3957e1e74 100644
--- a/packages/router-devtools/package.json
+++ b/packages/router-devtools/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/router-devtools",
-  "version": "1.87.12",
+  "version": "1.88.0",
   "description": "Modern and scalable routing for React applications",
   "author": "Tanner Linsley",
   "license": "MIT",
diff --git a/packages/router-plugin/package.json b/packages/router-plugin/package.json
index e8e1fcbae..3f4e176d1 100644
--- a/packages/router-plugin/package.json
+++ b/packages/router-plugin/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/router-plugin",
-  "version": "1.87.11",
+  "version": "1.87.13",
   "description": "Modern and scalable routing for React applications",
   "author": "Tanner Linsley",
   "license": "MIT",
diff --git a/packages/router-plugin/src/core/code-splitter/compilers.ts b/packages/router-plugin/src/core/code-splitter/compilers.ts
index 3b80659c4..84f082e1c 100644
--- a/packages/router-plugin/src/core/code-splitter/compilers.ts
+++ b/packages/router-plugin/src/core/code-splitter/compilers.ts
@@ -269,6 +269,7 @@ export function compileCodeSplitReferenceRoute(opts: ParseAstOptions) {
 
   return generate(ast, {
     sourceMaps: true,
+    sourceFileName: opts.filename,
   })
 }
 
@@ -541,6 +542,7 @@ export function compileCodeSplitVirtualRoute(opts: ParseAstOptions) {
 
   return generate(ast, {
     sourceMaps: true,
+    sourceFileName: opts.filename,
   })
 }
 
diff --git a/packages/router-vite-plugin/package.json b/packages/router-vite-plugin/package.json
index 50b276bd7..046b4e33a 100644
--- a/packages/router-vite-plugin/package.json
+++ b/packages/router-vite-plugin/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/router-vite-plugin",
-  "version": "1.87.11",
+  "version": "1.87.13",
   "description": "Modern and scalable routing for React applications",
   "author": "Tanner Linsley",
   "license": "MIT",
diff --git a/packages/start-vite-plugin/package.json b/packages/start-vite-plugin/package.json
index 064d36e11..5b0c20c1c 100644
--- a/packages/start-vite-plugin/package.json
+++ b/packages/start-vite-plugin/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/start-vite-plugin",
-  "version": "1.87.10",
+  "version": "1.87.14",
   "description": "Modern and scalable routing for React applications",
   "author": "Tanner Linsley",
   "license": "MIT",
diff --git a/packages/start-vite-plugin/src/compilers.ts b/packages/start-vite-plugin/src/compilers.ts
index f5e6dfd3c..365a38609 100644
--- a/packages/start-vite-plugin/src/compilers.ts
+++ b/packages/start-vite-plugin/src/compilers.ts
@@ -21,6 +21,7 @@ export function compileEliminateDeadCode(opts: ParseAstOptions) {
   deadCodeElimination(ast)
   return generate(ast, {
     sourceMaps: true,
+    filename: opts.filename,
   })
 }
 
@@ -185,6 +186,7 @@ export function compileStartOutput(opts: ParseAstOptions) {
 
   return generate(ast, {
     sourceMaps: true,
+    filename: opts.filename,
     minified: process.env.NODE_ENV === 'production',
   })
 }
diff --git a/packages/start/package.json b/packages/start/package.json
index ba01f4352..bd988b248 100644
--- a/packages/start/package.json
+++ b/packages/start/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/start",
-  "version": "1.87.12",
+  "version": "1.88.0",
   "description": "Modern and scalable routing for React applications",
   "author": "Tanner Linsley",
   "license": "MIT",
diff --git a/packages/start/src/client/useServerFn.ts b/packages/start/src/client/useServerFn.ts
index 1bdfa4bc5..241e102da 100644
--- a/packages/start/src/client/useServerFn.ts
+++ b/packages/start/src/client/useServerFn.ts
@@ -16,12 +16,12 @@ export function useServerFn<T extends (...deps: Array<any>) => Promise<any>>(
       return res
     } catch (err) {
       if (isRedirect(err)) {
-        router.navigate(
-          router.resolveRedirect({
-            ...err,
-            _fromLocation: router.state.location,
-          }),
-        )
+        const resolvedRedirect = router.resolveRedirect({
+          ...err,
+          _fromLocation: router.state.location,
+        })
+
+        return router.navigate(resolvedRedirect)
       }
 
       throw err
diff --git a/packages/start/src/server-handler/index.tsx b/packages/start/src/server-handler/index.tsx
index 8e8dea0ba..51c988198 100644
--- a/packages/start/src/server-handler/index.tsx
+++ b/packages/start/src/server-handler/index.tsx
@@ -168,7 +168,7 @@ function redirectOrNotFoundResponse(error: any) {
     status: 200,
     headers: {
       'Content-Type': 'application/json',
-      ...(error.headers || {}),
+      ...(headers || {}),
     },
   })
 }
diff --git a/packages/valibot-adapter/package.json b/packages/valibot-adapter/package.json
index bb060a17d..2877a2aa7 100644
--- a/packages/valibot-adapter/package.json
+++ b/packages/valibot-adapter/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/valibot-adapter",
-  "version": "1.87.12",
+  "version": "1.88.0",
   "description": "Modern and scalable routing for React applications",
   "author": "Tanner Linsley",
   "license": "MIT",
diff --git a/packages/zod-adapter/package.json b/packages/zod-adapter/package.json
index dbf6de77e..9554cb0c6 100644
--- a/packages/zod-adapter/package.json
+++ b/packages/zod-adapter/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/zod-adapter",
-  "version": "1.87.12",
+  "version": "1.88.0",
   "description": "Modern and scalable routing for React applications",
   "author": "Tanner Linsley",
   "license": "MIT",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b2b058222..a3b04499d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -225,6 +225,9 @@ importers:
       '@vitejs/plugin-react':
         specifier: ^4.3.4
         version: 4.3.4(vite@6.0.3(@types/node@22.10.1)(jiti@2.4.1)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1))
+      combinate:
+        specifier: ^1.1.11
+        version: 1.1.11
       vite:
         specifier: ^6.0.3
         version: 6.0.3(@types/node@22.10.1)(jiti@2.4.1)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)
@@ -649,6 +652,9 @@ importers:
       autoprefixer:
         specifier: ^10.4.20
         version: 10.4.20(postcss@8.4.49)
+      combinate:
+        specifier: ^1.1.11
+        version: 1.1.11
       postcss:
         specifier: ^8.4.49
         version: 8.4.49
@@ -2007,10 +2013,10 @@ importers:
         version: 18.3.1
       html-webpack-plugin:
         specifier: ^5.6.3
-        version: 5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.1)
+        version: 5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))
       swc-loader:
         specifier: ^0.2.6
-        version: 0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1)
+        version: 0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))
       typescript:
         specifier: ^5.7.2
         version: 5.7.2
@@ -3237,7 +3243,7 @@ importers:
         version: 4.3.4(vite@6.0.3(@types/node@22.10.1)(jiti@2.4.1)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1))
       html-webpack-plugin:
         specifier: ^5.6.0
-        version: 5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.1)
+        version: 5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))
       react:
         specifier: ^18.3.1
         version: 18.3.1
@@ -3246,7 +3252,7 @@ importers:
         version: 18.3.1(react@18.3.1)
       swc-loader:
         specifier: ^0.2.6
-        version: 0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1)
+        version: 0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))
       typescript:
         specifier: ^5.7.2
         version: 5.7.2
@@ -13595,17 +13601,17 @@ snapshots:
       '@webassemblyjs/ast': 1.14.1
       '@xtuc/long': 4.2.2
 
-  '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.97.1)':
+  '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))':
     dependencies:
       webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)
       webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1)
 
-  '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.97.1)':
+  '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))':
     dependencies:
       webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)
       webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1)
 
-  '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.1.0)(webpack@5.97.1)':
+  '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack-dev-server@5.1.0(webpack-cli@5.1.4)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))':
     dependencies:
       webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)
       webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1)
@@ -15515,7 +15521,7 @@ snapshots:
       relateurl: 0.2.7
       terser: 5.36.0
 
-  html-webpack-plugin@5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.1):
+  html-webpack-plugin@5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)):
     dependencies:
       '@types/html-minifier-terser': 6.1.0
       html-minifier-terser: 6.1.0
@@ -17601,7 +17607,7 @@ snapshots:
       csso: 5.0.5
       picocolors: 1.1.1
 
-  swc-loader@0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1):
+  swc-loader@0.2.6(@swc/core@1.10.1(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)):
     dependencies:
       '@swc/core': 1.10.1(@swc/helpers@0.5.15)
       '@swc/counter': 0.1.3
@@ -17671,26 +17677,26 @@ snapshots:
       mkdirp: 1.0.4
       yallist: 4.0.0
 
-  terser-webpack-plugin@5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)):
+  terser-webpack-plugin@5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)):
     dependencies:
       '@jridgewell/trace-mapping': 0.3.25
       jest-worker: 27.5.1
       schema-utils: 3.3.0
       serialize-javascript: 6.0.2
       terser: 5.36.0
-      webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)
+      webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)
     optionalDependencies:
       '@swc/core': 1.10.1(@swc/helpers@0.5.15)
       esbuild: 0.24.0
 
-  terser-webpack-plugin@5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.1):
+  terser-webpack-plugin@5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)):
     dependencies:
       '@jridgewell/trace-mapping': 0.3.25
       jest-worker: 27.5.1
       schema-utils: 3.3.0
       serialize-javascript: 6.0.2
       terser: 5.36.0
-      webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)
+      webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)
     optionalDependencies:
       '@swc/core': 1.10.1(@swc/helpers@0.5.15)
       esbuild: 0.24.0
@@ -18312,9 +18318,9 @@ snapshots:
   webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1):
     dependencies:
       '@discoveryjs/json-ext': 0.5.7
-      '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.97.1)
-      '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.97.1)
-      '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.1.0)(webpack@5.97.1)
+      '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))
+      '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))
+      '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.1))(webpack-dev-server@5.1.0(webpack-cli@5.1.4)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))
       colorette: 2.0.20
       commander: 10.0.1
       cross-spawn: 7.0.6
@@ -18328,7 +18334,7 @@ snapshots:
     optionalDependencies:
       webpack-dev-server: 5.1.0(webpack-cli@5.1.4)(webpack@5.97.1)
 
-  webpack-dev-middleware@7.4.2(webpack@5.97.1):
+  webpack-dev-middleware@7.4.2(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)):
     dependencies:
       colorette: 2.0.20
       memfs: 4.14.1
@@ -18367,7 +18373,7 @@ snapshots:
       serve-index: 1.9.1
       sockjs: 0.3.24
       spdy: 4.0.2
-      webpack-dev-middleware: 7.4.2(webpack@5.97.1)
+      webpack-dev-middleware: 7.4.2(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))
       ws: 8.18.0
     optionalDependencies:
       webpack: 5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)
@@ -18440,7 +18446,7 @@ snapshots:
       neo-async: 2.6.2
       schema-utils: 3.3.0
       tapable: 2.2.1
-      terser-webpack-plugin: 5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.1)
+      terser-webpack-plugin: 5.3.10(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.1(@swc/core@1.10.1(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))
       watchpack: 2.4.2
       webpack-sources: 3.2.3
     optionalDependencies:


Test patch:
diff --git a/e2e/react-router/basic-file-based/package.json b/e2e/react-router/basic-file-based/package.json
index c13b0454e..915709c48 100644
--- a/e2e/react-router/basic-file-based/package.json
+++ b/e2e/react-router/basic-file-based/package.json
@@ -25,6 +25,7 @@
     "@types/react": "^18.2.47",
     "@types/react-dom": "^18.2.18",
     "@vitejs/plugin-react": "^4.3.4",
+    "combinate": "^1.1.11",
     "vite": "^6.0.3"
   }
 }
diff --git a/e2e/react-router/basic-file-based/src/routeTree.gen.ts b/e2e/react-router/basic-file-based/src/routeTree.gen.ts
index 67daf0e61..c5469a923 100644
--- a/e2e/react-router/basic-file-based/src/routeTree.gen.ts
+++ b/e2e/react-router/basic-file-based/src/routeTree.gen.ts
@@ -14,16 +14,22 @@ import { createFileRoute } from '@tanstack/react-router'
 
 import { Route as rootRoute } from './routes/__root'
 import { Route as PostsImport } from './routes/posts'
+import { Route as AnchorImport } from './routes/anchor'
 import { Route as LayoutImport } from './routes/_layout'
 import { Route as IndexImport } from './routes/index'
+import { Route as RedirectIndexImport } from './routes/redirect/index'
 import { Route as PostsIndexImport } from './routes/posts.index'
 import { Route as StructuralSharingEnabledImport } from './routes/structural-sharing.$enabled'
+import { Route as RedirectTargetImport } from './routes/redirect/$target'
 import { Route as PostsPostIdImport } from './routes/posts.$postId'
 import { Route as LayoutLayout2Import } from './routes/_layout/_layout-2'
 import { Route as groupLazyinsideImport } from './routes/(group)/lazyinside'
 import { Route as groupInsideImport } from './routes/(group)/inside'
 import { Route as groupLayoutImport } from './routes/(group)/_layout'
 import { Route as anotherGroupOnlyrouteinsideImport } from './routes/(another-group)/onlyrouteinside'
+import { Route as RedirectTargetIndexImport } from './routes/redirect/$target/index'
+import { Route as RedirectTargetViaLoaderImport } from './routes/redirect/$target/via-loader'
+import { Route as RedirectTargetViaBeforeLoadImport } from './routes/redirect/$target/via-beforeLoad'
 import { Route as PostsPostIdEditImport } from './routes/posts_.$postId.edit'
 import { Route as LayoutLayout2LayoutBImport } from './routes/_layout/_layout-2/layout-b'
 import { Route as LayoutLayout2LayoutAImport } from './routes/_layout/_layout-2/layout-a'
@@ -47,6 +53,12 @@ const PostsRoute = PostsImport.update({
   getParentRoute: () => rootRoute,
 } as any)
 
+const AnchorRoute = AnchorImport.update({
+  id: '/anchor',
+  path: '/anchor',
+  getParentRoute: () => rootRoute,
+} as any)
+
 const LayoutRoute = LayoutImport.update({
   id: '/_layout',
   getParentRoute: () => rootRoute,
@@ -58,6 +70,12 @@ const IndexRoute = IndexImport.update({
   getParentRoute: () => rootRoute,
 } as any)
 
+const RedirectIndexRoute = RedirectIndexImport.update({
+  id: '/redirect/',
+  path: '/redirect/',
+  getParentRoute: () => rootRoute,
+} as any)
+
 const PostsIndexRoute = PostsIndexImport.update({
   id: '/',
   path: '/',
@@ -70,6 +88,12 @@ const StructuralSharingEnabledRoute = StructuralSharingEnabledImport.update({
   getParentRoute: () => rootRoute,
 } as any)
 
+const RedirectTargetRoute = RedirectTargetImport.update({
+  id: '/redirect/$target',
+  path: '/redirect/$target',
+  getParentRoute: () => rootRoute,
+} as any)
+
 const PostsPostIdRoute = PostsPostIdImport.update({
   id: '/$postId',
   path: '/$postId',
@@ -107,6 +131,25 @@ const anotherGroupOnlyrouteinsideRoute =
     getParentRoute: () => rootRoute,
   } as any)
 
+const RedirectTargetIndexRoute = RedirectTargetIndexImport.update({
+  id: '/',
+  path: '/',
+  getParentRoute: () => RedirectTargetRoute,
+} as any)
+
+const RedirectTargetViaLoaderRoute = RedirectTargetViaLoaderImport.update({
+  id: '/via-loader',
+  path: '/via-loader',
+  getParentRoute: () => RedirectTargetRoute,
+} as any)
+
+const RedirectTargetViaBeforeLoadRoute =
+  RedirectTargetViaBeforeLoadImport.update({
+    id: '/via-beforeLoad',
+    path: '/via-beforeLoad',
+    getParentRoute: () => RedirectTargetRoute,
+  } as any)
+
 const PostsPostIdEditRoute = PostsPostIdEditImport.update({
   id: '/posts_/$postId/edit',
   path: '/posts/$postId/edit',
@@ -155,6 +198,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof LayoutImport
       parentRoute: typeof rootRoute
     }
+    '/anchor': {
+      id: '/anchor'
+      path: '/anchor'
+      fullPath: '/anchor'
+      preLoaderRoute: typeof AnchorImport
+      parentRoute: typeof rootRoute
+    }
     '/posts': {
       id: '/posts'
       path: '/posts'
@@ -211,6 +261,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof PostsPostIdImport
       parentRoute: typeof PostsImport
     }
+    '/redirect/$target': {
+      id: '/redirect/$target'
+      path: '/redirect/$target'
+      fullPath: '/redirect/$target'
+      preLoaderRoute: typeof RedirectTargetImport
+      parentRoute: typeof rootRoute
+    }
     '/structural-sharing/$enabled': {
       id: '/structural-sharing/$enabled'
       path: '/structural-sharing/$enabled'
@@ -225,6 +282,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof PostsIndexImport
       parentRoute: typeof PostsImport
     }
+    '/redirect/': {
+      id: '/redirect/'
+      path: '/redirect'
+      fullPath: '/redirect'
+      preLoaderRoute: typeof RedirectIndexImport
+      parentRoute: typeof rootRoute
+    }
     '/(group)/_layout/insidelayout': {
       id: '/(group)/_layout/insidelayout'
       path: '/insidelayout'
@@ -260,6 +324,27 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof PostsPostIdEditImport
       parentRoute: typeof rootRoute
     }
+    '/redirect/$target/via-beforeLoad': {
+      id: '/redirect/$target/via-beforeLoad'
+      path: '/via-beforeLoad'
+      fullPath: '/redirect/$target/via-beforeLoad'
+      preLoaderRoute: typeof RedirectTargetViaBeforeLoadImport
+      parentRoute: typeof RedirectTargetImport
+    }
+    '/redirect/$target/via-loader': {
+      id: '/redirect/$target/via-loader'
+      path: '/via-loader'
+      fullPath: '/redirect/$target/via-loader'
+      preLoaderRoute: typeof RedirectTargetViaLoaderImport
+      parentRoute: typeof RedirectTargetImport
+    }
+    '/redirect/$target/': {
+      id: '/redirect/$target/'
+      path: '/'
+      fullPath: '/redirect/$target/'
+      preLoaderRoute: typeof RedirectTargetIndexImport
+      parentRoute: typeof RedirectTargetImport
+    }
   }
 }
 
@@ -330,43 +415,71 @@ const groupRouteChildren: groupRouteChildren = {
 
 const groupRouteWithChildren = groupRoute._addFileChildren(groupRouteChildren)
 
+interface RedirectTargetRouteChildren {
+  RedirectTargetViaBeforeLoadRoute: typeof RedirectTargetViaBeforeLoadRoute
+  RedirectTargetViaLoaderRoute: typeof RedirectTargetViaLoaderRoute
+  RedirectTargetIndexRoute: typeof RedirectTargetIndexRoute
+}
+
+const RedirectTargetRouteChildren: RedirectTargetRouteChildren = {
+  RedirectTargetViaBeforeLoadRoute: RedirectTargetViaBeforeLoadRoute,
+  RedirectTargetViaLoaderRoute: RedirectTargetViaLoaderRoute,
+  RedirectTargetIndexRoute: RedirectTargetIndexRoute,
+}
+
+const RedirectTargetRouteWithChildren = RedirectTargetRoute._addFileChildren(
+  RedirectTargetRouteChildren,
+)
+
 export interface FileRoutesByFullPath {
   '/': typeof groupLayoutRouteWithChildren
   '': typeof LayoutLayout2RouteWithChildren
+  '/anchor': typeof AnchorRoute
   '/posts': typeof PostsRouteWithChildren
   '/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
   '/inside': typeof groupInsideRoute
   '/lazyinside': typeof groupLazyinsideRoute
   '/posts/$postId': typeof PostsPostIdRoute
+  '/redirect/$target': typeof RedirectTargetRouteWithChildren
   '/structural-sharing/$enabled': typeof StructuralSharingEnabledRoute
   '/posts/': typeof PostsIndexRoute
+  '/redirect': typeof RedirectIndexRoute
   '/insidelayout': typeof groupLayoutInsidelayoutRoute
   '/subfolder/inside': typeof groupSubfolderInsideRoute
   '/layout-a': typeof LayoutLayout2LayoutARoute
   '/layout-b': typeof LayoutLayout2LayoutBRoute
   '/posts/$postId/edit': typeof PostsPostIdEditRoute
+  '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
+  '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+  '/redirect/$target/': typeof RedirectTargetIndexRoute
 }
 
 export interface FileRoutesByTo {
   '/': typeof groupLayoutRouteWithChildren
   '': typeof LayoutLayout2RouteWithChildren
+  '/anchor': typeof AnchorRoute
   '/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
   '/inside': typeof groupInsideRoute
   '/lazyinside': typeof groupLazyinsideRoute
   '/posts/$postId': typeof PostsPostIdRoute
   '/structural-sharing/$enabled': typeof StructuralSharingEnabledRoute
   '/posts': typeof PostsIndexRoute
+  '/redirect': typeof RedirectIndexRoute
   '/insidelayout': typeof groupLayoutInsidelayoutRoute
   '/subfolder/inside': typeof groupSubfolderInsideRoute
   '/layout-a': typeof LayoutLayout2LayoutARoute
   '/layout-b': typeof LayoutLayout2LayoutBRoute
   '/posts/$postId/edit': typeof PostsPostIdEditRoute
+  '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
+  '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+  '/redirect/$target': typeof RedirectTargetIndexRoute
 }
 
 export interface FileRoutesById {
   __root__: typeof rootRoute
   '/': typeof IndexRoute
   '/_layout': typeof LayoutRouteWithChildren
+  '/anchor': typeof AnchorRoute
   '/posts': typeof PostsRouteWithChildren
   '/(another-group)/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
   '/(group)': typeof groupRouteWithChildren
@@ -375,13 +488,18 @@ export interface FileRoutesById {
   '/(group)/lazyinside': typeof groupLazyinsideRoute
   '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
   '/posts/$postId': typeof PostsPostIdRoute
+  '/redirect/$target': typeof RedirectTargetRouteWithChildren
   '/structural-sharing/$enabled': typeof StructuralSharingEnabledRoute
   '/posts/': typeof PostsIndexRoute
+  '/redirect/': typeof RedirectIndexRoute
   '/(group)/_layout/insidelayout': typeof groupLayoutInsidelayoutRoute
   '/(group)/subfolder/inside': typeof groupSubfolderInsideRoute
   '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute
   '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute
   '/posts_/$postId/edit': typeof PostsPostIdEditRoute
+  '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
+  '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+  '/redirect/$target/': typeof RedirectTargetIndexRoute
 }
 
 export interface FileRouteTypes {
@@ -389,37 +507,49 @@ export interface FileRouteTypes {
   fullPaths:
     | '/'
     | ''
+    | '/anchor'
     | '/posts'
     | '/onlyrouteinside'
     | '/inside'
     | '/lazyinside'
     | '/posts/$postId'
+    | '/redirect/$target'
     | '/structural-sharing/$enabled'
     | '/posts/'
+    | '/redirect'
     | '/insidelayout'
     | '/subfolder/inside'
     | '/layout-a'
     | '/layout-b'
     | '/posts/$postId/edit'
+    | '/redirect/$target/via-beforeLoad'
+    | '/redirect/$target/via-loader'
+    | '/redirect/$target/'
   fileRoutesByTo: FileRoutesByTo
   to:
     | '/'
     | ''
+    | '/anchor'
     | '/onlyrouteinside'
     | '/inside'
     | '/lazyinside'
     | '/posts/$postId'
     | '/structural-sharing/$enabled'
     | '/posts'
+    | '/redirect'
     | '/insidelayout'
     | '/subfolder/inside'
     | '/layout-a'
     | '/layout-b'
     | '/posts/$postId/edit'
+    | '/redirect/$target/via-beforeLoad'
+    | '/redirect/$target/via-loader'
+    | '/redirect/$target'
   id:
     | '__root__'
     | '/'
     | '/_layout'
+    | '/anchor'
     | '/posts'
     | '/(another-group)/onlyrouteinside'
     | '/(group)'
@@ -428,33 +558,44 @@ export interface FileRouteTypes {
     | '/(group)/lazyinside'
     | '/_layout/_layout-2'
     | '/posts/$postId'
+    | '/redirect/$target'
     | '/structural-sharing/$enabled'
     | '/posts/'
+    | '/redirect/'
     | '/(group)/_layout/insidelayout'
     | '/(group)/subfolder/inside'
     | '/_layout/_layout-2/layout-a'
     | '/_layout/_layout-2/layout-b'
     | '/posts_/$postId/edit'
+    | '/redirect/$target/via-beforeLoad'
+    | '/redirect/$target/via-loader'
+    | '/redirect/$target/'
   fileRoutesById: FileRoutesById
 }
 
 export interface RootRouteChildren {
   IndexRoute: typeof IndexRoute
   LayoutRoute: typeof LayoutRouteWithChildren
+  AnchorRoute: typeof AnchorRoute
   PostsRoute: typeof PostsRouteWithChildren
   anotherGroupOnlyrouteinsideRoute: typeof anotherGroupOnlyrouteinsideRoute
   groupRoute: typeof groupRouteWithChildren
+  RedirectTargetRoute: typeof RedirectTargetRouteWithChildren
   StructuralSharingEnabledRoute: typeof StructuralSharingEnabledRoute
+  RedirectIndexRoute: typeof RedirectIndexRoute
   PostsPostIdEditRoute: typeof PostsPostIdEditRoute
 }
 
 const rootRouteChildren: RootRouteChildren = {
   IndexRoute: IndexRoute,
   LayoutRoute: LayoutRouteWithChildren,
+  AnchorRoute: AnchorRoute,
   PostsRoute: PostsRouteWithChildren,
   anotherGroupOnlyrouteinsideRoute: anotherGroupOnlyrouteinsideRoute,
   groupRoute: groupRouteWithChildren,
+  RedirectTargetRoute: RedirectTargetRouteWithChildren,
   StructuralSharingEnabledRoute: StructuralSharingEnabledRoute,
+  RedirectIndexRoute: RedirectIndexRoute,
   PostsPostIdEditRoute: PostsPostIdEditRoute,
 }
 
@@ -470,10 +611,13 @@ export const routeTree = rootRoute
       "children": [
         "/",
         "/_layout",
+        "/anchor",
         "/posts",
         "/(another-group)/onlyrouteinside",
         "/(group)",
+        "/redirect/$target",
         "/structural-sharing/$enabled",
+        "/redirect/",
         "/posts_/$postId/edit"
       ]
     },
@@ -486,6 +630,9 @@ export const routeTree = rootRoute
         "/_layout/_layout-2"
       ]
     },
+    "/anchor": {
+      "filePath": "anchor.tsx"
+    },
     "/posts": {
       "filePath": "posts.tsx",
       "children": [
@@ -532,6 +679,14 @@ export const routeTree = rootRoute
       "filePath": "posts.$postId.tsx",
       "parent": "/posts"
     },
+    "/redirect/$target": {
+      "filePath": "redirect/$target.tsx",
+      "children": [
+        "/redirect/$target/via-beforeLoad",
+        "/redirect/$target/via-loader",
+        "/redirect/$target/"
+      ]
+    },
     "/structural-sharing/$enabled": {
       "filePath": "structural-sharing.$enabled.tsx"
     },
@@ -539,6 +694,9 @@ export const routeTree = rootRoute
       "filePath": "posts.index.tsx",
       "parent": "/posts"
     },
+    "/redirect/": {
+      "filePath": "redirect/index.tsx"
+    },
     "/(group)/_layout/insidelayout": {
       "filePath": "(group)/_layout.insidelayout.tsx",
       "parent": "/(group)/_layout"
@@ -557,6 +715,18 @@ export const routeTree = rootRoute
     },
     "/posts_/$postId/edit": {
       "filePath": "posts_.$postId.edit.tsx"
+    },
+    "/redirect/$target/via-beforeLoad": {
+      "filePath": "redirect/$target/via-beforeLoad.tsx",
+      "parent": "/redirect/$target"
+    },
+    "/redirect/$target/via-loader": {
+      "filePath": "redirect/$target/via-loader.tsx",
+      "parent": "/redirect/$target"
+    },
+    "/redirect/$target/": {
+      "filePath": "redirect/$target/index.tsx",
+      "parent": "/redirect/$target"
     }
   }
 }
diff --git a/e2e/react-router/basic-file-based/src/routes/__root.tsx b/e2e/react-router/basic-file-based/src/routes/__root.tsx
index 975015e56..3e1e2220a 100644
--- a/e2e/react-router/basic-file-based/src/routes/__root.tsx
+++ b/e2e/react-router/basic-file-based/src/routes/__root.tsx
@@ -93,6 +93,14 @@ function RootComponent() {
         >
           Lazy Inside Group
         </Link>{' '}
+        <Link
+          to="/redirect"
+          activeProps={{
+            className: 'font-bold',
+          }}
+        >
+          redirect
+        </Link>{' '}
         <Link
           // @ts-expect-error
           to="/this-route-does-not-exist"
diff --git a/e2e/react-router/basic-file-based/src/routes/anchor.tsx b/e2e/react-router/basic-file-based/src/routes/anchor.tsx
new file mode 100644
index 000000000..c0c8cdd8a
--- /dev/null
+++ b/e2e/react-router/basic-file-based/src/routes/anchor.tsx
@@ -0,0 +1,223 @@
+import { useLayoutEffect, useRef, useState } from 'react'
+import {
+  Link,
+  createFileRoute,
+  useLocation,
+  useNavigate,
+} from '@tanstack/react-router'
+
+export const Route = createFileRoute('/anchor')({
+  component: AnchorComponent,
+})
+
+const anchors: Array<{
+  id: string
+  title: string
+  hashScrollIntoView?: boolean | ScrollIntoViewOptions
+}> = [
+  {
+    id: 'default-anchor',
+    title: 'Default Anchor',
+  },
+  {
+    id: 'false-anchor',
+    title: 'No Scroll Into View',
+    hashScrollIntoView: false,
+  },
+  {
+    id: 'smooth-scroll',
+    title: 'Smooth Scroll',
+    hashScrollIntoView: { behavior: 'smooth' },
+  },
+] as const
+
+function AnchorSection({ id, title }: { id: string; title: string }) {
+  const [hasShown, setHasShown] = useState(false)
+  const elementRef = useRef<HTMLHeadingElement>(null)
+
+  useLayoutEffect(() => {
+    const observer = new IntersectionObserver(
+      ([entry]) => {
+        if (!hasShown && entry.isIntersecting) {
+          setHasShown(true)
+        }
+      },
+      { threshold: 0.01 },
+    )
+
+    const currentRef = elementRef.current
+    if (currentRef) {
+      observer.observe(currentRef)
+    }
+
+    return () => {
+      if (currentRef) {
+        observer.unobserve(currentRef)
+      }
+    }
+  }, [hasShown])
+
+  return (
+    <div id={id} className="p-2 min-h-dvh">
+      <h1
+        data-testid={`heading-${id}`}
+        className="font-bold text-xl pt-10"
+        ref={elementRef}
+      >
+        {title}
+        {hasShown ? ' (shown)' : ''}
+      </h1>
+    </div>
+  )
+}
+
+function AnchorComponent() {
+  const navigate = useNavigate()
+  const location = useLocation()
+  const [withScroll, setWithScroll] = useState(true)
+
+  return (
+    <div className="flex flex-col w-full">
+      <nav className="sticky top-0 z-10 p-2 bg-gray-50 dark:bg-gray-900 border-b">
+        <ul className="inline-flex gap-2">
+          {anchors.map((anchor) => (
+            <li key={anchor.id}>
+              <Link
+                data-testid={`link-${anchor.id}`}
+                hash={anchor.id}
+                activeOptions={{ includeHash: true }}
+                activeProps={{
+                  className: 'font-bold active',
+                }}
+                hashScrollIntoView={anchor.hashScrollIntoView}
+              >
+                {anchor.title}
+              </Link>
+            </li>
+          ))}
+        </ul>
+      </nav>
+      <main className="overflow-auto">
+        <form
+          className="p-2 space-y-2 min-h-dvh"
+          onSubmit={(event) => {
+            event.preventDefault()
+            event.stopPropagation()
+            const formData = new FormData(event.target as HTMLFormElement)
+
+            const toHash = formData.get('hash') as string
+
+            if (!toHash) {
+              return
+            }
+
+            const hashScrollIntoView = withScroll
+              ? ({
+                  behavior: formData.get('scrollBehavior') as ScrollBehavior,
+                  block: formData.get('scrollBlock') as ScrollLogicalPosition,
+                  inline: formData.get('scrollInline') as ScrollLogicalPosition,
+                } satisfies ScrollIntoViewOptions)
+              : false
+
+            navigate({ hash: toHash, hashScrollIntoView })
+          }}
+        >
+          <h1 className="font-bold text-xl">Scroll with navigate</h1>
+          <div className="space-y-2">
+            <label>
+              <span>Target Anchor</span>
+              <select
+                className="border border-opacity-50 rounded p-2 w-full"
+                data-testid="hash-select"
+                defaultValue={location.hash || anchors[0].id}
+                name="hash"
+              >
+                {anchors.map((anchor) => (
+                  <option key={anchor.id} value={anchor.id}>
+                    {anchor.title}
+                  </option>
+                ))}
+              </select>
+            </label>
+            <div>
+              <label>
+                <input
+                  checked={withScroll}
+                  data-testid="with-scroll"
+                  onChange={(e) => setWithScroll(e.target.checked)}
+                  type="checkbox"
+                />{' '}
+                Scroll Into View
+              </label>
+            </div>
+          </div>
+          {withScroll ? (
+            <>
+              <div className="space-y-2">
+                <label>
+                  <span>Behavior</span>
+                  <select
+                    className="border border-opacity-50 rounded p-2 w-full"
+                    data-testid="behavior-select"
+                    defaultValue="instant"
+                    name="scrollBehavior"
+                  >
+                    <option value="instant">instant</option>
+                    <option value="smooth">smooth</option>
+                    <option value="auto">auto</option>
+                  </select>
+                </label>
+              </div>
+
+              <div className="space-y-2">
+                <label>
+                  <span>Block</span>
+                  <select
+                    className="border border-opacity-50 rounded p-2 w-full"
+                    data-testid="block-select"
+                    defaultValue="start"
+                    name="scrollBlock"
+                  >
+                    <option value="start">start</option>
+                    <option value="center">center</option>
+                    <option value="end">end</option>
+                    <option value="nearest">nearest</option>
+                  </select>
+                </label>
+              </div>
+
+              <div className="space-y-2">
+                <label>
+                  <span>Inline</span>
+                  <select
+                    className="border border-opacity-50 rounded p-2 w-full"
+                    data-testid="inline-select"
+                    defaultValue="nearest"
+                    name="scrollInline"
+                  >
+                    <option value="start">start</option>
+                    <option value="center">center</option>
+                    <option value="end">end</option>
+                    <option value="nearest">nearest</option>
+                  </select>
+                </label>
+              </div>
+            </>
+          ) : null}
+          <div>
+            <button
+              className="bg-blue-500 rounded p-2 uppercase text-white font-black disabled:opacity-50"
+              data-testid="navigate-button"
+            >
+              Navigate
+            </button>
+          </div>
+        </form>
+
+        {anchors.map((anchor) => (
+          <AnchorSection key={anchor.id} id={anchor.id} title={anchor.title} />
+        ))}
+      </main>
+    </div>
+  )
+}
diff --git a/e2e/react-router/basic-file-based/src/routes/posts.index.tsx b/e2e/react-router/basic-file-based/src/routes/posts.index.tsx
index 056433ca0..ab89fb5a6 100644
--- a/e2e/react-router/basic-file-based/src/routes/posts.index.tsx
+++ b/e2e/react-router/basic-file-based/src/routes/posts.index.tsx
@@ -6,5 +6,5 @@ export const Route = createFileRoute('/posts/')({
 })
 
 function PostsIndexComponent() {
-  return <div>Select a post.</div>
+  return <div data-testid="PostsIndexComponent">Select a post.</div>
 }
diff --git a/e2e/react-router/basic-file-based/src/routes/redirect/$target.tsx b/e2e/react-router/basic-file-based/src/routes/redirect/$target.tsx
new file mode 100644
index 000000000..0bda04ff4
--- /dev/null
+++ b/e2e/react-router/basic-file-based/src/routes/redirect/$target.tsx
@@ -0,0 +1,17 @@
+import { createFileRoute } from '@tanstack/react-router'
+import z from 'zod'
+
+export const Route = createFileRoute('/redirect/$target')({
+  params: {
+    parse: (p) =>
+      z
+        .object({
+          target: z.union([z.literal('internal'), z.literal('external')]),
+        })
+        .parse(p),
+  },
+  validateSearch: z.object({
+    reloadDocument: z.boolean().optional(),
+    preload: z.literal(false).optional(),
+  }),
+})
diff --git a/e2e/react-router/basic-file-based/src/routes/redirect/$target/index.tsx b/e2e/react-router/basic-file-based/src/routes/redirect/$target/index.tsx
new file mode 100644
index 000000000..57760cec8
--- /dev/null
+++ b/e2e/react-router/basic-file-based/src/routes/redirect/$target/index.tsx
@@ -0,0 +1,65 @@
+import { Link, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/redirect/$target/')({
+  component: () => {
+    const preload = Route.useSearch({ select: (s) => s.preload })
+    return (
+      <div>
+        <div className="mb-2">
+          <Link
+            from={Route.fullPath}
+            to="./via-beforeLoad"
+            activeProps={{
+              className: 'font-bold',
+            }}
+            preload={preload}
+            data-testid={'via-beforeLoad'}
+          >
+            via-beforeLoad
+          </Link>
+        </div>
+        <div className="mb-2">
+          <Link
+            from={Route.fullPath}
+            to="./via-beforeLoad"
+            search={{ reloadDocument: true }}
+            activeProps={{
+              className: 'font-bold',
+            }}
+            preload={preload}
+            data-testid={'via-beforeLoad-reloadDocument'}
+          >
+            via-beforeLoad (reloadDocument=true)
+          </Link>
+        </div>
+        <div className="mb-2">
+          <Link
+            from={Route.fullPath}
+            to="./via-loader"
+            activeProps={{
+              className: 'font-bold',
+            }}
+            preload={preload}
+            data-testid={'via-loader'}
+          >
+            via-loader
+          </Link>
+        </div>
+        <div className="mb-2">
+          <Link
+            from={Route.fullPath}
+            to="./via-loader"
+            activeProps={{
+              className: 'font-bold',
+            }}
+            search={{ reloadDocument: true }}
+            preload={preload}
+            data-testid={'via-loader-reloadDocument'}
+          >
+            via-loader (reloadDocument=true)
+          </Link>
+        </div>
+      </div>
+    )
+  },
+})
diff --git a/e2e/react-router/basic-file-based/src/routes/redirect/$target/via-beforeLoad.tsx b/e2e/react-router/basic-file-based/src/routes/redirect/$target/via-beforeLoad.tsx
new file mode 100644
index 000000000..d13eb5a34
--- /dev/null
+++ b/e2e/react-router/basic-file-based/src/routes/redirect/$target/via-beforeLoad.tsx
@@ -0,0 +1,13 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/redirect/$target/via-beforeLoad')({
+  beforeLoad: ({ params: { target }, search: { reloadDocument } }) => {
+    switch (target) {
+      case 'internal':
+        throw redirect({ to: '/posts', reloadDocument })
+      case 'external':
+        throw redirect({ href: 'http://example.com' })
+    }
+  },
+  component: () => <div>{Route.fullPath}</div>,
+})
diff --git a/e2e/react-router/basic-file-based/src/routes/redirect/$target/via-loader.tsx b/e2e/react-router/basic-file-based/src/routes/redirect/$target/via-loader.tsx
new file mode 100644
index 000000000..5cf0dc6de
--- /dev/null
+++ b/e2e/react-router/basic-file-based/src/routes/redirect/$target/via-loader.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/redirect/$target/via-loader')({
+  loaderDeps: ({ search: { reloadDocument } }) => ({ reloadDocument }),
+  loader: ({ params: { target }, deps: { reloadDocument } }) => {
+    switch (target) {
+      case 'internal':
+        throw redirect({ to: '/posts', reloadDocument })
+      case 'external':
+        throw redirect({ href: 'http://example.com' })
+    }
+  },
+  component: () => <div>{Route.fullPath}</div>,
+})
diff --git a/e2e/react-router/basic-file-based/src/routes/redirect/index.tsx b/e2e/react-router/basic-file-based/src/routes/redirect/index.tsx
new file mode 100644
index 000000000..c0b26a1df
--- /dev/null
+++ b/e2e/react-router/basic-file-based/src/routes/redirect/index.tsx
@@ -0,0 +1,28 @@
+import { Link, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/redirect/')({
+  component: () => (
+    <div>
+      <Link
+        from={Route.fullPath}
+        to="./$target"
+        params={{ target: 'internal' }}
+        activeProps={{
+          className: 'font-bold',
+        }}
+      >
+        internal
+      </Link>{' '}
+      <Link
+        from={Route.fullPath}
+        to="./$target"
+        params={{ target: 'external' }}
+        activeProps={{
+          className: 'font-bold',
+        }}
+      >
+        external
+      </Link>
+    </div>
+  ),
+})
diff --git a/e2e/react-router/basic-file-based/tests/redirect.spec.ts b/e2e/react-router/basic-file-based/tests/redirect.spec.ts
new file mode 100644
index 000000000..54d2ae11c
--- /dev/null
+++ b/e2e/react-router/basic-file-based/tests/redirect.spec.ts
@@ -0,0 +1,117 @@
+import { expect, test } from '@playwright/test'
+import combinateImport from 'combinate'
+import { derivePort } from '../../../utils'
+import packageJson from '../package.json' with { type: 'json' }
+
+// somehow playwright does not correctly import default exports
+const combinate = (combinateImport as any).default as typeof combinateImport
+
+const PORT = derivePort(packageJson.name)
+
+test.describe('redirects', () => {
+  const internalNavigationTestMatrix = combinate({
+    thrower: ['beforeLoad', 'loader'] as const,
+    reloadDocument: [false, true] as const,
+    preload: [false, true] as const,
+  })
+
+  internalNavigationTestMatrix.forEach(
+    ({ thrower, reloadDocument, preload }) => {
+      test(`internal target, navigation: thrower: ${thrower}, reloadDocument: ${reloadDocument}, preload: ${preload}`, async ({
+        page,
+      }) => {
+        await page.waitForLoadState('networkidle')
+        await page.goto(
+          `/redirect/internal${preload === false ? '?preload=false' : ''}`,
+        )
+        const link = page.getByTestId(
+          `via-${thrower}${reloadDocument ? '-reloadDocument' : ''}`,
+        )
+
+        await page.waitForLoadState('networkidle')
+        let requestHappened = false
+
+        const requestPromise = new Promise<void>((resolve) => {
+          page.on('request', (request) => {
+            if (
+              request.url() === 'https://jsonplaceholder.typicode.com/posts'
+            ) {
+              requestHappened = true
+              resolve()
+            }
+          })
+        })
+        await link.focus()
+
+        const expectRequestHappened = preload && !reloadDocument
+        const timeoutPromise = new Promise((resolve) =>
+          setTimeout(resolve, expectRequestHappened ? 5000 : 500),
+        )
+        await Promise.race([requestPromise, timeoutPromise])
+        expect(requestHappened).toBe(expectRequestHappened)
+        await link.click()
+        let fullPageLoad = false
+        page.on('domcontentloaded', () => {
+          fullPageLoad = true
+        })
+
+        const url = `http://localhost:${PORT}/posts`
+
+        await page.waitForURL(url)
+        expect(page.url()).toBe(url)
+        await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport()
+        expect(fullPageLoad).toBe(reloadDocument)
+      })
+    },
+  )
+
+  const internalDirectVisitTestMatrix = combinate({
+    thrower: ['beforeLoad', 'loader'] as const,
+    reloadDocument: [false, true] as const,
+    preload: [false, true] as const,
+  })
+
+  internalDirectVisitTestMatrix.forEach(
+    ({ thrower, reloadDocument, preload }) => {
+      test(`internal target, direct visit: thrower: ${thrower}, reloadDocument: ${reloadDocument}, preload: ${preload}`, async ({
+        page,
+      }) => {
+        await page.waitForLoadState('networkidle')
+
+        await page.goto(`/redirect/internal/via-${thrower}`)
+
+        const url = `http://localhost:${PORT}/posts`
+
+        await page.waitForURL(url)
+        expect(page.url()).toBe(url)
+        await page.waitForLoadState('networkidle')
+        await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport()
+      })
+    },
+  )
+
+  const externalTestMatrix = combinate({
+    scenario: ['navigate', 'direct_visit'] as const,
+    thrower: ['beforeLoad', 'loader'] as const,
+  })
+
+  externalTestMatrix.forEach(({ scenario, thrower }) => {
+    test(`external target: scenario: ${scenario}, thrower: ${thrower}`, async ({
+      page,
+    }) => {
+      await page.waitForLoadState('networkidle')
+
+      if (scenario === 'navigate') {
+        await page.goto(`/redirect/external`)
+        await page.getByTestId(`via-${thrower}`).click()
+      } else {
+        await page.goto(`/redirect/external/via-${thrower}`)
+      }
+
+      const url = 'http://example.com/'
+
+      await page.waitForURL(url)
+      expect(page.url()).toBe(url)
+    })
+  })
+})
diff --git a/e2e/react-router/basic-file-based/tests/scroll-into-view.spec.ts b/e2e/react-router/basic-file-based/tests/scroll-into-view.spec.ts
new file mode 100644
index 000000000..d1af86adf
--- /dev/null
+++ b/e2e/react-router/basic-file-based/tests/scroll-into-view.spec.ts
@@ -0,0 +1,213 @@
+import { expect, test } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+const anchors = {
+  defaultAnchor: 'default-anchor',
+  noScrollIntoView: 'false-anchor',
+  smoothScroll: 'smooth-scroll',
+} as const
+
+const formTestIds = {
+  targetAnchor: 'hash-select',
+  scrollIntoView: 'with-scroll',
+  behaviorSelect: 'behavior-select',
+  blockSelect: 'block-select',
+  inlineSelect: 'inline-select',
+  navigateButton: 'navigate-button',
+}
+
+const shownSuffix = '(shown)'
+
+const activeClass = 'font-bold active'
+
+function getAnchorTarget(page: Page, anchor: string) {
+  return page.getByTestId(`heading-${anchor}`)
+}
+
+function getAnchorLink(page: Page, anchor: string) {
+  return page.getByTestId(`link-${anchor}`)
+}
+
+test.beforeEach(async ({ page }) => {
+  await page.goto('/anchor')
+})
+
+// Testing the `Link` component with the `hashScrollIntoView` prop
+test('Navigating via anchor `Link` with default hash scrolling behavior', async ({
+  page,
+}) => {
+  await expect(getAnchorTarget(page, anchors.defaultAnchor)).not.toContainText(
+    shownSuffix,
+  )
+
+  await getAnchorLink(page, anchors.defaultAnchor).click()
+
+  await expect(getAnchorTarget(page, anchors.defaultAnchor)).toBeVisible()
+  await expect(getAnchorTarget(page, anchors.defaultAnchor)).toContainText(
+    shownSuffix,
+  )
+
+  await expect(getAnchorLink(page, anchors.defaultAnchor)).toHaveClass(
+    activeClass,
+  )
+})
+
+test('Navigating via anchor `Link` with hash scrolling disabled', async ({
+  page,
+}) => {
+  const initialScrollPosition = await page.evaluate(() => window.scrollY)
+
+  await expect(
+    getAnchorTarget(page, anchors.noScrollIntoView),
+  ).not.toContainText(shownSuffix)
+
+  await getAnchorLink(page, anchors.noScrollIntoView).click()
+
+  // The active anchor should have updated
+  await expect(getAnchorLink(page, anchors.noScrollIntoView)).toHaveClass(
+    activeClass,
+  )
+
+  // The anchor should not have been visible, because the scroll should not have been activated
+  await expect(getAnchorTarget(page, anchors.defaultAnchor)).not.toContainText(
+    shownSuffix,
+  )
+
+  // Expect the same scroll position as before
+  expect(await page.evaluate(() => window.scrollY)).toBe(initialScrollPosition)
+})
+
+test('Navigating via anchor `Link` with smooth hash scrolling behavior', async ({
+  page,
+}) => {
+  await expect(getAnchorTarget(page, anchors.smoothScroll)).not.toContainText(
+    shownSuffix,
+  )
+
+  await getAnchorLink(page, anchors.smoothScroll).click()
+  await expect(getAnchorTarget(page, anchors.smoothScroll)).toBeVisible()
+
+  // Smooth scrolling should activate the IntersectionObserver on all headings, making them all render "(shown)"
+  await expect(getAnchorTarget(page, anchors.defaultAnchor)).toContainText(
+    shownSuffix,
+  )
+  await expect(getAnchorTarget(page, anchors.noScrollIntoView)).toContainText(
+    shownSuffix,
+  )
+  await expect(getAnchorTarget(page, anchors.smoothScroll)).toContainText(
+    shownSuffix,
+  )
+
+  await expect(getAnchorLink(page, anchors.smoothScroll)).toHaveClass(
+    activeClass,
+  )
+})
+
+// Testing the `useNavigate` hook with the `hashScrollIntoView` option
+test('Navigating via `useNavigate` with instant scroll behavior', async ({
+  page,
+}) => {
+  await expect(getAnchorTarget(page, anchors.smoothScroll)).not.toContainText(
+    shownSuffix,
+  )
+
+  // Scroll to the last anchor instantly, should not activate Intersection Observers for the other anchors
+  await page.getByTestId(formTestIds.targetAnchor).selectOption('Smooth Scroll')
+  await page.getByTestId(formTestIds.scrollIntoView).check()
+  await page.getByTestId(formTestIds.behaviorSelect).selectOption('instant')
+  await page.getByTestId(formTestIds.blockSelect).selectOption('start')
+  await page.getByTestId(formTestIds.inlineSelect).selectOption('nearest')
+  await page.getByTestId(formTestIds.navigateButton).click()
+
+  await expect(getAnchorTarget(page, anchors.defaultAnchor)).not.toContainText(
+    shownSuffix,
+  )
+
+  await expect(
+    getAnchorTarget(page, anchors.noScrollIntoView),
+  ).not.toContainText(shownSuffix)
+
+  await expect(getAnchorTarget(page, anchors.smoothScroll)).toContainText(
+    shownSuffix,
+  )
+
+  await expect(getAnchorLink(page, anchors.smoothScroll)).toBeVisible()
+
+  await expect(getAnchorLink(page, anchors.smoothScroll)).toHaveClass(
+    activeClass,
+  )
+})
+
+test('Navigating via `useNavigate` with scrollIntoView disabled', async ({
+  page,
+}) => {
+  const initialScrollPosition = await page.evaluate(() => window.scrollY)
+
+  await expect(getAnchorTarget(page, anchors.defaultAnchor)).not.toContainText(
+    shownSuffix,
+  )
+
+  await expect(
+    getAnchorTarget(page, anchors.noScrollIntoView),
+  ).not.toContainText(shownSuffix)
+
+  await expect(getAnchorTarget(page, anchors.smoothScroll)).not.toContainText(
+    shownSuffix,
+  )
+
+  // Navigate to the last anchor, but with scrollIntoView disabled should not activate Intersection Observers for any anchors
+  await page.getByTestId(formTestIds.targetAnchor).selectOption('Smooth Scroll')
+  await page.getByTestId(formTestIds.scrollIntoView).uncheck()
+  await page.getByTestId(formTestIds.navigateButton).click()
+
+  await expect(getAnchorTarget(page, anchors.defaultAnchor)).not.toContainText(
+    shownSuffix,
+  )
+
+  await expect(
+    getAnchorTarget(page, anchors.noScrollIntoView),
+  ).not.toContainText(shownSuffix)
+
+  await expect(getAnchorTarget(page, anchors.smoothScroll)).not.toContainText(
+    shownSuffix,
+  )
+
+  await expect(getAnchorLink(page, anchors.smoothScroll)).toHaveClass(
+    activeClass,
+  )
+
+  // Expect the same scroll position as before
+  expect(await page.evaluate(() => window.scrollY)).toBe(initialScrollPosition)
+})
+
+test('Navigating via `useNavigate` with smooth scroll behavior', async ({
+  page,
+}) => {
+  await expect(getAnchorTarget(page, anchors.smoothScroll)).not.toContainText(
+    shownSuffix,
+  )
+
+  // Scroll to the last anchor smoothly, should activate Intersection Observers for the other anchors, making them all render "(shown)"
+  await page.getByTestId(formTestIds.targetAnchor).selectOption('Smooth Scroll')
+  await page.getByTestId(formTestIds.scrollIntoView).check()
+  await page.getByTestId(formTestIds.behaviorSelect).selectOption('smooth')
+  await page.getByTestId(formTestIds.blockSelect).selectOption('start')
+  await page.getByTestId(formTestIds.inlineSelect).selectOption('nearest')
+  await page.getByTestId(formTestIds.navigateButton).click()
+
+  await expect(getAnchorTarget(page, anchors.defaultAnchor)).toContainText(
+    shownSuffix,
+  )
+
+  await expect(getAnchorTarget(page, anchors.noScrollIntoView)).toContainText(
+    shownSuffix,
+  )
+
+  await expect(getAnchorTarget(page, anchors.smoothScroll)).toContainText(
+    shownSuffix,
+  )
+
+  await expect(getAnchorLink(page, anchors.smoothScroll)).toHaveClass(
+    activeClass,
+  )
+})
diff --git a/e2e/start/basic/app/components/RedirectOnClick.tsx b/e2e/start/basic/app/components/RedirectOnClick.tsx
new file mode 100644
index 000000000..97bf80683
--- /dev/null
+++ b/e2e/start/basic/app/components/RedirectOnClick.tsx
@@ -0,0 +1,22 @@
+import { useServerFn } from '@tanstack/start'
+import { throwRedirect } from './throwRedirect'
+
+interface RedirectOnClickProps {
+  target: 'internal' | 'external'
+  reloadDocument?: boolean
+}
+
+export function RedirectOnClick({
+  target,
+  reloadDocument,
+}: RedirectOnClickProps) {
+  const execute = useServerFn(throwRedirect)
+  return (
+    <button
+      data-testid="redirect-on-click"
+      onClick={() => execute({ data: { target, reloadDocument } })}
+    >
+      click me
+    </button>
+  )
+}
diff --git a/e2e/start/basic/app/components/throwRedirect.ts b/e2e/start/basic/app/components/throwRedirect.ts
new file mode 100644
index 000000000..5a722482b
--- /dev/null
+++ b/e2e/start/basic/app/components/throwRedirect.ts
@@ -0,0 +1,16 @@
+import { redirect } from '@tanstack/react-router'
+import { createServerFn } from '@tanstack/start'
+
+export const throwRedirect = createServerFn()
+  .validator(
+    (opts: { target: 'internal' | 'external'; reloadDocument?: boolean }) =>
+      opts,
+  )
+  .handler((ctx) => {
+    if (ctx.data.target === 'internal') {
+      throw redirect({ to: '/posts', reloadDocument: ctx.data.reloadDocument })
+    }
+    throw redirect({
+      href: 'http://example.com',
+    })
+  })
diff --git a/e2e/start/basic/app/routeTree.gen.ts b/e2e/start/basic/app/routeTree.gen.ts
index 5f29d2300..97f04f768 100644
--- a/e2e/start/basic/app/routeTree.gen.ts
+++ b/e2e/start/basic/app/routeTree.gen.ts
@@ -15,21 +15,30 @@ import { Route as UsersImport } from './routes/users'
 import { Route as StatusImport } from './routes/status'
 import { Route as ServerFnsImport } from './routes/server-fns'
 import { Route as SearchParamsImport } from './routes/search-params'
-import { Route as RedirectImport } from './routes/redirect'
 import { Route as PostsImport } from './routes/posts'
+import { Route as LinksImport } from './routes/links'
 import { Route as IsomorphicFnsImport } from './routes/isomorphic-fns'
 import { Route as EnvOnlyImport } from './routes/env-only'
 import { Route as DeferredImport } from './routes/deferred'
 import { Route as LayoutImport } from './routes/_layout'
 import { Route as IndexImport } from './routes/index'
 import { Route as UsersIndexImport } from './routes/users.index'
+import { Route as RedirectIndexImport } from './routes/redirect/index'
 import { Route as PostsIndexImport } from './routes/posts.index'
 import { Route as UsersUserIdImport } from './routes/users.$userId'
+import { Route as RedirectTargetImport } from './routes/redirect/$target'
 import { Route as PostsPostIdImport } from './routes/posts.$postId'
 import { Route as LayoutLayout2Import } from './routes/_layout/_layout-2'
+import { Route as RedirectTargetIndexImport } from './routes/redirect/$target/index'
+import { Route as RedirectTargetViaLoaderImport } from './routes/redirect/$target/via-loader'
+import { Route as RedirectTargetViaBeforeLoadImport } from './routes/redirect/$target/via-beforeLoad'
 import { Route as PostsPostIdDeepImport } from './routes/posts_.$postId.deep'
 import { Route as LayoutLayout2LayoutBImport } from './routes/_layout/_layout-2/layout-b'
 import { Route as LayoutLayout2LayoutAImport } from './routes/_layout/_layout-2/layout-a'
+import { Route as RedirectTargetServerFnIndexImport } from './routes/redirect/$target/serverFn/index'
+import { Route as RedirectTargetServerFnViaUseServerFnImport } from './routes/redirect/$target/serverFn/via-useServerFn'
+import { Route as RedirectTargetServerFnViaLoaderImport } from './routes/redirect/$target/serverFn/via-loader'
+import { Route as RedirectTargetServerFnViaBeforeLoadImport } from './routes/redirect/$target/serverFn/via-beforeLoad'
 
 // Create/Update Routes
 
@@ -57,18 +66,18 @@ const SearchParamsRoute = SearchParamsImport.update({
   getParentRoute: () => rootRoute,
 } as any)
 
-const RedirectRoute = RedirectImport.update({
-  id: '/redirect',
-  path: '/redirect',
-  getParentRoute: () => rootRoute,
-} as any)
-
 const PostsRoute = PostsImport.update({
   id: '/posts',
   path: '/posts',
   getParentRoute: () => rootRoute,
 } as any)
 
+const LinksRoute = LinksImport.update({
+  id: '/links',
+  path: '/links',
+  getParentRoute: () => rootRoute,
+} as any)
+
 const IsomorphicFnsRoute = IsomorphicFnsImport.update({
   id: '/isomorphic-fns',
   path: '/isomorphic-fns',
@@ -104,6 +113,12 @@ const UsersIndexRoute = UsersIndexImport.update({
   getParentRoute: () => UsersRoute,
 } as any)
 
+const RedirectIndexRoute = RedirectIndexImport.update({
+  id: '/redirect/',
+  path: '/redirect/',
+  getParentRoute: () => rootRoute,
+} as any)
+
 const PostsIndexRoute = PostsIndexImport.update({
   id: '/',
   path: '/',
@@ -116,6 +131,12 @@ const UsersUserIdRoute = UsersUserIdImport.update({
   getParentRoute: () => UsersRoute,
 } as any)
 
+const RedirectTargetRoute = RedirectTargetImport.update({
+  id: '/redirect/$target',
+  path: '/redirect/$target',
+  getParentRoute: () => rootRoute,
+} as any)
+
 const PostsPostIdRoute = PostsPostIdImport.update({
   id: '/$postId',
   path: '/$postId',
@@ -127,6 +148,25 @@ const LayoutLayout2Route = LayoutLayout2Import.update({
   getParentRoute: () => LayoutRoute,
 } as any)
 
+const RedirectTargetIndexRoute = RedirectTargetIndexImport.update({
+  id: '/',
+  path: '/',
+  getParentRoute: () => RedirectTargetRoute,
+} as any)
+
+const RedirectTargetViaLoaderRoute = RedirectTargetViaLoaderImport.update({
+  id: '/via-loader',
+  path: '/via-loader',
+  getParentRoute: () => RedirectTargetRoute,
+} as any)
+
+const RedirectTargetViaBeforeLoadRoute =
+  RedirectTargetViaBeforeLoadImport.update({
+    id: '/via-beforeLoad',
+    path: '/via-beforeLoad',
+    getParentRoute: () => RedirectTargetRoute,
+  } as any)
+
 const PostsPostIdDeepRoute = PostsPostIdDeepImport.update({
   id: '/posts_/$postId/deep',
   path: '/posts/$postId/deep',
@@ -145,6 +185,34 @@ const LayoutLayout2LayoutARoute = LayoutLayout2LayoutAImport.update({
   getParentRoute: () => LayoutLayout2Route,
 } as any)
 
+const RedirectTargetServerFnIndexRoute =
+  RedirectTargetServerFnIndexImport.update({
+    id: '/serverFn/',
+    path: '/serverFn/',
+    getParentRoute: () => RedirectTargetRoute,
+  } as any)
+
+const RedirectTargetServerFnViaUseServerFnRoute =
+  RedirectTargetServerFnViaUseServerFnImport.update({
+    id: '/serverFn/via-useServerFn',
+    path: '/serverFn/via-useServerFn',
+    getParentRoute: () => RedirectTargetRoute,
+  } as any)
+
+const RedirectTargetServerFnViaLoaderRoute =
+  RedirectTargetServerFnViaLoaderImport.update({
+    id: '/serverFn/via-loader',
+    path: '/serverFn/via-loader',
+    getParentRoute: () => RedirectTargetRoute,
+  } as any)
+
+const RedirectTargetServerFnViaBeforeLoadRoute =
+  RedirectTargetServerFnViaBeforeLoadImport.update({
+    id: '/serverFn/via-beforeLoad',
+    path: '/serverFn/via-beforeLoad',
+    getParentRoute: () => RedirectTargetRoute,
+  } as any)
+
 // Populate the FileRoutesByPath interface
 
 declare module '@tanstack/react-router' {
@@ -184,6 +252,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof IsomorphicFnsImport
       parentRoute: typeof rootRoute
     }
+    '/links': {
+      id: '/links'
+      path: '/links'
+      fullPath: '/links'
+      preLoaderRoute: typeof LinksImport
+      parentRoute: typeof rootRoute
+    }
     '/posts': {
       id: '/posts'
       path: '/posts'
@@ -191,13 +266,6 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof PostsImport
       parentRoute: typeof rootRoute
     }
-    '/redirect': {
-      id: '/redirect'
-      path: '/redirect'
-      fullPath: '/redirect'
-      preLoaderRoute: typeof RedirectImport
-      parentRoute: typeof rootRoute
-    }
     '/search-params': {
       id: '/search-params'
       path: '/search-params'
@@ -240,6 +308,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof PostsPostIdImport
       parentRoute: typeof PostsImport
     }
+    '/redirect/$target': {
+      id: '/redirect/$target'
+      path: '/redirect/$target'
+      fullPath: '/redirect/$target'
+      preLoaderRoute: typeof RedirectTargetImport
+      parentRoute: typeof rootRoute
+    }
     '/users/$userId': {
       id: '/users/$userId'
       path: '/$userId'
@@ -254,6 +329,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof PostsIndexImport
       parentRoute: typeof PostsImport
     }
+    '/redirect/': {
+      id: '/redirect/'
+      path: '/redirect'
+      fullPath: '/redirect'
+      preLoaderRoute: typeof RedirectIndexImport
+      parentRoute: typeof rootRoute
+    }
     '/users/': {
       id: '/users/'
       path: '/'
@@ -282,6 +364,55 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof PostsPostIdDeepImport
       parentRoute: typeof rootRoute
     }
+    '/redirect/$target/via-beforeLoad': {
+      id: '/redirect/$target/via-beforeLoad'
+      path: '/via-beforeLoad'
+      fullPath: '/redirect/$target/via-beforeLoad'
+      preLoaderRoute: typeof RedirectTargetViaBeforeLoadImport
+      parentRoute: typeof RedirectTargetImport
+    }
+    '/redirect/$target/via-loader': {
+      id: '/redirect/$target/via-loader'
+      path: '/via-loader'
+      fullPath: '/redirect/$target/via-loader'
+      preLoaderRoute: typeof RedirectTargetViaLoaderImport
+      parentRoute: typeof RedirectTargetImport
+    }
+    '/redirect/$target/': {
+      id: '/redirect/$target/'
+      path: '/'
+      fullPath: '/redirect/$target/'
+      preLoaderRoute: typeof RedirectTargetIndexImport
+      parentRoute: typeof RedirectTargetImport
+    }
+    '/redirect/$target/serverFn/via-beforeLoad': {
+      id: '/redirect/$target/serverFn/via-beforeLoad'
+      path: '/serverFn/via-beforeLoad'
+      fullPath: '/redirect/$target/serverFn/via-beforeLoad'
+      preLoaderRoute: typeof RedirectTargetServerFnViaBeforeLoadImport
+      parentRoute: typeof RedirectTargetImport
+    }
+    '/redirect/$target/serverFn/via-loader': {
+      id: '/redirect/$target/serverFn/via-loader'
+      path: '/serverFn/via-loader'
+      fullPath: '/redirect/$target/serverFn/via-loader'
+      preLoaderRoute: typeof RedirectTargetServerFnViaLoaderImport
+      parentRoute: typeof RedirectTargetImport
+    }
+    '/redirect/$target/serverFn/via-useServerFn': {
+      id: '/redirect/$target/serverFn/via-useServerFn'
+      path: '/serverFn/via-useServerFn'
+      fullPath: '/redirect/$target/serverFn/via-useServerFn'
+      preLoaderRoute: typeof RedirectTargetServerFnViaUseServerFnImport
+      parentRoute: typeof RedirectTargetImport
+    }
+    '/redirect/$target/serverFn/': {
+      id: '/redirect/$target/serverFn/'
+      path: '/serverFn'
+      fullPath: '/redirect/$target/serverFn'
+      preLoaderRoute: typeof RedirectTargetServerFnIndexImport
+      parentRoute: typeof RedirectTargetImport
+    }
   }
 }
 
@@ -336,25 +467,60 @@ const UsersRouteChildren: UsersRouteChildren = {
 
 const UsersRouteWithChildren = UsersRoute._addFileChildren(UsersRouteChildren)
 
+interface RedirectTargetRouteChildren {
+  RedirectTargetViaBeforeLoadRoute: typeof RedirectTargetViaBeforeLoadRoute
+  RedirectTargetViaLoaderRoute: typeof RedirectTargetViaLoaderRoute
+  RedirectTargetIndexRoute: typeof RedirectTargetIndexRoute
+  RedirectTargetServerFnViaBeforeLoadRoute: typeof RedirectTargetServerFnViaBeforeLoadRoute
+  RedirectTargetServerFnViaLoaderRoute: typeof RedirectTargetServerFnViaLoaderRoute
+  RedirectTargetServerFnViaUseServerFnRoute: typeof RedirectTargetServerFnViaUseServerFnRoute
+  RedirectTargetServerFnIndexRoute: typeof RedirectTargetServerFnIndexRoute
+}
+
+const RedirectTargetRouteChildren: RedirectTargetRouteChildren = {
+  RedirectTargetViaBeforeLoadRoute: RedirectTargetViaBeforeLoadRoute,
+  RedirectTargetViaLoaderRoute: RedirectTargetViaLoaderRoute,
+  RedirectTargetIndexRoute: RedirectTargetIndexRoute,
+  RedirectTargetServerFnViaBeforeLoadRoute:
+    RedirectTargetServerFnViaBeforeLoadRoute,
+  RedirectTargetServerFnViaLoaderRoute: RedirectTargetServerFnViaLoaderRoute,
+  RedirectTargetServerFnViaUseServerFnRoute:
+    RedirectTargetServerFnViaUseServerFnRoute,
+  RedirectTargetServerFnIndexRoute: RedirectTargetServerFnIndexRoute,
+}
+
+const RedirectTargetRouteWithChildren = RedirectTargetRoute._addFileChildren(
+  RedirectTargetRouteChildren,
+)
+
 export interface FileRoutesByFullPath {
   '/': typeof IndexRoute
   '': typeof LayoutLayout2RouteWithChildren
   '/deferred': typeof DeferredRoute
   '/env-only': typeof EnvOnlyRoute
   '/isomorphic-fns': typeof IsomorphicFnsRoute
+  '/links': typeof LinksRoute
   '/posts': typeof PostsRouteWithChildren
-  '/redirect': typeof RedirectRoute
   '/search-params': typeof SearchParamsRoute
   '/server-fns': typeof ServerFnsRoute
   '/status': typeof StatusRoute
   '/users': typeof UsersRouteWithChildren
   '/posts/$postId': typeof PostsPostIdRoute
+  '/redirect/$target': typeof RedirectTargetRouteWithChildren
   '/users/$userId': typeof UsersUserIdRoute
   '/posts/': typeof PostsIndexRoute
+  '/redirect': typeof RedirectIndexRoute
   '/users/': typeof UsersIndexRoute
   '/layout-a': typeof LayoutLayout2LayoutARoute
   '/layout-b': typeof LayoutLayout2LayoutBRoute
   '/posts/$postId/deep': typeof PostsPostIdDeepRoute
+  '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
+  '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+  '/redirect/$target/': typeof RedirectTargetIndexRoute
+  '/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute
+  '/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute
+  '/redirect/$target/serverFn/via-useServerFn': typeof RedirectTargetServerFnViaUseServerFnRoute
+  '/redirect/$target/serverFn': typeof RedirectTargetServerFnIndexRoute
 }
 
 export interface FileRoutesByTo {
@@ -363,17 +529,25 @@ export interface FileRoutesByTo {
   '/deferred': typeof DeferredRoute
   '/env-only': typeof EnvOnlyRoute
   '/isomorphic-fns': typeof IsomorphicFnsRoute
-  '/redirect': typeof RedirectRoute
+  '/links': typeof LinksRoute
   '/search-params': typeof SearchParamsRoute
   '/server-fns': typeof ServerFnsRoute
   '/status': typeof StatusRoute
   '/posts/$postId': typeof PostsPostIdRoute
   '/users/$userId': typeof UsersUserIdRoute
   '/posts': typeof PostsIndexRoute
+  '/redirect': typeof RedirectIndexRoute
   '/users': typeof UsersIndexRoute
   '/layout-a': typeof LayoutLayout2LayoutARoute
   '/layout-b': typeof LayoutLayout2LayoutBRoute
   '/posts/$postId/deep': typeof PostsPostIdDeepRoute
+  '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
+  '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+  '/redirect/$target': typeof RedirectTargetIndexRoute
+  '/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute
+  '/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute
+  '/redirect/$target/serverFn/via-useServerFn': typeof RedirectTargetServerFnViaUseServerFnRoute
+  '/redirect/$target/serverFn': typeof RedirectTargetServerFnIndexRoute
 }
 
 export interface FileRoutesById {
@@ -383,20 +557,29 @@ export interface FileRoutesById {
   '/deferred': typeof DeferredRoute
   '/env-only': typeof EnvOnlyRoute
   '/isomorphic-fns': typeof IsomorphicFnsRoute
+  '/links': typeof LinksRoute
   '/posts': typeof PostsRouteWithChildren
-  '/redirect': typeof RedirectRoute
   '/search-params': typeof SearchParamsRoute
   '/server-fns': typeof ServerFnsRoute
   '/status': typeof StatusRoute
   '/users': typeof UsersRouteWithChildren
   '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
   '/posts/$postId': typeof PostsPostIdRoute
+  '/redirect/$target': typeof RedirectTargetRouteWithChildren
   '/users/$userId': typeof UsersUserIdRoute
   '/posts/': typeof PostsIndexRoute
+  '/redirect/': typeof RedirectIndexRoute
   '/users/': typeof UsersIndexRoute
   '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute
   '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute
   '/posts_/$postId/deep': typeof PostsPostIdDeepRoute
+  '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
+  '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+  '/redirect/$target/': typeof RedirectTargetIndexRoute
+  '/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute
+  '/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute
+  '/redirect/$target/serverFn/via-useServerFn': typeof RedirectTargetServerFnViaUseServerFnRoute
+  '/redirect/$target/serverFn/': typeof RedirectTargetServerFnIndexRoute
 }
 
 export interface FileRouteTypes {
@@ -407,19 +590,28 @@ export interface FileRouteTypes {
     | '/deferred'
     | '/env-only'
     | '/isomorphic-fns'
+    | '/links'
     | '/posts'
-    | '/redirect'
     | '/search-params'
     | '/server-fns'
     | '/status'
     | '/users'
     | '/posts/$postId'
+    | '/redirect/$target'
     | '/users/$userId'
     | '/posts/'
+    | '/redirect'
     | '/users/'
     | '/layout-a'
     | '/layout-b'
     | '/posts/$postId/deep'
+    | '/redirect/$target/via-beforeLoad'
+    | '/redirect/$target/via-loader'
+    | '/redirect/$target/'
+    | '/redirect/$target/serverFn/via-beforeLoad'
+    | '/redirect/$target/serverFn/via-loader'
+    | '/redirect/$target/serverFn/via-useServerFn'
+    | '/redirect/$target/serverFn'
   fileRoutesByTo: FileRoutesByTo
   to:
     | '/'
@@ -427,17 +619,25 @@ export interface FileRouteTypes {
     | '/deferred'
     | '/env-only'
     | '/isomorphic-fns'
-    | '/redirect'
+    | '/links'
     | '/search-params'
     | '/server-fns'
     | '/status'
     | '/posts/$postId'
     | '/users/$userId'
     | '/posts'
+    | '/redirect'
     | '/users'
     | '/layout-a'
     | '/layout-b'
     | '/posts/$postId/deep'
+    | '/redirect/$target/via-beforeLoad'
+    | '/redirect/$target/via-loader'
+    | '/redirect/$target'
+    | '/redirect/$target/serverFn/via-beforeLoad'
+    | '/redirect/$target/serverFn/via-loader'
+    | '/redirect/$target/serverFn/via-useServerFn'
+    | '/redirect/$target/serverFn'
   id:
     | '__root__'
     | '/'
@@ -445,20 +645,29 @@ export interface FileRouteTypes {
     | '/deferred'
     | '/env-only'
     | '/isomorphic-fns'
+    | '/links'
     | '/posts'
-    | '/redirect'
     | '/search-params'
     | '/server-fns'
     | '/status'
     | '/users'
     | '/_layout/_layout-2'
     | '/posts/$postId'
+    | '/redirect/$target'
     | '/users/$userId'
     | '/posts/'
+    | '/redirect/'
     | '/users/'
     | '/_layout/_layout-2/layout-a'
     | '/_layout/_layout-2/layout-b'
     | '/posts_/$postId/deep'
+    | '/redirect/$target/via-beforeLoad'
+    | '/redirect/$target/via-loader'
+    | '/redirect/$target/'
+    | '/redirect/$target/serverFn/via-beforeLoad'
+    | '/redirect/$target/serverFn/via-loader'
+    | '/redirect/$target/serverFn/via-useServerFn'
+    | '/redirect/$target/serverFn/'
   fileRoutesById: FileRoutesById
 }
 
@@ -468,12 +677,14 @@ export interface RootRouteChildren {
   DeferredRoute: typeof DeferredRoute
   EnvOnlyRoute: typeof EnvOnlyRoute
   IsomorphicFnsRoute: typeof IsomorphicFnsRoute
+  LinksRoute: typeof LinksRoute
   PostsRoute: typeof PostsRouteWithChildren
-  RedirectRoute: typeof RedirectRoute
   SearchParamsRoute: typeof SearchParamsRoute
   ServerFnsRoute: typeof ServerFnsRoute
   StatusRoute: typeof StatusRoute
   UsersRoute: typeof UsersRouteWithChildren
+  RedirectTargetRoute: typeof RedirectTargetRouteWithChildren
+  RedirectIndexRoute: typeof RedirectIndexRoute
   PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute
 }
 
@@ -483,12 +694,14 @@ const rootRouteChildren: RootRouteChildren = {
   DeferredRoute: DeferredRoute,
   EnvOnlyRoute: EnvOnlyRoute,
   IsomorphicFnsRoute: IsomorphicFnsRoute,
+  LinksRoute: LinksRoute,
   PostsRoute: PostsRouteWithChildren,
-  RedirectRoute: RedirectRoute,
   SearchParamsRoute: SearchParamsRoute,
   ServerFnsRoute: ServerFnsRoute,
   StatusRoute: StatusRoute,
   UsersRoute: UsersRouteWithChildren,
+  RedirectTargetRoute: RedirectTargetRouteWithChildren,
+  RedirectIndexRoute: RedirectIndexRoute,
   PostsPostIdDeepRoute: PostsPostIdDeepRoute,
 }
 
@@ -507,12 +720,14 @@ export const routeTree = rootRoute
         "/deferred",
         "/env-only",
         "/isomorphic-fns",
+        "/links",
         "/posts",
-        "/redirect",
         "/search-params",
         "/server-fns",
         "/status",
         "/users",
+        "/redirect/$target",
+        "/redirect/",
         "/posts_/$postId/deep"
       ]
     },
@@ -534,6 +749,9 @@ export const routeTree = rootRoute
     "/isomorphic-fns": {
       "filePath": "isomorphic-fns.tsx"
     },
+    "/links": {
+      "filePath": "links.tsx"
+    },
     "/posts": {
       "filePath": "posts.tsx",
       "children": [
@@ -541,9 +759,6 @@ export const routeTree = rootRoute
         "/posts/"
       ]
     },
-    "/redirect": {
-      "filePath": "redirect.tsx"
-    },
     "/search-params": {
       "filePath": "search-params.tsx"
     },
@@ -572,6 +787,18 @@ export const routeTree = rootRoute
       "filePath": "posts.$postId.tsx",
       "parent": "/posts"
     },
+    "/redirect/$target": {
+      "filePath": "redirect/$target.tsx",
+      "children": [
+        "/redirect/$target/via-beforeLoad",
+        "/redirect/$target/via-loader",
+        "/redirect/$target/",
+        "/redirect/$target/serverFn/via-beforeLoad",
+        "/redirect/$target/serverFn/via-loader",
+        "/redirect/$target/serverFn/via-useServerFn",
+        "/redirect/$target/serverFn/"
+      ]
+    },
     "/users/$userId": {
       "filePath": "users.$userId.tsx",
       "parent": "/users"
@@ -580,6 +807,9 @@ export const routeTree = rootRoute
       "filePath": "posts.index.tsx",
       "parent": "/posts"
     },
+    "/redirect/": {
+      "filePath": "redirect/index.tsx"
+    },
     "/users/": {
       "filePath": "users.index.tsx",
       "parent": "/users"
@@ -594,6 +824,34 @@ export const routeTree = rootRoute
     },
     "/posts_/$postId/deep": {
       "filePath": "posts_.$postId.deep.tsx"
+    },
+    "/redirect/$target/via-beforeLoad": {
+      "filePath": "redirect/$target/via-beforeLoad.tsx",
+      "parent": "/redirect/$target"
+    },
+    "/redirect/$target/via-loader": {
+      "filePath": "redirect/$target/via-loader.tsx",
+      "parent": "/redirect/$target"
+    },
+    "/redirect/$target/": {
+      "filePath": "redirect/$target/index.tsx",
+      "parent": "/redirect/$target"
+    },
+    "/redirect/$target/serverFn/via-beforeLoad": {
+      "filePath": "redirect/$target/serverFn/via-beforeLoad.tsx",
+      "parent": "/redirect/$target"
+    },
+    "/redirect/$target/serverFn/via-loader": {
+      "filePath": "redirect/$target/serverFn/via-loader.tsx",
+      "parent": "/redirect/$target"
+    },
+    "/redirect/$target/serverFn/via-useServerFn": {
+      "filePath": "redirect/$target/serverFn/via-useServerFn.tsx",
+      "parent": "/redirect/$target"
+    },
+    "/redirect/$target/serverFn/": {
+      "filePath": "redirect/$target/serverFn/index.tsx",
+      "parent": "/redirect/$target"
     }
   }
 }
diff --git a/e2e/start/basic/app/routes/__root.tsx b/e2e/start/basic/app/routes/__root.tsx
index 3c28fb603..876210e14 100644
--- a/e2e/start/basic/app/routes/__root.tsx
+++ b/e2e/start/basic/app/routes/__root.tsx
@@ -120,6 +120,14 @@ function RootDocument({ children }: { children: React.ReactNode }) {
           >
             Deferred
           </Link>{' '}
+          <Link
+            to="/redirect"
+            activeProps={{
+              className: 'font-bold',
+            }}
+          >
+            redirect
+          </Link>{' '}
           <Link
             // @ts-expect-error
             to="/this-route-does-not-exist"
diff --git a/e2e/start/basic/app/routes/links.tsx b/e2e/start/basic/app/routes/links.tsx
new file mode 100644
index 000000000..adc8fe9c4
--- /dev/null
+++ b/e2e/start/basic/app/routes/links.tsx
@@ -0,0 +1,47 @@
+import { Link, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/links')({
+  component: () => {
+    const navigate = Route.useNavigate()
+    return (
+      <div>
+        <h1 className="mb-4 text-4xl font-extrabold lmd:text-5xl lg:text-6xl ">
+          link test
+        </h1>
+        <div className="mb-2">
+          <Link
+            to="/posts"
+            activeProps={{
+              className: 'font-bold',
+            }}
+          >
+            Link to /posts
+          </Link>
+        </div>
+        <div className="mb-2">
+          <Link
+            to="/posts"
+            reloadDocument={true}
+            activeProps={{
+              className: 'font-bold',
+            }}
+          >
+            Link to /posts (reloadDocument=true)
+          </Link>
+        </div>
+        <div className="mb-2">
+          <button onClick={() => navigate({ to: '/posts' })}>
+            navigate to /posts
+          </button>
+        </div>
+        <div className="mb-2">
+          <button
+            onClick={() => navigate({ to: '/posts', reloadDocument: true })}
+          >
+            navigate to /posts (reloadDocument=true)
+          </button>
+        </div>
+      </div>
+    )
+  },
+})
diff --git a/e2e/start/basic/app/routes/posts.index.tsx b/e2e/start/basic/app/routes/posts.index.tsx
index 5b5f08f95..1bad79b0f 100644
--- a/e2e/start/basic/app/routes/posts.index.tsx
+++ b/e2e/start/basic/app/routes/posts.index.tsx
@@ -5,5 +5,5 @@ export const Route = createFileRoute('/posts/')({
 })
 
 function PostsIndexComponent() {
-  return <div>Select a post.</div>
+  return <div data-testid="PostsIndexComponent">Select a post.</div>
 }
diff --git a/e2e/start/basic/app/routes/redirect.tsx b/e2e/start/basic/app/routes/redirect.tsx
deleted file mode 100644
index c9286de13..000000000
--- a/e2e/start/basic/app/routes/redirect.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { createFileRoute, redirect } from '@tanstack/react-router'
-
-export const Route = createFileRoute('/redirect')({
-  beforeLoad: async () => {
-    throw redirect({
-      to: '/posts',
-    })
-  },
-})
diff --git a/e2e/start/basic/app/routes/redirect/$target.tsx b/e2e/start/basic/app/routes/redirect/$target.tsx
new file mode 100644
index 000000000..0bda04ff4
--- /dev/null
+++ b/e2e/start/basic/app/routes/redirect/$target.tsx
@@ -0,0 +1,17 @@
+import { createFileRoute } from '@tanstack/react-router'
+import z from 'zod'
+
+export const Route = createFileRoute('/redirect/$target')({
+  params: {
+    parse: (p) =>
+      z
+        .object({
+          target: z.union([z.literal('internal'), z.literal('external')]),
+        })
+        .parse(p),
+  },
+  validateSearch: z.object({
+    reloadDocument: z.boolean().optional(),
+    preload: z.literal(false).optional(),
+  }),
+})
diff --git a/e2e/start/basic/app/routes/redirect/$target/index.tsx b/e2e/start/basic/app/routes/redirect/$target/index.tsx
new file mode 100644
index 000000000..f399d965c
--- /dev/null
+++ b/e2e/start/basic/app/routes/redirect/$target/index.tsx
@@ -0,0 +1,76 @@
+import { Link, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/redirect/$target/')({
+  component: () => {
+    const preload = Route.useSearch({ select: (s) => s.preload })
+    return (
+      <div>
+        <div className="mb-2">
+          <Link
+            from={Route.fullPath}
+            to="./via-beforeLoad"
+            activeProps={{
+              className: 'font-bold',
+            }}
+            preload={preload}
+            data-testid="via-beforeLoad"
+          >
+            via-beforeLoad
+          </Link>
+        </div>
+        <div className="mb-2">
+          <Link
+            from={Route.fullPath}
+            to="./via-beforeLoad"
+            search={{ reloadDocument: true }}
+            activeProps={{
+              className: 'font-bold',
+            }}
+            preload={preload}
+            data-testid="via-beforeLoad-reloadDocument"
+          >
+            via-beforeLoad (reloadDocument=true)
+          </Link>
+        </div>
+        <div className="mb-2">
+          <Link
+            from={Route.fullPath}
+            to="./via-loader"
+            activeProps={{
+              className: 'font-bold',
+            }}
+            preload={preload}
+            data-testid="via-loader"
+          >
+            via-loader
+          </Link>
+        </div>
+        <div className="mb-2">
+          <Link
+            from={Route.fullPath}
+            to="./via-loader"
+            activeProps={{
+              className: 'font-bold',
+            }}
+            search={{ reloadDocument: true }}
+            preload={preload}
+            data-testid="via-loader-reloadDocument"
+          >
+            via-loader (reloadDocument=true)
+          </Link>
+        </div>
+        <div className="mb-2">
+          <Link
+            from={Route.fullPath}
+            to="./serverFn"
+            activeProps={{
+              className: 'font-bold',
+            }}
+          >
+            serverFn
+          </Link>
+        </div>
+      </div>
+    )
+  },
+})
diff --git a/e2e/start/basic/app/routes/redirect/$target/serverFn/index.tsx b/e2e/start/basic/app/routes/redirect/$target/serverFn/index.tsx
new file mode 100644
index 000000000..ddb0d8e99
--- /dev/null
+++ b/e2e/start/basic/app/routes/redirect/$target/serverFn/index.tsx
@@ -0,0 +1,86 @@
+import { Link, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/redirect/$target/serverFn/')({
+  component: () => (
+    <div>
+      <h1 className="mb-4 text-4xl font-extrabold lmd:text-5xl lg:text-6xl ">
+        redirect test with server functions (target {Route.useParams().target})
+      </h1>
+      <div className="mb-2">
+        <Link
+          from={Route.fullPath}
+          to="./via-beforeLoad"
+          activeProps={{
+            className: 'font-bold',
+          }}
+          data-testid="via-beforeLoad"
+        >
+          via-beforeLoad
+        </Link>
+      </div>
+      <div className="mb-2">
+        <Link
+          from={Route.fullPath}
+          to="./via-beforeLoad"
+          search={{ reloadDocument: true }}
+          activeProps={{
+            className: 'font-bold',
+          }}
+          data-testid="via-beforeLoad-reloadDocument"
+        >
+          via-beforeLoad (reloadDocument=true)
+        </Link>
+      </div>
+      <div className="mb-2">
+        <Link
+          from={Route.fullPath}
+          to="./via-loader"
+          activeProps={{
+            className: 'font-bold',
+          }}
+          data-testid="via-loader"
+        >
+          via-loader
+        </Link>
+      </div>
+      <div className="mb-2">
+        <Link
+          from={Route.fullPath}
+          to="./via-loader"
+          activeProps={{
+            className: 'font-bold',
+          }}
+          search={{ reloadDocument: true }}
+          data-testid="via-loader-reloadDocument"
+        >
+          via-loader (reloadDocument=true)
+        </Link>
+      </div>
+      <div className="mb-2">
+        <Link
+          from={Route.fullPath}
+          to="./via-useServerFn"
+          activeProps={{
+            className: 'font-bold',
+          }}
+          data-testid="via-useServerFn"
+        >
+          via-useServerFn
+        </Link>
+      </div>
+      <div className="mb-2">
+        <Link
+          from={Route.fullPath}
+          to="./via-useServerFn"
+          activeProps={{
+            className: 'font-bold',
+          }}
+          search={{ reloadDocument: true }}
+          data-testid="via-useServerFn-reloadDocument"
+        >
+          via-useServerFn (reloadDocument=true)
+        </Link>
+      </div>
+    </div>
+  ),
+})
diff --git a/e2e/start/basic/app/routes/redirect/$target/serverFn/via-beforeLoad.tsx b/e2e/start/basic/app/routes/redirect/$target/serverFn/via-beforeLoad.tsx
new file mode 100644
index 000000000..1bfe12115
--- /dev/null
+++ b/e2e/start/basic/app/routes/redirect/$target/serverFn/via-beforeLoad.tsx
@@ -0,0 +1,10 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { throwRedirect } from '~/components/throwRedirect'
+
+export const Route = createFileRoute(
+  '/redirect/$target/serverFn/via-beforeLoad',
+)({
+  beforeLoad: ({ params: { target }, search: { reloadDocument } }) =>
+    throwRedirect({ data: { target, reloadDocument } }),
+  component: () => <div>{Route.fullPath}</div>,
+})
diff --git a/e2e/start/basic/app/routes/redirect/$target/serverFn/via-loader.tsx b/e2e/start/basic/app/routes/redirect/$target/serverFn/via-loader.tsx
new file mode 100644
index 000000000..0d704a11d
--- /dev/null
+++ b/e2e/start/basic/app/routes/redirect/$target/serverFn/via-loader.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { throwRedirect } from '~/components/throwRedirect'
+
+export const Route = createFileRoute('/redirect/$target/serverFn/via-loader')({
+  loaderDeps: ({ search: { reloadDocument } }) => ({ reloadDocument }),
+  loader: ({ params: { target }, deps: { reloadDocument } }) =>
+    throwRedirect({ data: { target, reloadDocument } }),
+  component: () => <div>{Route.fullPath}</div>,
+})
diff --git a/e2e/start/basic/app/routes/redirect/$target/serverFn/via-useServerFn.tsx b/e2e/start/basic/app/routes/redirect/$target/serverFn/via-useServerFn.tsx
new file mode 100644
index 000000000..197d362d5
--- /dev/null
+++ b/e2e/start/basic/app/routes/redirect/$target/serverFn/via-useServerFn.tsx
@@ -0,0 +1,12 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { RedirectOnClick } from '~/components/RedirectOnClick'
+
+export const Route = createFileRoute(
+  '/redirect/$target/serverFn/via-useServerFn',
+)({
+  component: () => {
+    const { target } = Route.useParams()
+    const { reloadDocument } = Route.useSearch()
+    return <RedirectOnClick target={target} reloadDocument={reloadDocument} />
+  },
+})
diff --git a/e2e/start/basic/app/routes/redirect/$target/via-beforeLoad.tsx b/e2e/start/basic/app/routes/redirect/$target/via-beforeLoad.tsx
new file mode 100644
index 000000000..d13eb5a34
--- /dev/null
+++ b/e2e/start/basic/app/routes/redirect/$target/via-beforeLoad.tsx
@@ -0,0 +1,13 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/redirect/$target/via-beforeLoad')({
+  beforeLoad: ({ params: { target }, search: { reloadDocument } }) => {
+    switch (target) {
+      case 'internal':
+        throw redirect({ to: '/posts', reloadDocument })
+      case 'external':
+        throw redirect({ href: 'http://example.com' })
+    }
+  },
+  component: () => <div>{Route.fullPath}</div>,
+})
diff --git a/e2e/start/basic/app/routes/redirect/$target/via-loader.tsx b/e2e/start/basic/app/routes/redirect/$target/via-loader.tsx
new file mode 100644
index 000000000..5cf0dc6de
--- /dev/null
+++ b/e2e/start/basic/app/routes/redirect/$target/via-loader.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/redirect/$target/via-loader')({
+  loaderDeps: ({ search: { reloadDocument } }) => ({ reloadDocument }),
+  loader: ({ params: { target }, deps: { reloadDocument } }) => {
+    switch (target) {
+      case 'internal':
+        throw redirect({ to: '/posts', reloadDocument })
+      case 'external':
+        throw redirect({ href: 'http://example.com' })
+    }
+  },
+  component: () => <div>{Route.fullPath}</div>,
+})
diff --git a/e2e/start/basic/app/routes/redirect/index.tsx b/e2e/start/basic/app/routes/redirect/index.tsx
new file mode 100644
index 000000000..c0b26a1df
--- /dev/null
+++ b/e2e/start/basic/app/routes/redirect/index.tsx
@@ -0,0 +1,28 @@
+import { Link, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/redirect/')({
+  component: () => (
+    <div>
+      <Link
+        from={Route.fullPath}
+        to="./$target"
+        params={{ target: 'internal' }}
+        activeProps={{
+          className: 'font-bold',
+        }}
+      >
+        internal
+      </Link>{' '}
+      <Link
+        from={Route.fullPath}
+        to="./$target"
+        params={{ target: 'external' }}
+        activeProps={{
+          className: 'font-bold',
+        }}
+      >
+        external
+      </Link>
+    </div>
+  ),
+})
diff --git a/e2e/start/basic/package.json b/e2e/start/basic/package.json
index a1223ea49..676fcf2eb 100644
--- a/e2e/start/basic/package.json
+++ b/e2e/start/basic/package.json
@@ -28,6 +28,7 @@
     "@types/react-dom": "^18.2.21",
     "@vitejs/plugin-react": "^4.3.4",
     "autoprefixer": "^10.4.20",
+    "combinate": "^1.1.11",
     "postcss": "^8.4.49",
     "tailwindcss": "^3.4.16",
     "typescript": "^5.7.2",
diff --git a/e2e/start/basic/tests/redirect.spec.ts b/e2e/start/basic/tests/redirect.spec.ts
new file mode 100644
index 000000000..caff77d6a
--- /dev/null
+++ b/e2e/start/basic/tests/redirect.spec.ts
@@ -0,0 +1,196 @@
+import { expect, test } from '@playwright/test'
+import combinateImport from 'combinate'
+import { derivePort } from '../../../utils'
+import packageJson from '../package.json' with { type: 'json' }
+
+// somehow playwright does not correctly import default exports
+const combinate = (combinateImport as any).default as typeof combinateImport
+
+const PORT = derivePort(packageJson.name)
+
+test.describe('redirects', () => {
+  const internalNavigationTestMatrix = combinate({
+    thrower: ['beforeLoad', 'loader'] as const,
+    reloadDocument: [false, true] as const,
+    preload: [false, true] as const,
+  })
+
+  internalNavigationTestMatrix.forEach(
+    ({ thrower, reloadDocument, preload }) => {
+      test(`internal target, navigation: thrower: ${thrower}, reloadDocument: ${reloadDocument}, preload: ${preload}`, async ({
+        page,
+      }) => {
+        await page.goto(
+          `/redirect/internal${preload === false ? '?preload=false' : ''}`,
+        )
+        const link = page.getByTestId(
+          `via-${thrower}${reloadDocument ? '-reloadDocument' : ''}`,
+        )
+
+        await page.waitForLoadState('networkidle')
+        let requestHappened = false
+
+        const requestPromise = new Promise<void>((resolve) => {
+          page.on('request', (request) => {
+            if (
+              request
+                .url()
+                .startsWith(`http://localhost:${PORT}/_server/?_serverFnId=`)
+            ) {
+              requestHappened = true
+              resolve()
+            }
+          })
+        })
+        await link.focus()
+
+        const expectRequestHappened = preload && !reloadDocument
+        const timeoutPromise = new Promise((resolve) =>
+          setTimeout(resolve, expectRequestHappened ? 5000 : 500),
+        )
+        await Promise.race([requestPromise, timeoutPromise])
+        expect(requestHappened).toBe(expectRequestHappened)
+        let fullPageLoad = false
+        page.on('domcontentloaded', () => {
+          fullPageLoad = true
+        })
+
+        await link.click()
+
+        const url = `http://localhost:${PORT}/posts`
+
+        await page.waitForURL(url)
+        expect(page.url()).toBe(url)
+        await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport()
+        expect(fullPageLoad).toBe(reloadDocument)
+      })
+    },
+  )
+
+  const internalDirectVisitTestMatrix = combinate({
+    thrower: ['beforeLoad', 'loader'] as const,
+    reloadDocument: [false, true] as const,
+    preload: [false, true] as const,
+  })
+
+  internalDirectVisitTestMatrix.forEach(
+    ({ thrower, reloadDocument, preload }) => {
+      test(`internal target, direct visit: thrower: ${thrower}, reloadDocument: ${reloadDocument}, preload: ${preload}`, async ({
+        page,
+      }) => {
+        await page.goto(`/redirect/internal/via-${thrower}`)
+
+        const url = `http://localhost:${PORT}/posts`
+
+        await page.waitForURL(url)
+        expect(page.url()).toBe(url)
+        await page.waitForLoadState('networkidle')
+        await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport()
+      })
+    },
+  )
+
+  const externalTestMatrix = combinate({
+    scenario: ['navigate', 'direct_visit'] as const,
+    thrower: ['beforeLoad', 'loader'] as const,
+  })
+
+  externalTestMatrix.forEach(({ scenario, thrower }) => {
+    test(`external target: scenario: ${scenario}, thrower: ${thrower}`, async ({
+      page,
+    }) => {
+      if (scenario === 'navigate') {
+        await page.goto(`/redirect/external`)
+        await page.waitForLoadState('networkidle')
+        const link = page.getByTestId(`via-${thrower}`)
+        await link.focus()
+        await link.click()
+      } else {
+        await page.goto(`/redirect/external/via-${thrower}`)
+      }
+
+      const url = 'http://example.com/'
+
+      await page.waitForURL(url)
+      expect(page.url()).toBe(url)
+    })
+  })
+
+  const serverFnTestMatrix = combinate({
+    target: ['internal', 'external'] as const,
+    scenario: ['navigate', 'direct_visit'] as const,
+    thrower: ['beforeLoad', 'loader'] as const,
+    reloadDocument: [false, true] as const,
+  })
+
+  serverFnTestMatrix.forEach(
+    ({ target, thrower, scenario, reloadDocument }) => {
+      test(`serverFn redirects to target: ${target}, scenario: ${scenario}, thrower: ${thrower}, reloadDocument: ${reloadDocument}`, async ({
+        page,
+      }) => {
+        let fullPageLoad = false
+        if (scenario === 'navigate') {
+          await page.goto(`/redirect/${target}/serverFn`)
+          await page.waitForLoadState('networkidle')
+          const link = page.getByTestId(
+            `via-${thrower}${reloadDocument ? '-reloadDocument' : ''}`,
+          )
+          page.on('domcontentloaded', () => {
+            fullPageLoad = true
+          })
+          await link.focus()
+          await link.click()
+        } else {
+          await page.goto(
+            `/redirect/${target}/serverFn/via-${thrower}${reloadDocument ? '?reloadDocument=true' : ''}`,
+          )
+        }
+
+        const url =
+          target === 'internal'
+            ? `http://localhost:${PORT}/posts`
+            : 'http://example.com/'
+        await page.waitForURL(url)
+        expect(page.url()).toBe(url)
+        if (target === 'internal' && scenario === 'navigate') {
+          await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport()
+          expect(fullPageLoad).toBe(reloadDocument)
+        }
+      })
+    },
+  )
+
+  const useServerFnTestMatrix = combinate({
+    target: ['internal', 'external'] as const,
+    reloadDocument: [false, true] as const,
+  })
+
+  useServerFnTestMatrix.forEach(({ target, reloadDocument }) => {
+    test(`useServerFn redirects to target: ${target}, reloadDocument: ${reloadDocument}`, async ({
+      page,
+    }) => {
+      await page.goto(
+        `/redirect/${target}/serverFn/via-useServerFn${reloadDocument ? '?reloadDocument=true' : ''}`,
+      )
+      const button = page.getByTestId('redirect-on-click')
+
+      let fullPageLoad = false
+      page.on('domcontentloaded', () => {
+        fullPageLoad = true
+      })
+
+      await button.click()
+
+      const url =
+        target === 'internal'
+          ? `http://localhost:${PORT}/posts`
+          : 'http://example.com/'
+      await page.waitForURL(url)
+      expect(page.url()).toBe(url)
+      if (target === 'internal') {
+        await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport()
+        expect(fullPageLoad).toBe(reloadDocument)
+      }
+    })
+  })
+})
diff --git a/packages/react-router/tests/redirect.test.tsx b/packages/react-router/tests/redirect.test.tsx
index 895bb33bc..679158c2c 100644
--- a/packages/react-router/tests/redirect.test.tsx
+++ b/packages/react-router/tests/redirect.test.tsx
@@ -294,6 +294,7 @@ describe('redirect', () => {
         }),
         to: '/about',
         headers: {},
+        reloadDocument: false,
         href: '/about',
         isRedirect: true,
         routeId: '/',
@@ -347,6 +348,7 @@ describe('redirect', () => {
         headers: {},
         href: '/about',
         isRedirect: true,
+        reloadDocument: false,
         routeId: '/',
         statusCode: 307,
       })
diff --git a/packages/react-router/tests/route.test-d.tsx b/packages/react-router/tests/route.test-d.tsx
index d7ad59484..f54a0cb7a 100644
--- a/packages/react-router/tests/route.test-d.tsx
+++ b/packages/react-router/tests/route.test-d.tsx
@@ -85,7 +85,7 @@ test('when creating the root with a loader', () => {
         deps: {}
         context: {}
         location: ParsedLocation
-        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void>
+        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void> | void
         parentMatchPromise: never
         cause: 'preload' | 'enter' | 'stay'
         route: Route
@@ -191,7 +191,7 @@ test('when creating the root route with context and a loader', () => {
         deps: {}
         context: { userId: string }
         location: ParsedLocation
-        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void>
+        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void> | void
         parentMatchPromise: never
         cause: 'preload' | 'enter' | 'stay'
         route: Route
@@ -265,7 +265,7 @@ test('when creating the root route with context, routeContext, beforeLoad and a
         deps: {}
         context: { userId: string; permission: 'view'; env: 'env1' }
         location: ParsedLocation
-        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void>
+        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void> | void
         parentMatchPromise: never
         cause: 'preload' | 'enter' | 'stay'
         route: Route
@@ -405,7 +405,7 @@ test('when creating a child route with a loader from the root route', () => {
         deps: {}
         context: {}
         location: ParsedLocation
-        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void>
+        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void> | void
         parentMatchPromise: Promise<MakeRouteMatchFromRoute<typeof rootRoute>>
         cause: 'preload' | 'enter' | 'stay'
         route: Route
@@ -456,7 +456,7 @@ test('when creating a child route with a loader from the root route with context
         deps: {}
         context: { userId: string }
         location: ParsedLocation
-        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void>
+        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void> | void
         parentMatchPromise: Promise<MakeRouteMatchFromRoute<typeof rootRoute>>
         cause: 'preload' | 'enter' | 'stay'
         route: Route
@@ -676,7 +676,7 @@ test('when creating a child route with params, search and loader from the root r
         deps: {}
         context: {}
         location: ParsedLocation
-        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void>
+        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void> | void
         parentMatchPromise: Promise<MakeRouteMatchFromRoute<typeof rootRoute>>
         cause: 'preload' | 'enter' | 'stay'
         route: Route
@@ -701,7 +701,7 @@ test('when creating a child route with params, search, loader and loaderDeps fro
         deps: { page: number }
         context: {}
         location: ParsedLocation
-        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void>
+        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void> | void
         parentMatchPromise: Promise<MakeRouteMatchFromRoute<typeof rootRoute>>
         cause: 'preload' | 'enter' | 'stay'
         route: Route
@@ -725,7 +725,7 @@ test('when creating a child route with params, search, loader and loaderDeps fro
         deps: { page: number }
         context: { userId: string }
         location: ParsedLocation
-        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void>
+        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void> | void
         parentMatchPromise: Promise<MakeRouteMatchFromRoute<typeof rootRoute>>
         cause: 'preload' | 'enter' | 'stay'
         route: Route
@@ -828,7 +828,7 @@ test('when creating a child route with params, search with routeContext, beforeL
         deps: {}
         context: { userId: string; env: string; readonly permission: 'view' }
         location: ParsedLocation
-        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void>
+        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void> | void
         parentMatchPromise: Promise<MakeRouteMatchFromRoute<typeof rootRoute>>
         cause: 'preload' | 'enter' | 'stay'
         route: Route
@@ -1195,7 +1195,7 @@ test('when creating a child route with routeContext, beforeLoad, search, params,
           detailsPermissions: readonly ['view']
         }
         location: ParsedLocation
-        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void>
+        navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void> | void
         parentMatchPromise: Promise<
           MakeRouteMatchFromRoute<typeof detailsRoute>
         >