diff --git a/e2e/react-router/basepath-file-based/src/routes/index.tsx b/e2e/react-router/basepath-file-based/src/routes/index.tsx
index 7ae0eca2bc3..551724d3376 100644
--- a/e2e/react-router/basepath-file-based/src/routes/index.tsx
+++ b/e2e/react-router/basepath-file-based/src/routes/index.tsx
@@ -39,6 +39,27 @@ function App() {
}
>
Navigate to /redirectReload
+ {' '}
+
+ navigate({
+ href: '/app/about',
+ })
+ }
+ >
+ Navigate to /about using href with basepath
+ {' '}
+
+ navigate({
+ href: '/app/about',
+ reloadDocument: true,
+ })
+ }
+ >
+ Navigate to /about using href with basepath (reloadDocument)
)
diff --git a/e2e/react-router/basepath-file-based/tests/reload-document.test.ts b/e2e/react-router/basepath-file-based/tests/reload-document.test.ts
index f35ddd5670d..ce07699aed8 100644
--- a/e2e/react-router/basepath-file-based/tests/reload-document.test.ts
+++ b/e2e/react-router/basepath-file-based/tests/reload-document.test.ts
@@ -38,3 +38,27 @@ test('redirect respects basepath with reloadDocument = true on redirect', async
await page.waitForURL('/app/about')
await expect(page.getByTestId(`about-component`)).toBeInViewport()
})
+
+test('navigate() with href containing basepath', async ({ page }) => {
+ await page.goto(`/app/`)
+ await expect(page.getByTestId(`home-component`)).toBeInViewport()
+
+ const aboutBtn = page.getByTestId(`to-about-href-with-basepath-btn`)
+ await aboutBtn.click()
+ // Should navigate to /app/about, NOT /app/app/about
+ await page.waitForURL('/app/about')
+ await expect(page.getByTestId(`about-component`)).toBeInViewport()
+})
+
+test('navigate() with href containing basepath and reloadDocument=true', async ({
+ page,
+}) => {
+ await page.goto(`/app/`)
+ await expect(page.getByTestId(`home-component`)).toBeInViewport()
+
+ const aboutBtn = page.getByTestId(`to-about-href-with-basepath-reload-btn`)
+ await aboutBtn.click()
+ // Should navigate to /app/about, NOT stay on current page
+ await page.waitForURL('/app/about')
+ await expect(page.getByTestId(`about-component`)).toBeInViewport()
+})
diff --git a/e2e/react-start/custom-basepath/src/routeTree.gen.ts b/e2e/react-start/custom-basepath/src/routeTree.gen.ts
index 0e57e8bf39e..986f31bbbbd 100644
--- a/e2e/react-start/custom-basepath/src/routeTree.gen.ts
+++ b/e2e/react-start/custom-basepath/src/routeTree.gen.ts
@@ -11,6 +11,7 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as UsersRouteImport } from './routes/users'
import { Route as PostsRouteImport } from './routes/posts'
+import { Route as NavigateTestRouteImport } from './routes/navigate-test'
import { Route as LogoutRouteImport } from './routes/logout'
import { Route as DeferredRouteImport } from './routes/deferred'
import { Route as IndexRouteImport } from './routes/index'
@@ -34,6 +35,11 @@ const PostsRoute = PostsRouteImport.update({
path: '/posts',
getParentRoute: () => rootRouteImport,
} as any)
+const NavigateTestRoute = NavigateTestRouteImport.update({
+ id: '/navigate-test',
+ path: '/navigate-test',
+ getParentRoute: () => rootRouteImport,
+} as any)
const LogoutRoute = LogoutRouteImport.update({
id: '/logout',
path: '/logout',
@@ -99,6 +105,7 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/deferred': typeof DeferredRoute
'/logout': typeof LogoutRoute
+ '/navigate-test': typeof NavigateTestRoute
'/posts': typeof PostsRouteWithChildren
'/users': typeof UsersRouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
@@ -115,6 +122,7 @@ export interface FileRoutesByTo {
'/': typeof IndexRoute
'/deferred': typeof DeferredRoute
'/logout': typeof LogoutRoute
+ '/navigate-test': typeof NavigateTestRoute
'/api/users': typeof ApiUsersRouteWithChildren
'/posts/$postId': typeof PostsPostIdRoute
'/redirect/throw-it': typeof RedirectThrowItRoute
@@ -130,6 +138,7 @@ export interface FileRoutesById {
'/': typeof IndexRoute
'/deferred': typeof DeferredRoute
'/logout': typeof LogoutRoute
+ '/navigate-test': typeof NavigateTestRoute
'/posts': typeof PostsRouteWithChildren
'/users': typeof UsersRouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
@@ -148,6 +157,7 @@ export interface FileRouteTypes {
| '/'
| '/deferred'
| '/logout'
+ | '/navigate-test'
| '/posts'
| '/users'
| '/api/users'
@@ -164,6 +174,7 @@ export interface FileRouteTypes {
| '/'
| '/deferred'
| '/logout'
+ | '/navigate-test'
| '/api/users'
| '/posts/$postId'
| '/redirect/throw-it'
@@ -178,6 +189,7 @@ export interface FileRouteTypes {
| '/'
| '/deferred'
| '/logout'
+ | '/navigate-test'
| '/posts'
| '/users'
| '/api/users'
@@ -195,6 +207,7 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
DeferredRoute: typeof DeferredRoute
LogoutRoute: typeof LogoutRoute
+ NavigateTestRoute: typeof NavigateTestRoute
PostsRoute: typeof PostsRouteWithChildren
UsersRoute: typeof UsersRouteWithChildren
ApiUsersRoute: typeof ApiUsersRouteWithChildren
@@ -219,6 +232,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof PostsRouteImport
parentRoute: typeof rootRouteImport
}
+ '/navigate-test': {
+ id: '/navigate-test'
+ path: '/navigate-test'
+ fullPath: '/navigate-test'
+ preLoaderRoute: typeof NavigateTestRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/logout': {
id: '/logout'
path: '/logout'
@@ -346,6 +366,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
DeferredRoute: DeferredRoute,
LogoutRoute: LogoutRoute,
+ NavigateTestRoute: NavigateTestRoute,
PostsRoute: PostsRouteWithChildren,
UsersRoute: UsersRouteWithChildren,
ApiUsersRoute: ApiUsersRouteWithChildren,
diff --git a/e2e/react-start/custom-basepath/src/routes/navigate-test.tsx b/e2e/react-start/custom-basepath/src/routes/navigate-test.tsx
new file mode 100644
index 00000000000..db81aba5f6c
--- /dev/null
+++ b/e2e/react-start/custom-basepath/src/routes/navigate-test.tsx
@@ -0,0 +1,36 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/navigate-test')({
+ component: NavigateTest,
+})
+
+function NavigateTest() {
+ const navigate = Route.useNavigate()
+
+ return (
+
+
Navigate Test
+
+ navigate({
+ href: '/custom/basepath/posts',
+ })
+ }
+ >
+ Navigate to /posts using href with basepath
+ {' '}
+
+ navigate({
+ href: '/custom/basepath/posts',
+ reloadDocument: true,
+ })
+ }
+ >
+ Navigate to /posts using href with basepath (reloadDocument)
+
+
+ )
+}
diff --git a/e2e/react-start/custom-basepath/src/routes/posts.tsx b/e2e/react-start/custom-basepath/src/routes/posts.tsx
index 0f69c183419..0c309c23c11 100644
--- a/e2e/react-start/custom-basepath/src/routes/posts.tsx
+++ b/e2e/react-start/custom-basepath/src/routes/posts.tsx
@@ -18,7 +18,7 @@ function PostsComponent() {
const posts = Route.useLoaderData()
return (
-
+
{[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map(
(post) => {
diff --git a/e2e/react-start/custom-basepath/tests/navigation.spec.ts b/e2e/react-start/custom-basepath/tests/navigation.spec.ts
index 3e6a5bd1bea..49454e1b8e6 100644
--- a/e2e/react-start/custom-basepath/tests/navigation.spec.ts
+++ b/e2e/react-start/custom-basepath/tests/navigation.spec.ts
@@ -75,3 +75,28 @@ test('server-side redirect', async ({ page, baseURL }) => {
expect(headers.get('location')).toBe('/custom/basepath/posts/1')
})
})
+
+test('navigate() with href containing basepath', async ({ page, baseURL }) => {
+ await page.goto('/navigate-test')
+ await expect(page.getByTestId('navigate-test-component')).toBeVisible()
+
+ const btn = page.getByTestId('to-posts-href-with-basepath-btn')
+ await btn.click()
+ // Should navigate to /custom/basepath/posts, NOT /custom/basepath/custom/basepath/posts
+ await page.waitForURL(`${baseURL}/posts`)
+ await expect(page.getByTestId('posts-component')).toBeVisible()
+})
+
+test('navigate() with href containing basepath and reloadDocument=true', async ({
+ page,
+ baseURL,
+}) => {
+ await page.goto('/navigate-test')
+ await expect(page.getByTestId('navigate-test-component')).toBeVisible()
+
+ const btn = page.getByTestId('to-posts-href-with-basepath-reload-btn')
+ await btn.click()
+ // Should navigate to /custom/basepath/posts, NOT stay on current page
+ await page.waitForURL(`${baseURL}/posts`)
+ await expect(page.getByTestId('posts-component')).toBeVisible()
+})
diff --git a/e2e/solid-router/basepath-file-based/src/routes/index.tsx b/e2e/solid-router/basepath-file-based/src/routes/index.tsx
index a43e4e7313b..13923f844e9 100644
--- a/e2e/solid-router/basepath-file-based/src/routes/index.tsx
+++ b/e2e/solid-router/basepath-file-based/src/routes/index.tsx
@@ -19,6 +19,27 @@ function App() {
}
>
Navigate to /about with document reload
+ {' '}
+
+ navigate({
+ href: '/app/about',
+ })
+ }
+ >
+ Navigate to /about using href with basepath
+ {' '}
+
+ navigate({
+ href: '/app/about',
+ reloadDocument: true,
+ })
+ }
+ >
+ Navigate to /about using href with basepath (reloadDocument)
)
diff --git a/e2e/solid-router/basepath-file-based/tests/reload-document.test.ts b/e2e/solid-router/basepath-file-based/tests/reload-document.test.ts
index 3e60f3bedcb..a50d133115b 100644
--- a/e2e/solid-router/basepath-file-based/tests/reload-document.test.ts
+++ b/e2e/solid-router/basepath-file-based/tests/reload-document.test.ts
@@ -16,3 +16,27 @@ test('navigate() respects basepath for when reloadDocument=true', async ({
await page.waitForURL('/app/')
await expect(page.getByTestId(`home-component`)).toBeInViewport()
})
+
+test('navigate() with href containing basepath', async ({ page }) => {
+ await page.goto(`/app/`)
+ await expect(page.getByTestId(`home-component`)).toBeInViewport()
+
+ const aboutBtn = page.getByTestId(`to-about-href-with-basepath-btn`)
+ await aboutBtn.click()
+ // Should navigate to /app/about, NOT /app/app/about
+ await page.waitForURL('/app/about')
+ await expect(page.getByTestId(`about-component`)).toBeInViewport()
+})
+
+test('navigate() with href containing basepath and reloadDocument=true', async ({
+ page,
+}) => {
+ await page.goto(`/app/`)
+ await expect(page.getByTestId(`home-component`)).toBeInViewport()
+
+ const aboutBtn = page.getByTestId(`to-about-href-with-basepath-reload-btn`)
+ await aboutBtn.click()
+ // Should navigate to /app/about, NOT stay on current page
+ await page.waitForURL('/app/about')
+ await expect(page.getByTestId(`about-component`)).toBeInViewport()
+})
diff --git a/e2e/solid-start/custom-basepath/src/routeTree.gen.ts b/e2e/solid-start/custom-basepath/src/routeTree.gen.ts
index 1a94469caaf..8f6c31e98a3 100644
--- a/e2e/solid-start/custom-basepath/src/routeTree.gen.ts
+++ b/e2e/solid-start/custom-basepath/src/routeTree.gen.ts
@@ -11,6 +11,7 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as UsersRouteImport } from './routes/users'
import { Route as PostsRouteImport } from './routes/posts'
+import { Route as NavigateTestRouteImport } from './routes/navigate-test'
import { Route as LogoutRouteImport } from './routes/logout'
import { Route as DeferredRouteImport } from './routes/deferred'
import { Route as IndexRouteImport } from './routes/index'
@@ -34,6 +35,11 @@ const PostsRoute = PostsRouteImport.update({
path: '/posts',
getParentRoute: () => rootRouteImport,
} as any)
+const NavigateTestRoute = NavigateTestRouteImport.update({
+ id: '/navigate-test',
+ path: '/navigate-test',
+ getParentRoute: () => rootRouteImport,
+} as any)
const LogoutRoute = LogoutRouteImport.update({
id: '/logout',
path: '/logout',
@@ -99,6 +105,7 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/deferred': typeof DeferredRoute
'/logout': typeof LogoutRoute
+ '/navigate-test': typeof NavigateTestRoute
'/posts': typeof PostsRouteWithChildren
'/users': typeof UsersRouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
@@ -115,6 +122,7 @@ export interface FileRoutesByTo {
'/': typeof IndexRoute
'/deferred': typeof DeferredRoute
'/logout': typeof LogoutRoute
+ '/navigate-test': typeof NavigateTestRoute
'/api/users': typeof ApiUsersRouteWithChildren
'/posts/$postId': typeof PostsPostIdRoute
'/redirect/throw-it': typeof RedirectThrowItRoute
@@ -130,6 +138,7 @@ export interface FileRoutesById {
'/': typeof IndexRoute
'/deferred': typeof DeferredRoute
'/logout': typeof LogoutRoute
+ '/navigate-test': typeof NavigateTestRoute
'/posts': typeof PostsRouteWithChildren
'/users': typeof UsersRouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
@@ -148,6 +157,7 @@ export interface FileRouteTypes {
| '/'
| '/deferred'
| '/logout'
+ | '/navigate-test'
| '/posts'
| '/users'
| '/api/users'
@@ -164,6 +174,7 @@ export interface FileRouteTypes {
| '/'
| '/deferred'
| '/logout'
+ | '/navigate-test'
| '/api/users'
| '/posts/$postId'
| '/redirect/throw-it'
@@ -178,6 +189,7 @@ export interface FileRouteTypes {
| '/'
| '/deferred'
| '/logout'
+ | '/navigate-test'
| '/posts'
| '/users'
| '/api/users'
@@ -195,6 +207,7 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
DeferredRoute: typeof DeferredRoute
LogoutRoute: typeof LogoutRoute
+ NavigateTestRoute: typeof NavigateTestRoute
PostsRoute: typeof PostsRouteWithChildren
UsersRoute: typeof UsersRouteWithChildren
ApiUsersRoute: typeof ApiUsersRouteWithChildren
@@ -219,6 +232,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsRouteImport
parentRoute: typeof rootRouteImport
}
+ '/navigate-test': {
+ id: '/navigate-test'
+ path: '/navigate-test'
+ fullPath: '/navigate-test'
+ preLoaderRoute: typeof NavigateTestRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/logout': {
id: '/logout'
path: '/logout'
@@ -346,6 +366,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
DeferredRoute: DeferredRoute,
LogoutRoute: LogoutRoute,
+ NavigateTestRoute: NavigateTestRoute,
PostsRoute: PostsRouteWithChildren,
UsersRoute: UsersRouteWithChildren,
ApiUsersRoute: ApiUsersRouteWithChildren,
diff --git a/e2e/solid-start/custom-basepath/src/routes/navigate-test.tsx b/e2e/solid-start/custom-basepath/src/routes/navigate-test.tsx
new file mode 100644
index 00000000000..cd1cd1e1744
--- /dev/null
+++ b/e2e/solid-start/custom-basepath/src/routes/navigate-test.tsx
@@ -0,0 +1,36 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/navigate-test')({
+ component: NavigateTest,
+})
+
+function NavigateTest() {
+ const navigate = Route.useNavigate()
+
+ return (
+
+
Navigate Test
+
+ navigate({
+ href: '/custom/basepath/posts',
+ })
+ }
+ >
+ Navigate to /posts using href with basepath
+ {' '}
+
+ navigate({
+ href: '/custom/basepath/posts',
+ reloadDocument: true,
+ })
+ }
+ >
+ Navigate to /posts using href with basepath (reloadDocument)
+
+
+ )
+}
diff --git a/e2e/solid-start/custom-basepath/src/routes/posts.tsx b/e2e/solid-start/custom-basepath/src/routes/posts.tsx
index 0e94cd4d2cf..22187bf863b 100644
--- a/e2e/solid-start/custom-basepath/src/routes/posts.tsx
+++ b/e2e/solid-start/custom-basepath/src/routes/posts.tsx
@@ -19,7 +19,7 @@ function PostsComponent() {
const posts = Route.useLoaderData()
return (
-
+
{(post) => {
diff --git a/e2e/solid-start/custom-basepath/tests/navigation.spec.ts b/e2e/solid-start/custom-basepath/tests/navigation.spec.ts
index 3e6a5bd1bea..49454e1b8e6 100644
--- a/e2e/solid-start/custom-basepath/tests/navigation.spec.ts
+++ b/e2e/solid-start/custom-basepath/tests/navigation.spec.ts
@@ -75,3 +75,28 @@ test('server-side redirect', async ({ page, baseURL }) => {
expect(headers.get('location')).toBe('/custom/basepath/posts/1')
})
})
+
+test('navigate() with href containing basepath', async ({ page, baseURL }) => {
+ await page.goto('/navigate-test')
+ await expect(page.getByTestId('navigate-test-component')).toBeVisible()
+
+ const btn = page.getByTestId('to-posts-href-with-basepath-btn')
+ await btn.click()
+ // Should navigate to /custom/basepath/posts, NOT /custom/basepath/custom/basepath/posts
+ await page.waitForURL(`${baseURL}/posts`)
+ await expect(page.getByTestId('posts-component')).toBeVisible()
+})
+
+test('navigate() with href containing basepath and reloadDocument=true', async ({
+ page,
+ baseURL,
+}) => {
+ await page.goto('/navigate-test')
+ await expect(page.getByTestId('navigate-test-component')).toBeVisible()
+
+ const btn = page.getByTestId('to-posts-href-with-basepath-reload-btn')
+ await btn.click()
+ // Should navigate to /custom/basepath/posts, NOT stay on current page
+ await page.waitForURL(`${baseURL}/posts`)
+ await expect(page.getByTestId('posts-component')).toBeVisible()
+})
diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts
index 1e837bd4261..cf86c48bd46 100644
--- a/packages/router-core/src/router.ts
+++ b/packages/router-core/src/router.ts
@@ -1977,7 +1977,14 @@ export class RouterCore<
const parsed = parseHref(href, {
__TSR_index: replace ? currentIndex : currentIndex + 1,
})
- rest.to = parsed.pathname
+
+ // If the href contains the basepath, we need to strip it before setting `to`
+ // because `buildLocation` will add the basepath back when creating the final URL.
+ // Without this, hrefs like '/app/about' would become '/app/app/about'.
+ const hrefUrl = new URL(parsed.pathname, this.origin)
+ const rewrittenUrl = executeRewriteInput(this.rewrite, hrefUrl)
+
+ rest.to = rewrittenUrl.pathname
rest.search = this.options.parseSearch(parsed.search)
// remove the leading `#` from the hash
rest.hash = parsed.hash.slice(1)
@@ -2040,12 +2047,18 @@ export class RouterCore<
}
if (reloadDocument) {
- if (!href || (!publicHref && !hrefIsUrl)) {
+ // When to is provided, always build a location to get the proper publicHref
+ // (this handles redirects where href might be an internal path from resolveRedirect)
+ // When only href is provided (no to), use it directly as it should already
+ // be a complete path (possibly with basepath)
+ if (to !== undefined || !href) {
const location = this.buildLocation({ to, ...rest } as any)
href = href ?? location.url.href
publicHref = publicHref ?? location.url.href
}
+ // Use publicHref when available and href is not a full URL,
+ // otherwise use href directly (which may already include basepath)
const reloadHref = !hrefIsUrl && publicHref ? publicHref : href
// Check blockers for external URLs unless ignoreBlocker is true