Repository: TanStack/router (TypeScript)

Issue:
<Link target="_blank"> broken with hash history
### Which project does this relate to?

Router

### Describe the bug

Hi 👋,

After upgrading to the latest TanStack Router (v1.132.2), I noticed that `<Link>` elements using `target="_blank"` no longer work correctly when using hash history.

Previously, the rendered `<a>` looked like this:
```
<a href="/#/about" target="_blank">About</a>
```

Now the rendered `<a>` looks like this:
```
<a href="/about" target="_blank">About</a>
```

This means opening the link in a new tab results in the URL:
```
http://localhost:5173/about
```

instead of the expected:
```
http://localhost:5173/#/about
```

So the hash is missing in the final URL when `target="_blank"` is used, breaking navigation in hash-history mode.

---

Expected behavior:

The `<Link>` should render with the hash prefix (`/#/about`) so that opening it in a new tab lands on the correct route in hash-history mode.

Actual behavior:

The `<Link>` renders as `/about`, causing the browser to open the wrong URL without the hash.


### Your Example Website or App

https://stackblitz.com/edit/github-gsxsnwbi?file=src%2Froutes%2F__root.tsx

### Steps to Reproduce the Bug or Issue

1. Click on `About (target _blank)`.
2. It opens a new tab with the following URL: `<baseURL>/about` instead of `<baseURL>/#/about`

### Expected behavior

The `<Link>` should render with the hash prefix (`/#/about`) so that opening it in a new tab lands on the correct route in hash-history mode.


### Screenshots or Videos

_No response_

### Platform

- Router Version: v1.131.2
- History mode: `createHashHistory()`
- OS: macOS/Linux
- Browser: Chrome
- Browser Version: v138
- Bundler: vite
- Bundler Version:  v7.1.7


### Additional context

This only happens when using `target="_blank"`. Links without target navigate correctly within the same tab.


Discussion:
I think this works fine on the latest version(v1.132.6).
The same issue occurs on the latest version as well. But it’s not related to the target. When using HashRouter, the Link component does not add the “#” before the path.
> I think this works fine on the latest version(v1.132.6).

I've updated the reproduction link to use v1.132.6 and it's still not working as expected

https://stackblitz.com/edit/github-gsxsnwbi?file=src%2Froutes%2F__root.tsx
> The same issue occurs on the latest version as well. But it’s not related to the target. When using HashRouter, the Link component does not add the “#” before the path.

Yep! that is true, but regular links work fine even without the hash symbol, as the resulting URL gets it anyway. 
> > I think this works fine on the latest version(v1.132.6).
> 
> I've updated the reproduction link to use v1.132.6 and it's still not working as expected
> 
> https://stackblitz.com/edit/github-gsxsnwbi?file=src%2Froutes%2F__root.tsx

I realize now that I misunderstood — my apologies.
I’m not very familiar with hashHistory.
Could you try applying it with the version of the PR I opened?
> > > I think this works fine on the latest version(v1.132.6).
> > 
> > 
> > I've updated the reproduction link to use v1.132.6 and it's still not working as expected
> > https://stackblitz.com/edit/github-gsxsnwbi?file=src%2Froutes%2F__root.tsx
> 
> I realize now that I misunderstood — my apologies. I’m not very familiar with hashHistory. Could you try applying it with the version of the PR I opened?

No worries 🙂!

Your PR seems to in fact fix the issue, now all rendered `<a>` elements have the hash symbol (`#`) on their `href`, and work as expected regardless of whether the `target` property is present or not.

Thank you for your work!
Dan



Patch:
diff --git a/packages/react-router/src/link.tsx b/packages/react-router/src/link.tsx
index f5d2b987652..2d4c2a60daf 100644
--- a/packages/react-router/src/link.tsx
+++ b/packages/react-router/src/link.tsx
@@ -120,13 +120,13 @@ export function useLinkProps<
     let external = false
     if (router.origin) {
       if (href.startsWith(router.origin)) {
-        href = href.replace(router.origin, '') || '/'
+        href = router.history.createHref(href.replace(router.origin, '')) || '/'
       } else {
         external = true
       }
     }
     return { href, external }
