Repository: TanStack/router
Pull request #3004 (2024-12-13 11:08:54)

feat(react-router): allow to redirect to external targets, add `reloadDocument` option

nx-cloud[bot]: # ☁️ Nx Cloud Report

CI is running for commit c038ba319aa8338078f781c68cb79296aaeb1995.

[📂 Click to track the progress, see the status, the terminal output, and the build insights.](https://cloud.nx.app/cipes/675c15e85958682aae4c198e?utm_source=pull-request&utm_medium=comment)


---

Sent with 💌 from [NxCloud.](https://nx.app?utm_source=pull-request&utm_medium=comment)
pkg-pr-new[bot]: 
[Open in Stackblitz](https://pkg.pr.new/template/4147441b-2a2c-49bf-8bd6-2d89f3b7aa73)
<details><summary><b>More templates</b></summary><p>

- [tanstack-router-react-example-authenticated-routes](https://pkg.pr.new/template/f1d0a67f-5e3a-40fe-964b-1ab28af1c955)
- [tanstack-router-react-example-basic](https://pkg.pr.new/template/c322ac51-f6e6-4dfd-93c7-1f02adc5777e)
- [tanstack-router-react-example-basic-default-search-params](https://pkg.pr.new/template/e567f05b-b91c-4fde-bd9a-90c7f0bfa814)
- [tanstack-router-react-example-basic-file-based](https://pkg.pr.new/template/732a1b5d-28bf-4a49-bcb5-5733745e468c)
- [tanstack-router-react-example-basic-file-based-codesplitting](https://pkg.pr.new/template/6d562104-3ed9-4381-aad8-e9dfce60d933)
- [tanstack-router-react-example-react-query](https://pkg.pr.new/template/5bc51007-e15e-4bc6-96d2-a00b21c0fe9c)
- [tanstack-router-react-example-basic-react-query-file-based](https://pkg.pr.new/template/f544f15e-9a49-4e5c-8a33-f236afa5aa80)
- [tanstack-router-react-example-basic-ssr-file-based](https://pkg.pr.new/template/7074d8fa-e08f-4972-83b0-8a38523e7a08)
- [tanstack-router-react-example-basic-ssr-streaming-file-based](https://pkg.pr.new/template/06cba8bd-ca55-4ee0-8708-68e72e6c6be7)
- [tanstack-router-react-example-basic-virtual-file-based](https://pkg.pr.new/template/7e246f52-c404-4ffe-bfb3-e2ff46d669cd)
- [tanstack-router-react-example-basic-virtual-inside-file-based](https://pkg.pr.new/template/c0bc9499-f4a5-4c91-af55-1c464ef69459)
- [tanstack-router-react-example-deferred-data](https://pkg.pr.new/template/b92f4cd0-bea9-4726-bfff-05414d050594)
- [tanstack-router-react-example-kitchen-sink](https://pkg.pr.new/template/fb9afad6-5d30-43c4-9bd4-f6b55f5017bd)
- [tanstack-router-react-example-kitchen-sink-file-based](https://pkg.pr.new/template/13514df6-8fe0-442f-8c05-58a198e79f87)
- [tanstack-router-react-example-kitchen-sink-react-query](https://pkg.pr.new/template/1d9f9ab6-3973-4bdb-856b-c63d876b4919)
- [tanstack-router-react-example-kitchen-sink-react-query-file-based](https://pkg.pr.new/template/17b96c70-c249-45ea-86a3-ee5e8dbd00ed)
- [tanstack-router-react-example-large-file-based](https://pkg.pr.new/template/485feb00-3c6d-4f94-b99b-55a0dc684cb7)
- [tanstack-router-react-example-location-masking](https://pkg.pr.new/template/b9535e9e-dce0-4c77-bd1d-98494a69ad02)
- [tanstack-router-react-example-navigation-blocking](https://pkg.pr.new/template/a1136cbe-fbf1-450e-adf2-0d92d79040ec)
- [tanstack-router-react-example-quickstart](https://pkg.pr.new/template/d8ea6a26-9320-4ad9-b4ed-364a8d164694)
- [tanstack-router-react-example-quickstart-esbuild-file-based](https://pkg.pr.new/template/db0d396a-0a3d-49c1-b258-722ec0c4b5ad)
- [tanstack-router-react-example-quickstart-file-based](https://pkg.pr.new/template/935b240e-eb32-4f8e-8118-e875f431dc4c)
- [tanstack-router-react-example-quickstart-rspack-file-based](https://pkg.pr.new/template/9269f9d5-3de1-482d-8cb6-de82a6c3c4fa)
- [tanstack-router-react-example-quickstart-webpack-file-based](https://pkg.pr.new/template/0abb5380-cf61-4f91-a46f-81a4e4668bcc)
- [router-monorepo-react-query](https://pkg.pr.new/template/ec1470a0-63b2-4de4-8f31-667c456362d3)
- [router-mono-simple](https://pkg.pr.new/template/a7086317-ba11-423d-a7c1-70eda0ac050e)
- [tanstack-router-react-example-scroll-restoration](https://pkg.pr.new/template/a0406114-8629-455e-ba29-62a658cd295f)
- [tanstack-search-validator-adapters](https://pkg.pr.new/template/6db53b43-65ff-40af-a619-140575e77554)
- [tanstack-start-example-basic](https://pkg.pr.new/template/ca9ad1d7-8744-4232-ad62-1d767a5a5baf)
- [tanstack-start-example-basic-auth](https://pkg.pr.new/template/b8a4f5a0-ca63-467a-81b8-63cbad8ec0a8)
- [tanstack-start-example-basic-react-query](https://pkg.pr.new/template/a995dbd7-f7d6-40a8-b356-2ca4f753f623)
- [tanstack-start-example-basic-rsc](https://pkg.pr.new/template/636af516-71ea-4bd8-a83e-1ad6e97ae697)
- [tanstack-start-example-clerk-basic](https://pkg.pr.new/template/207ca48c-4ccb-4569-9714-b2a59bf91f2a)
- [tanstack-start-example-counter](https://pkg.pr.new/template/2ec84bb7-e4f2-4b5a-9fe1-bfb51cec3097)
- [tanstack-start-example-convex-trellaux](https://pkg.pr.new/template/75c633c0-8954-4dcd-9c1d-061ef9700221)
- [tanstack-start-example-large](https://pkg.pr.new/template/0e3ed599-f5ee-4302-a8d1-dfc409599ee1)
- [tanstack-start-example-supabase-basic](https://pkg.pr.new/template/104c9bdd-0f55-48e1-9c9a-b9b77bfd5fc5)
- [tanstack-start-example-trellaux](https://pkg.pr.new/template/c097a1a0-96c8-44b8-8f2f-407c3c33618c)
- [tanstack-router-react-example-with-framer-motion](https://pkg.pr.new/template/aa4df217-2721-43d8-83a8-7c501abe316d)
- [tanstack-router-react-example-with-trpc](https://pkg.pr.new/template/171d92bd-2ac4-41f8-95ff-bcc641b44e14)
- [tanstack-router-react-example-with-trpc-react-query](https://pkg.pr.new/template/58059178-e27b-4465-97d2-b539c93205c2)

</p></details>
      
    


<details><summary><b>@tanstack/arktype-adapter</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/arktype-adapter@3004
```

</p></details>
      
    

<details><summary><b>@tanstack/create-router</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/create-router@3004
```

</p></details>
      
    

<details><summary><b>@tanstack/history</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/history@3004
```

</p></details>
      
    

<details><summary><b>@tanstack/eslint-plugin-router</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@3004
```

</p></details>
      
    

<details><summary><b>@tanstack/react-cross-context</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/react-cross-context@3004
```

</p></details>
      
    

<details><summary><b>@tanstack/react-router</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/react-router@3004
```

</p></details>
      
    

<details><summary><b>@tanstack/react-router-with-query</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/react-router-with-query@3004
```

</p></details>
      
    

<details><summary><b>@tanstack/router-cli</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-cli@3004
```

</p></details>
      
    

<details><summary><b>@tanstack/router-devtools</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-devtools@3004
```

</p></details>
      
    

<details><summary><b>@tanstack/router-generator</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-generator@3004
```

</p></details>
      
    

<details><summary><b>@tanstack/router-plugin</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-plugin@3004
```

</p></details>
      
    

<details><summary><b>@tanstack/router-vite-plugin</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-vite-plugin@3004
```

</p></details>
      
    

<details><summary><b>@tanstack/start</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/start@3004
```

</p></details>
      
    

<details><summary><b>@tanstack/start-vite-plugin</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/start-vite-plugin@3004
```

</p></details>
      
    

<details><summary><b>@tanstack/valibot-adapter</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/valibot-adapter@3004
```

</p></details>
      
    

<details><summary><b>@tanstack/virtual-file-routes</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/virtual-file-routes@3004
```

</p></details>
      
    

<details><summary><b>@tanstack/zod-adapter</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/zod-adapter@3004
```

</p></details>
      
    

_commit: <a href="https://github.com/TanStack/router/runs/34371016334"><code>7ec5019</code></a>_


Patch:
diff --git a/docs/framework/react/api/router/NavigateOptionsType.md b/docs/framework/react/api/router/NavigateOptionsType.md
index f832d650b..df98defb4 100644
--- a/docs/framework/react/api/router/NavigateOptionsType.md
+++ b/docs/framework/react/api/router/NavigateOptionsType.md
@@ -10,7 +10,47 @@ type NavigateOptions = ToOptions & {
   replace?: boolean
   resetScroll?: boolean
   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/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/packages/react-router/src/RouterProvider.tsx b/packages/react-router/src/RouterProvider.tsx
index 67be7b953..cf44b316a 100644
--- a/packages/react-router/src/RouterProvider.tsx
+++ b/packages/react-router/src/RouterProvider.tsx
@@ -38,7 +38,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/link.tsx b/packages/react-router/src/link.tsx
index 387926fec..c7ad907b7 100644
--- a/packages/react-router/src/link.tsx
+++ b/packages/react-router/src/link.tsx
@@ -219,6 +219,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<
@@ -643,6 +645,9 @@ export function useLinkProps<
   // null for LinkUtils
 
   const type: 'internal' | 'external' = React.useMemo(() => {
+    if (rest.reloadDocument) {
+      return 'external'
+    }
     try {
       new URL(`${to}`)
       return 'external'
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..5338133c8 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -536,6 +536,7 @@ export interface BuildNextOptions {
   }
   from?: string
   _fromLocation?: ParsedLocation
+  href?: string
 }
 
 export interface MatchedRoutesResult {
@@ -1861,9 +1862,9 @@ export class Router<
     resetScroll,
     viewTransition,
     ignoreBlocker,
+    href,
     ...rest
   }: BuildNextOptions & CommitLocationOptions = {}) => {
-    const href = (rest as any).href
     if (href) {
       const parsed = parseHref(href, {})
       rest.to = parsed.pathname
@@ -1885,29 +1886,24 @@ export class Router<
     })
   }
 
-  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 +2037,7 @@ export class Router<
             redirect = err
             if (!this.isServer) {
               this.navigate({
-                ...err,
+                ...redirect,
                 replace: true,
                 ignoreBlocker: true,
               })
@@ -2194,7 +2190,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 +2877,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/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/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..02aba27f9 100644
--- a/e2e/react-router/basic-file-based/src/routeTree.gen.ts
+++ b/e2e/react-router/basic-file-based/src/routeTree.gen.ts
@@ -16,14 +16,19 @@ import { Route as rootRoute } from './routes/__root'
 import { Route as PostsImport } from './routes/posts'
 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'
@@ -58,6 +63,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 +81,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 +124,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',
@@ -211,6 +247,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 +268,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 +310,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,6 +401,22 @@ 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
@@ -338,13 +425,18 @@ export interface FileRoutesByFullPath {
   '/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 {
@@ -356,11 +448,15 @@ export interface FileRoutesByTo {
   '/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 {
@@ -375,13 +471,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 {
@@ -394,13 +495,18 @@ export interface FileRouteTypes {
     | '/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:
     | '/'
@@ -411,11 +517,15 @@ export interface FileRouteTypes {
     | '/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__'
     | '/'
@@ -428,13 +538,18 @@ 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
 }
 
@@ -444,7 +559,9 @@ export interface RootRouteChildren {
   PostsRoute: typeof PostsRouteWithChildren
   anotherGroupOnlyrouteinsideRoute: typeof anotherGroupOnlyrouteinsideRoute
   groupRoute: typeof groupRouteWithChildren
+  RedirectTargetRoute: typeof RedirectTargetRouteWithChildren
   StructuralSharingEnabledRoute: typeof StructuralSharingEnabledRoute
+  RedirectIndexRoute: typeof RedirectIndexRoute
   PostsPostIdEditRoute: typeof PostsPostIdEditRoute
 }
 
@@ -454,7 +571,9 @@ const rootRouteChildren: RootRouteChildren = {
   PostsRoute: PostsRouteWithChildren,
   anotherGroupOnlyrouteinsideRoute: anotherGroupOnlyrouteinsideRoute,
   groupRoute: groupRouteWithChildren,
+  RedirectTargetRoute: RedirectTargetRouteWithChildren,
   StructuralSharingEnabledRoute: StructuralSharingEnabledRoute,
+  RedirectIndexRoute: RedirectIndexRoute,
   PostsPostIdEditRoute: PostsPostIdEditRoute,
 }
 
@@ -473,7 +592,9 @@ export const routeTree = rootRoute
         "/posts",
         "/(another-group)/onlyrouteinside",
         "/(group)",
+        "/redirect/$target",
         "/structural-sharing/$enabled",
+        "/redirect/",
         "/posts_/$postId/edit"
       ]
     },
@@ -532,6 +653,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 +668,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 +689,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/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/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>
         >