diff --git a/docs/router/framework/react/guide/navigation.md b/docs/router/framework/react/guide/navigation.md
index 42e0784d8ac..d40829ada7c 100644
--- a/docs/router/framework/react/guide/navigation.md
+++ b/docs/router/framework/react/guide/navigation.md
@@ -290,6 +290,16 @@ const link = (
)
```
+> ⚠️ When directly navigating to a URL with a hash fragment, the fragment is only available on the client; the browser does not send the fragment to the server as part of the request URL.
+>
+> This means that if you are using a server-side rendering approach, the hash fragment will not be available on the server-side, and hydration mismatches can occur when using the hash for rendering markup.
+>
+> Examples of this would be:
+>
+> - returning the hash value in the markup,
+> - conditional rendering based on the hash value, or
+> - setting the Link as active based on the hash value.
+
### Navigating with Optional Parameters
Optional path parameters provide flexible navigation patterns where you can include or omit parameters as needed. Optional parameters use the `{-$paramName}` syntax and offer fine-grained control over URL structure.
diff --git a/e2e/react-start/basic/src/routeTree.gen.ts b/e2e/react-start/basic/src/routeTree.gen.ts
index 1477ed0bac3..b5ecc4a50ff 100644
--- a/e2e/react-start/basic/src/routeTree.gen.ts
+++ b/e2e/react-start/basic/src/routeTree.gen.ts
@@ -34,6 +34,7 @@ import { Route as MultiCookieRedirectIndexRouteImport } from './routes/multi-coo
import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
import { Route as SpecialCharsChar45824Char54620Char48124Char44397RouteImport } from './routes/specialChars/대한민국'
import { Route as SpecialCharsSearchRouteImport } from './routes/specialChars/search'
+import { Route as SpecialCharsHashRouteImport } from './routes/specialChars/hash'
import { Route as SpecialCharsParamRouteImport } from './routes/specialChars/$param'
import { Route as SearchParamsLoaderThrowsRedirectRouteImport } from './routes/search-params/loader-throws-redirect'
import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/default'
@@ -193,6 +194,11 @@ const SpecialCharsSearchRoute = SpecialCharsSearchRouteImport.update({
path: '/search',
getParentRoute: () => SpecialCharsRouteRoute,
} as any)
+const SpecialCharsHashRoute = SpecialCharsHashRouteImport.update({
+ id: '/hash',
+ path: '/hash',
+ getParentRoute: () => SpecialCharsRouteRoute,
+} as any)
const SpecialCharsParamRoute = SpecialCharsParamRouteImport.update({
id: '/$param',
path: '/$param',
@@ -394,6 +400,7 @@ export interface FileRoutesByFullPath {
'/search-params/default': typeof SearchParamsDefaultRoute
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
'/specialChars/$param': typeof SpecialCharsParamRoute
+ '/specialChars/hash': typeof SpecialCharsHashRoute
'/specialChars/search': typeof SpecialCharsSearchRoute
'/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
'/users/$userId': typeof UsersUserIdRoute
@@ -445,6 +452,7 @@ export interface FileRoutesByTo {
'/search-params/default': typeof SearchParamsDefaultRoute
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
'/specialChars/$param': typeof SpecialCharsParamRoute
+ '/specialChars/hash': typeof SpecialCharsHashRoute
'/specialChars/search': typeof SpecialCharsSearchRoute
'/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
'/users/$userId': typeof UsersUserIdRoute
@@ -504,6 +512,7 @@ export interface FileRoutesById {
'/search-params/default': typeof SearchParamsDefaultRoute
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
'/specialChars/$param': typeof SpecialCharsParamRoute
+ '/specialChars/hash': typeof SpecialCharsHashRoute
'/specialChars/search': typeof SpecialCharsSearchRoute
'/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
'/users/$userId': typeof UsersUserIdRoute
@@ -563,6 +572,7 @@ export interface FileRouteTypes {
| '/search-params/default'
| '/search-params/loader-throws-redirect'
| '/specialChars/$param'
+ | '/specialChars/hash'
| '/specialChars/search'
| '/specialChars/대한민국'
| '/users/$userId'
@@ -614,6 +624,7 @@ export interface FileRouteTypes {
| '/search-params/default'
| '/search-params/loader-throws-redirect'
| '/specialChars/$param'
+ | '/specialChars/hash'
| '/specialChars/search'
| '/specialChars/대한민국'
| '/users/$userId'
@@ -672,6 +683,7 @@ export interface FileRouteTypes {
| '/search-params/default'
| '/search-params/loader-throws-redirect'
| '/specialChars/$param'
+ | '/specialChars/hash'
| '/specialChars/search'
| '/specialChars/대한민국'
| '/users/$userId'
@@ -901,6 +913,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SpecialCharsSearchRouteImport
parentRoute: typeof SpecialCharsRouteRoute
}
+ '/specialChars/hash': {
+ id: '/specialChars/hash'
+ path: '/hash'
+ fullPath: '/specialChars/hash'
+ preLoaderRoute: typeof SpecialCharsHashRouteImport
+ parentRoute: typeof SpecialCharsRouteRoute
+ }
'/specialChars/$param': {
id: '/specialChars/$param'
path: '/$param'
@@ -1178,6 +1197,7 @@ const SpecialCharsMalformedRouteRouteWithChildren =
interface SpecialCharsRouteRouteChildren {
SpecialCharsMalformedRouteRoute: typeof SpecialCharsMalformedRouteRouteWithChildren
SpecialCharsParamRoute: typeof SpecialCharsParamRoute
+ SpecialCharsHashRoute: typeof SpecialCharsHashRoute
SpecialCharsSearchRoute: typeof SpecialCharsSearchRoute
SpecialCharsChar45824Char54620Char48124Char44397Route: typeof SpecialCharsChar45824Char54620Char48124Char44397Route
}
@@ -1185,6 +1205,7 @@ interface SpecialCharsRouteRouteChildren {
const SpecialCharsRouteRouteChildren: SpecialCharsRouteRouteChildren = {
SpecialCharsMalformedRouteRoute: SpecialCharsMalformedRouteRouteWithChildren,
SpecialCharsParamRoute: SpecialCharsParamRoute,
+ SpecialCharsHashRoute: SpecialCharsHashRoute,
SpecialCharsSearchRoute: SpecialCharsSearchRoute,
SpecialCharsChar45824Char54620Char48124Char44397Route:
SpecialCharsChar45824Char54620Char48124Char44397Route,
diff --git a/e2e/react-start/basic/src/routes/specialChars/hash.tsx b/e2e/react-start/basic/src/routes/specialChars/hash.tsx
new file mode 100644
index 00000000000..1f1227e0207
--- /dev/null
+++ b/e2e/react-start/basic/src/routes/specialChars/hash.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute, useLocation } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/specialChars/hash')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const l = useLocation()
+ return (
+
+ Hello "/specialChars/hash"!
+ {l.hash}
+
+ )
+}
diff --git a/e2e/react-start/basic/src/routes/specialChars/route.tsx b/e2e/react-start/basic/src/routes/specialChars/route.tsx
index a6069cff481..4e75790ddd2 100644
--- a/e2e/react-start/basic/src/routes/specialChars/route.tsx
+++ b/e2e/react-start/basic/src/routes/specialChars/route.tsx
@@ -37,6 +37,19 @@ function RouteComponent() {
>
Unicode search param
{' '}
+
+ Unicode Hash
+ {' '}
{
})
})
+ test.describe('Special characters in url hash', () => {
+ test('should render route correctly on direct navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ await expect(page.getByTestId('special-hash-link')).not.toHaveClass(
+ 'font-bold',
+ )
+ await page.goto('/specialChars/hash#대|')
+
+ await page.waitForURL(`${baseURL}/specialChars/hash#%EB%8C%80|`)
+ await page.waitForLoadState('load')
+
+ await expect(page.getByTestId('special-hash-heading')).toBeInViewport()
+
+ const hashValue = await page.getByTestId('special-hash').textContent()
+
+ await expect(page.getByTestId('special-hash-link')).toHaveClass(
+ 'font-bold',
+ )
+ expect(hashValue).toBe('대|')
+ })
+
+ test('should render route correctly on router navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ await expect(page.getByTestId('special-hash-link')).not.toHaveClass(
+ 'font-bold',
+ )
+ const link = page.getByTestId('special-hash-link')
+
+ await link.click()
+
+ await page.waitForURL(`${baseURL}/specialChars/hash#%EB%8C%80|`)
+ await page.waitForLoadState('load')
+
+ await expect(page.getByTestId('special-hash-heading')).toBeInViewport()
+
+ const hashValue = await page.getByTestId('special-hash').textContent()
+
+ await expect(page.getByTestId('special-hash-link')).toHaveClass(
+ 'font-bold',
+ )
+ expect(hashValue).toBe('대|')
+ })
+ })
+
test.describe('malformed paths', () => {
test.use({
whitelistErrors: [
diff --git a/e2e/react-start/basic/vite.config.ts b/e2e/react-start/basic/vite.config.ts
index f0a0fe6a1b5..d196ac95385 100644
--- a/e2e/react-start/basic/vite.config.ts
+++ b/e2e/react-start/basic/vite.config.ts
@@ -23,6 +23,7 @@ const prerenderConfiguration = {
'/not-found/via-beforeLoad',
'/not-found/via-loader',
'/specialChars/search',
+ '/specialChars/hash',
'/specialChars/malformed',
'/users',
].some((p) => page.path.includes(p)),
diff --git a/e2e/solid-start/basic/src/routeTree.gen.ts b/e2e/solid-start/basic/src/routeTree.gen.ts
index 70ae25782e3..ad5f5431487 100644
--- a/e2e/solid-start/basic/src/routeTree.gen.ts
+++ b/e2e/solid-start/basic/src/routeTree.gen.ts
@@ -32,6 +32,7 @@ import { Route as MultiCookieRedirectIndexRouteImport } from './routes/multi-coo
import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
import { Route as SpecialCharsChar45824Char54620Char48124Char44397RouteImport } from './routes/specialChars/대한민국'
import { Route as SpecialCharsSearchRouteImport } from './routes/specialChars/search'
+import { Route as SpecialCharsHashRouteImport } from './routes/specialChars/hash'
import { Route as SpecialCharsParamRouteImport } from './routes/specialChars/$param'
import { Route as SearchParamsLoaderThrowsRedirectRouteImport } from './routes/search-params/loader-throws-redirect'
import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/default'
@@ -181,6 +182,11 @@ const SpecialCharsSearchRoute = SpecialCharsSearchRouteImport.update({
path: '/search',
getParentRoute: () => SpecialCharsRouteRoute,
} as any)
+const SpecialCharsHashRoute = SpecialCharsHashRouteImport.update({
+ id: '/hash',
+ path: '/hash',
+ getParentRoute: () => SpecialCharsRouteRoute,
+} as any)
const SpecialCharsParamRoute = SpecialCharsParamRouteImport.update({
id: '/$param',
path: '/$param',
@@ -382,6 +388,7 @@ export interface FileRoutesByFullPath {
'/search-params/default': typeof SearchParamsDefaultRoute
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
'/specialChars/$param': typeof SpecialCharsParamRoute
+ '/specialChars/hash': typeof SpecialCharsHashRoute
'/specialChars/search': typeof SpecialCharsSearchRoute
'/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
'/users/$userId': typeof UsersUserIdRoute
@@ -431,6 +438,7 @@ export interface FileRoutesByTo {
'/search-params/default': typeof SearchParamsDefaultRoute
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
'/specialChars/$param': typeof SpecialCharsParamRoute
+ '/specialChars/hash': typeof SpecialCharsHashRoute
'/specialChars/search': typeof SpecialCharsSearchRoute
'/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
'/users/$userId': typeof UsersUserIdRoute
@@ -489,6 +497,7 @@ export interface FileRoutesById {
'/search-params/default': typeof SearchParamsDefaultRoute
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
'/specialChars/$param': typeof SpecialCharsParamRoute
+ '/specialChars/hash': typeof SpecialCharsHashRoute
'/specialChars/search': typeof SpecialCharsSearchRoute
'/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
'/users/$userId': typeof UsersUserIdRoute
@@ -546,6 +555,7 @@ export interface FileRouteTypes {
| '/search-params/default'
| '/search-params/loader-throws-redirect'
| '/specialChars/$param'
+ | '/specialChars/hash'
| '/specialChars/search'
| '/specialChars/대한민국'
| '/users/$userId'
@@ -595,6 +605,7 @@ export interface FileRouteTypes {
| '/search-params/default'
| '/search-params/loader-throws-redirect'
| '/specialChars/$param'
+ | '/specialChars/hash'
| '/specialChars/search'
| '/specialChars/대한민국'
| '/users/$userId'
@@ -652,6 +663,7 @@ export interface FileRouteTypes {
| '/search-params/default'
| '/search-params/loader-throws-redirect'
| '/specialChars/$param'
+ | '/specialChars/hash'
| '/specialChars/search'
| '/specialChars/대한민국'
| '/users/$userId'
@@ -866,6 +878,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof SpecialCharsSearchRouteImport
parentRoute: typeof SpecialCharsRouteRoute
}
+ '/specialChars/hash': {
+ id: '/specialChars/hash'
+ path: '/hash'
+ fullPath: '/specialChars/hash'
+ preLoaderRoute: typeof SpecialCharsHashRouteImport
+ parentRoute: typeof SpecialCharsRouteRoute
+ }
'/specialChars/$param': {
id: '/specialChars/$param'
path: '/$param'
@@ -1143,6 +1162,7 @@ const SpecialCharsMalformedRouteRouteWithChildren =
interface SpecialCharsRouteRouteChildren {
SpecialCharsMalformedRouteRoute: typeof SpecialCharsMalformedRouteRouteWithChildren
SpecialCharsParamRoute: typeof SpecialCharsParamRoute
+ SpecialCharsHashRoute: typeof SpecialCharsHashRoute
SpecialCharsSearchRoute: typeof SpecialCharsSearchRoute
SpecialCharsChar45824Char54620Char48124Char44397Route: typeof SpecialCharsChar45824Char54620Char48124Char44397Route
}
@@ -1150,6 +1170,7 @@ interface SpecialCharsRouteRouteChildren {
const SpecialCharsRouteRouteChildren: SpecialCharsRouteRouteChildren = {
SpecialCharsMalformedRouteRoute: SpecialCharsMalformedRouteRouteWithChildren,
SpecialCharsParamRoute: SpecialCharsParamRoute,
+ SpecialCharsHashRoute: SpecialCharsHashRoute,
SpecialCharsSearchRoute: SpecialCharsSearchRoute,
SpecialCharsChar45824Char54620Char48124Char44397Route:
SpecialCharsChar45824Char54620Char48124Char44397Route,
diff --git a/e2e/solid-start/basic/src/routes/specialChars/hash.tsx b/e2e/solid-start/basic/src/routes/specialChars/hash.tsx
new file mode 100644
index 00000000000..43d24ada29a
--- /dev/null
+++ b/e2e/solid-start/basic/src/routes/specialChars/hash.tsx
@@ -0,0 +1,22 @@
+import { createFileRoute, useLocation } from '@tanstack/solid-router'
+import { createEffect, createSignal } from 'solid-js'
+
+export const Route = createFileRoute('/specialChars/hash')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const location = useLocation()
+ const [getHash, setHash] = createSignal('')
+
+ createEffect(() => {
+ setHash(location().hash)
+ })
+
+ return (
+
+ Hello "/specialChars/hash"!
+ {getHash()}
+
+ )
+}
diff --git a/e2e/solid-start/basic/src/routes/specialChars/route.tsx b/e2e/solid-start/basic/src/routes/specialChars/route.tsx
index 0bfad91cd77..863cb795068 100644
--- a/e2e/solid-start/basic/src/routes/specialChars/route.tsx
+++ b/e2e/solid-start/basic/src/routes/specialChars/route.tsx
@@ -37,6 +37,19 @@ function RouteComponent() {
>
Unicode search param
{' '}
+
+ Unicode Hash
+ {' '}
{
})
})
+ test.describe('Special characters in url hash', () => {
+ test('should render route correctly on direct navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ await expect(page.getByTestId('special-hash-link')).not.toHaveClass(
+ 'font-bold',
+ )
+ await page.goto('/specialChars/hash#대|')
+
+ await page.waitForURL(`${baseURL}/specialChars/hash#%EB%8C%80|`)
+ await page.waitForLoadState('load')
+
+ await expect(page.getByTestId('special-hash-heading')).toBeInViewport()
+
+ const hashValue = await page.getByTestId('special-hash').textContent()
+
+ const el = await page
+ .getByTestId('special-hash-link')
+ .evaluate((e) => e.classList.value)
+
+ expect(el).toContain('font-bold')
+
+ expect(hashValue).toBe('대|')
+ })
+
+ test('should render route correctly on router navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ await expect(page.getByTestId('special-hash-link')).not.toHaveClass(
+ 'font-bold',
+ )
+ const link = page.getByTestId('special-hash-link')
+
+ await link.click()
+
+ await page.waitForURL(`${baseURL}/specialChars/hash#%EB%8C%80|`)
+ await page.waitForLoadState('load')
+
+ await expect(page.getByTestId('special-hash-heading')).toBeInViewport()
+
+ const hashValue = await page.getByTestId('special-hash').textContent()
+
+ await expect(page.getByTestId('special-hash-link')).toHaveClass(
+ 'font-bold',
+ )
+ expect(hashValue).toBe('대|')
+ })
+ })
+
test.describe('malformed paths', () => {
test.use({
whitelistErrors: [
diff --git a/e2e/solid-start/basic/vite.config.ts b/e2e/solid-start/basic/vite.config.ts
index 1301ae91393..8db88f34903 100644
--- a/e2e/solid-start/basic/vite.config.ts
+++ b/e2e/solid-start/basic/vite.config.ts
@@ -23,6 +23,7 @@ const prerenderConfiguration = {
'/not-found/via-beforeLoad',
'/not-found/via-loader',
'/specialChars/search',
+ '/specialChars/hash',
'/specialChars/malformed',
'/search-params/default',
'/transition',
diff --git a/e2e/vue-start/basic/src/routeTree.gen.ts b/e2e/vue-start/basic/src/routeTree.gen.ts
index def40f2e94e..77a84e6ecaa 100644
--- a/e2e/vue-start/basic/src/routeTree.gen.ts
+++ b/e2e/vue-start/basic/src/routeTree.gen.ts
@@ -32,6 +32,7 @@ import { Route as MultiCookieRedirectIndexRouteImport } from './routes/multi-coo
import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
import { Route as SpecialCharsChar45824Char54620Char48124Char44397RouteImport } from './routes/specialChars/대한민국'
import { Route as SpecialCharsSearchRouteImport } from './routes/specialChars/search'
+import { Route as SpecialCharsHashRouteImport } from './routes/specialChars/hash'
import { Route as SpecialCharsParamRouteImport } from './routes/specialChars/$param'
import { Route as SearchParamsLoaderThrowsRedirectRouteImport } from './routes/search-params/loader-throws-redirect'
import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/default'
@@ -179,6 +180,11 @@ const SpecialCharsSearchRoute = SpecialCharsSearchRouteImport.update({
path: '/search',
getParentRoute: () => SpecialCharsRouteRoute,
} as any)
+const SpecialCharsHashRoute = SpecialCharsHashRouteImport.update({
+ id: '/hash',
+ path: '/hash',
+ getParentRoute: () => SpecialCharsRouteRoute,
+} as any)
const SpecialCharsParamRoute = SpecialCharsParamRouteImport.update({
id: '/$param',
path: '/$param',
@@ -368,6 +374,7 @@ export interface FileRoutesByFullPath {
'/search-params/default': typeof SearchParamsDefaultRoute
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
'/specialChars/$param': typeof SpecialCharsParamRoute
+ '/specialChars/hash': typeof SpecialCharsHashRoute
'/specialChars/search': typeof SpecialCharsSearchRoute
'/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
'/users/$userId': typeof UsersUserIdRoute
@@ -415,6 +422,7 @@ export interface FileRoutesByTo {
'/search-params/default': typeof SearchParamsDefaultRoute
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
'/specialChars/$param': typeof SpecialCharsParamRoute
+ '/specialChars/hash': typeof SpecialCharsHashRoute
'/specialChars/search': typeof SpecialCharsSearchRoute
'/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
'/users/$userId': typeof UsersUserIdRoute
@@ -471,6 +479,7 @@ export interface FileRoutesById {
'/search-params/default': typeof SearchParamsDefaultRoute
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
'/specialChars/$param': typeof SpecialCharsParamRoute
+ '/specialChars/hash': typeof SpecialCharsHashRoute
'/specialChars/search': typeof SpecialCharsSearchRoute
'/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
'/users/$userId': typeof UsersUserIdRoute
@@ -526,6 +535,7 @@ export interface FileRouteTypes {
| '/search-params/default'
| '/search-params/loader-throws-redirect'
| '/specialChars/$param'
+ | '/specialChars/hash'
| '/specialChars/search'
| '/specialChars/대한민국'
| '/users/$userId'
@@ -573,6 +583,7 @@ export interface FileRouteTypes {
| '/search-params/default'
| '/search-params/loader-throws-redirect'
| '/specialChars/$param'
+ | '/specialChars/hash'
| '/specialChars/search'
| '/specialChars/대한민국'
| '/users/$userId'
@@ -628,6 +639,7 @@ export interface FileRouteTypes {
| '/search-params/default'
| '/search-params/loader-throws-redirect'
| '/specialChars/$param'
+ | '/specialChars/hash'
| '/specialChars/search'
| '/specialChars/대한민국'
| '/users/$userId'
@@ -838,6 +850,13 @@ declare module '@tanstack/vue-router' {
preLoaderRoute: typeof SpecialCharsSearchRouteImport
parentRoute: typeof SpecialCharsRouteRoute
}
+ '/specialChars/hash': {
+ id: '/specialChars/hash'
+ path: '/hash'
+ fullPath: '/specialChars/hash'
+ preLoaderRoute: typeof SpecialCharsHashRouteImport
+ parentRoute: typeof SpecialCharsRouteRoute
+ }
'/specialChars/$param': {
id: '/specialChars/$param'
path: '/$param'
@@ -1101,6 +1120,7 @@ const SpecialCharsMalformedRouteRouteWithChildren =
interface SpecialCharsRouteRouteChildren {
SpecialCharsMalformedRouteRoute: typeof SpecialCharsMalformedRouteRouteWithChildren
SpecialCharsParamRoute: typeof SpecialCharsParamRoute
+ SpecialCharsHashRoute: typeof SpecialCharsHashRoute
SpecialCharsSearchRoute: typeof SpecialCharsSearchRoute
SpecialCharsChar45824Char54620Char48124Char44397Route: typeof SpecialCharsChar45824Char54620Char48124Char44397Route
}
@@ -1108,6 +1128,7 @@ interface SpecialCharsRouteRouteChildren {
const SpecialCharsRouteRouteChildren: SpecialCharsRouteRouteChildren = {
SpecialCharsMalformedRouteRoute: SpecialCharsMalformedRouteRouteWithChildren,
SpecialCharsParamRoute: SpecialCharsParamRoute,
+ SpecialCharsHashRoute: SpecialCharsHashRoute,
SpecialCharsSearchRoute: SpecialCharsSearchRoute,
SpecialCharsChar45824Char54620Char48124Char44397Route:
SpecialCharsChar45824Char54620Char48124Char44397Route,
diff --git a/e2e/vue-start/basic/src/routes/specialChars/hash.tsx b/e2e/vue-start/basic/src/routes/specialChars/hash.tsx
new file mode 100644
index 00000000000..1f76f5c7bba
--- /dev/null
+++ b/e2e/vue-start/basic/src/routes/specialChars/hash.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute, useLocation } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/specialChars/hash')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const l = useLocation()
+ return (
+
+ Hello "/specialChars/hash"!
+ {l.value.hash}
+
+ )
+}
diff --git a/e2e/vue-start/basic/src/routes/specialChars/route.tsx b/e2e/vue-start/basic/src/routes/specialChars/route.tsx
index a34cf5ee64a..b0366b65f69 100644
--- a/e2e/vue-start/basic/src/routes/specialChars/route.tsx
+++ b/e2e/vue-start/basic/src/routes/specialChars/route.tsx
@@ -37,6 +37,19 @@ function RouteComponent() {
>
Unicode search param
{' '}
+
+ Unicode Hash
+ {' '}
{
})
})
+ test.describe('Special characters in url hash', () => {
+ test('should render route correctly on direct navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ await expect(page.getByTestId('special-hash-link')).not.toHaveClass(
+ 'font-bold',
+ )
+ await page.goto('/specialChars/hash#대|')
+
+ await page.waitForURL(`${baseURL}/specialChars/hash#%EB%8C%80|`)
+ await page.waitForLoadState('load')
+
+ await expect(page.getByTestId('special-hash-heading')).toBeInViewport()
+
+ const hashValue = await page.getByTestId('special-hash').textContent()
+
+ await expect(page.getByTestId('special-hash-link')).toHaveClass(
+ 'font-bold',
+ )
+ expect(hashValue).toBe('대|')
+ })
+
+ test('should render route correctly on router navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ await expect(page.getByTestId('special-hash-link')).not.toHaveClass(
+ 'font-bold',
+ )
+ const link = page.getByTestId('special-hash-link')
+
+ await link.click()
+
+ await page.waitForURL(`${baseURL}/specialChars/hash#%EB%8C%80|`)
+ await page.waitForLoadState('load')
+
+ await expect(page.getByTestId('special-hash-heading')).toBeInViewport()
+
+ const hashValue = await page.getByTestId('special-hash').textContent()
+
+ await expect(page.getByTestId('special-hash-link')).toHaveClass(
+ 'font-bold',
+ )
+ expect(hashValue).toBe('대|')
+ })
+ })
+
test.describe('malformed paths', () => {
test.use({
whitelistErrors: [
diff --git a/e2e/vue-start/basic/vite.config.ts b/e2e/vue-start/basic/vite.config.ts
index 222fce19b32..7fa09b226cd 100644
--- a/e2e/vue-start/basic/vite.config.ts
+++ b/e2e/vue-start/basic/vite.config.ts
@@ -23,6 +23,7 @@ const prerenderConfiguration = {
'/not-found/via-beforeLoad',
'/not-found/via-loader',
'/specialChars/search',
+ '/specialChars/hash',
'/specialChars/malformed',
'/search-params', // search-param routes have dynamic content based on query params
'/transition',
diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts
index 57a28df667f..e35fa7d1f31 100644
--- a/packages/router-core/src/router.ts
+++ b/packages/router-core/src/router.ts
@@ -1199,7 +1199,7 @@ export class RouterCore<
pathname: decodePath(url.pathname),
searchStr,
search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
- hash: url.hash.split('#').reverse()[0] ?? '',
+ hash: decodePath(url.hash.split('#').reverse()[0] ?? ''),
state: replaceEqualDeep(previousLocation?.state, state),
}
}
diff --git a/packages/solid-router/src/link.tsx b/packages/solid-router/src/link.tsx
index 04ddc655960..fb1ec6e3c27 100644
--- a/packages/solid-router/src/link.tsx
+++ b/packages/solid-router/src/link.tsx
@@ -230,7 +230,8 @@ export function useLinkProps<
}
}
- if (local.activeOptions?.includeHash) {
+ // url hash is not available on server, so do not evaluate this here when on server
+ if (local.activeOptions?.includeHash && !router.isServer) {
return s.location.hash === next().hash
}
return true