-  }, [disabled, next.maskedLocation, next.url, router.origin])
+  }, [disabled, next.maskedLocation, next.url, router.origin, router.history])
 
   const externalLink = React.useMemo(() => {
     if (hrefOption?.external) {
diff --git a/packages/solid-router/src/link.tsx b/packages/solid-router/src/link.tsx
index ca0b70b9381..407f106d36b 100644
--- a/packages/solid-router/src/link.tsx
+++ b/packages/solid-router/src/link.tsx
@@ -146,7 +146,7 @@ export function useLinkProps<
     let external = false
     if (router.origin) {
       if (href.startsWith(router.origin)) {
-        href = href.replace(router.origin, '')
+        href = router.history.createHref(href.replace(router.origin, ''))
       } else {
         external = true
       }


Test patch:
diff --git a/packages/react-router/tests/link.test.tsx b/packages/react-router/tests/link.test.tsx
index d0c1d79b119..e9ca9564a62 100644
--- a/packages/react-router/tests/link.test.tsx
+++ b/packages/react-router/tests/link.test.tsx
@@ -17,6 +17,7 @@ import {
   Outlet,
   RouterProvider,
   createBrowserHistory,
+  createHashHistory,
   createLink,
   createMemoryHistory,
   createRootRoute,
@@ -6335,3 +6336,55 @@ describe('rewrite', () => {
     expect(appLink).toHaveAttribute('href', 'http://app.example.com/')
   })
 })
+
+describe('hash history with target="_blank" links', () => {
+  test('should generate correct href for target="_blank" links in hash history mode', async () => {
+    const hashHistory = createHashHistory()
+
+    const rootRoute = createRootRoute()
+    const indexRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: '/',
+      component: () => {
+        return (
+          <>
+            <h1>Index</h1>
+            <Link data-testid="posts-link" to="/posts">
+              Posts (same tab)
+            </Link>
+            <Link data-testid="about-blank-link" to="/about" target="_blank">
+              About (new tab)
+            </Link>
+          </>
+        )
+      },
+    })
+
+    const postsRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: '/posts',
+      component: () => <h1>Posts</h1>,
+    })
+
+    const aboutRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: '/about',
+      component: () => <h1>About</h1>,
+    })
+
+    const router = createRouter({
+      routeTree: rootRoute.addChildren([indexRoute, postsRoute, aboutRoute]),
+      history: hashHistory,
+    })
+
+    render(<RouterProvider router={router} />)
+
+    const postsLink = await screen.findByTestId('posts-link')
+    expect(postsLink).toHaveAttribute('href', '/#/posts')
+    expect(postsLink).not.toHaveAttribute('target', '_blank')
+
+    const postsBlankLink = await screen.findByTestId('about-blank-link')
+    expect(postsBlankLink).toHaveAttribute('href', '/#/about')
+    expect(postsBlankLink).toHaveAttribute('target', '_blank')
+  })
+})
diff --git a/packages/solid-router/tests/link.test.tsx b/packages/solid-router/tests/link.test.tsx
index e06100edf3b..b80bcb9442d 100644
--- a/packages/solid-router/tests/link.test.tsx
+++ b/packages/solid-router/tests/link.test.tsx
@@ -15,6 +15,7 @@ import {
   Outlet,
   RouterProvider,
   createBrowserHistory,
+  createHashHistory,
   createLink,
   createMemoryHistory,
   createRootRoute,
@@ -5674,3 +5675,55 @@ describe('relative links to from route', () => {
     },
   )
 })
+
+describe('hash history with target="_blank" links', () => {
+  test('should generate correct href for target="_blank" links in hash history mode', async () => {
+    const hashHistory = createHashHistory()
+
+    const rootRoute = createRootRoute()
+    const indexRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: '/',
+      component: () => {
+        return (
+          <>
+            <h1>Index</h1>
+            <Link data-testid="posts-link" to="/posts">
+              Posts (same tab)
+            </Link>
+            <Link data-testid="about-blank-link" to="/about" target="_blank">
+              About (new tab)
+            </Link>
+          </>
+        )
+      },
+    })
+
+    const postsRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: '/posts',
+      component: () => <h1>Posts</h1>,
+    })
+
+    const aboutRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: '/about',
+      component: () => <h1>About</h1>,
+    })
+
+    const router = createRouter({
+      routeTree: rootRoute.addChildren([indexRoute, postsRoute, aboutRoute]),
+      history: hashHistory,
+    })
+
+    render(() => <RouterProvider router={router} />)
+
+    const postsLink = await screen.findByTestId('posts-link')
+    expect(postsLink).toHaveAttribute('href', '/#/posts')
+    expect(postsLink).not.toHaveAttribute('target', '_blank')
+
+    const postsBlankLink = await screen.findByTestId('about-blank-link')
+    expect(postsBlankLink).toHaveAttribute('href', '/#/about')
+    expect(postsBlankLink).toHaveAttribute('target', '_blank')
+  })
+})