Skip to content

Commit 3da0227

Browse files
nlynzaadautofix-ci[bot]coderabbitai[bot]
authored
fix: encoded url hash fragment matching (#6416)
* decode hash * ci: apply automated fixes * cleanup * revert change to basic-rsc * add note in docs about hash fragment * ci: apply automated fixes * code rabbit suggestion Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent a857c3b commit 3da0227

File tree

18 files changed

+317
-2
lines changed

18 files changed

+317
-2
lines changed

docs/router/framework/react/guide/navigation.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,16 @@ const link = (
290290
)
291291
```
292292

293+
> ⚠️ 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.
294+
>
295+
> 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.
296+
>
297+
> Examples of this would be:
298+
>
299+
> - returning the hash value in the markup,
300+
> - conditional rendering based on the hash value, or
301+
> - setting the Link as active based on the hash value.
302+
293303
### Navigating with Optional Parameters
294304

295305
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.

e2e/react-start/basic/src/routeTree.gen.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { Route as MultiCookieRedirectIndexRouteImport } from './routes/multi-coo
3434
import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
3535
import { Route as SpecialCharsChar45824Char54620Char48124Char44397RouteImport } from './routes/specialChars/대한민국'
3636
import { Route as SpecialCharsSearchRouteImport } from './routes/specialChars/search'
37+
import { Route as SpecialCharsHashRouteImport } from './routes/specialChars/hash'
3738
import { Route as SpecialCharsParamRouteImport } from './routes/specialChars/$param'
3839
import { Route as SearchParamsLoaderThrowsRedirectRouteImport } from './routes/search-params/loader-throws-redirect'
3940
import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/default'
@@ -193,6 +194,11 @@ const SpecialCharsSearchRoute = SpecialCharsSearchRouteImport.update({
193194
path: '/search',
194195
getParentRoute: () => SpecialCharsRouteRoute,
195196
} as any)
197+
const SpecialCharsHashRoute = SpecialCharsHashRouteImport.update({
198+
id: '/hash',
199+
path: '/hash',
200+
getParentRoute: () => SpecialCharsRouteRoute,
201+
} as any)
196202
const SpecialCharsParamRoute = SpecialCharsParamRouteImport.update({
197203
id: '/$param',
198204
path: '/$param',
@@ -394,6 +400,7 @@ export interface FileRoutesByFullPath {
394400
'/search-params/default': typeof SearchParamsDefaultRoute
395401
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
396402
'/specialChars/$param': typeof SpecialCharsParamRoute
403+
'/specialChars/hash': typeof SpecialCharsHashRoute
397404
'/specialChars/search': typeof SpecialCharsSearchRoute
398405
'/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
399406
'/users/$userId': typeof UsersUserIdRoute
@@ -445,6 +452,7 @@ export interface FileRoutesByTo {
445452
'/search-params/default': typeof SearchParamsDefaultRoute
446453
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
447454
'/specialChars/$param': typeof SpecialCharsParamRoute
455+
'/specialChars/hash': typeof SpecialCharsHashRoute
448456
'/specialChars/search': typeof SpecialCharsSearchRoute
449457
'/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
450458
'/users/$userId': typeof UsersUserIdRoute
@@ -504,6 +512,7 @@ export interface FileRoutesById {
504512
'/search-params/default': typeof SearchParamsDefaultRoute
505513
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
506514
'/specialChars/$param': typeof SpecialCharsParamRoute
515+
'/specialChars/hash': typeof SpecialCharsHashRoute
507516
'/specialChars/search': typeof SpecialCharsSearchRoute
508517
'/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
509518
'/users/$userId': typeof UsersUserIdRoute
@@ -563,6 +572,7 @@ export interface FileRouteTypes {
563572
| '/search-params/default'
564573
| '/search-params/loader-throws-redirect'
565574
| '/specialChars/$param'
575+
| '/specialChars/hash'
566576
| '/specialChars/search'
567577
| '/specialChars/대한민국'
568578
| '/users/$userId'
@@ -614,6 +624,7 @@ export interface FileRouteTypes {
614624
| '/search-params/default'
615625
| '/search-params/loader-throws-redirect'
616626
| '/specialChars/$param'
627+
| '/specialChars/hash'
617628
| '/specialChars/search'
618629
| '/specialChars/대한민국'
619630
| '/users/$userId'
@@ -672,6 +683,7 @@ export interface FileRouteTypes {
672683
| '/search-params/default'
673684
| '/search-params/loader-throws-redirect'
674685
| '/specialChars/$param'
686+
| '/specialChars/hash'
675687
| '/specialChars/search'
676688
| '/specialChars/대한민국'
677689
| '/users/$userId'
@@ -901,6 +913,13 @@ declare module '@tanstack/react-router' {
901913
preLoaderRoute: typeof SpecialCharsSearchRouteImport
902914
parentRoute: typeof SpecialCharsRouteRoute
903915
}
916+
'/specialChars/hash': {
917+
id: '/specialChars/hash'
918+
path: '/hash'
919+
fullPath: '/specialChars/hash'
920+
preLoaderRoute: typeof SpecialCharsHashRouteImport
921+
parentRoute: typeof SpecialCharsRouteRoute
922+
}
904923
'/specialChars/$param': {
905924
id: '/specialChars/$param'
906925
path: '/$param'
@@ -1178,13 +1197,15 @@ const SpecialCharsMalformedRouteRouteWithChildren =
11781197
interface SpecialCharsRouteRouteChildren {
11791198
SpecialCharsMalformedRouteRoute: typeof SpecialCharsMalformedRouteRouteWithChildren
11801199
SpecialCharsParamRoute: typeof SpecialCharsParamRoute
1200+
SpecialCharsHashRoute: typeof SpecialCharsHashRoute
11811201
SpecialCharsSearchRoute: typeof SpecialCharsSearchRoute
11821202
SpecialCharsChar45824Char54620Char48124Char44397Route: typeof SpecialCharsChar45824Char54620Char48124Char44397Route
11831203
}
11841204

11851205
const SpecialCharsRouteRouteChildren: SpecialCharsRouteRouteChildren = {
11861206
SpecialCharsMalformedRouteRoute: SpecialCharsMalformedRouteRouteWithChildren,
11871207
SpecialCharsParamRoute: SpecialCharsParamRoute,
1208+
SpecialCharsHashRoute: SpecialCharsHashRoute,
11881209
SpecialCharsSearchRoute: SpecialCharsSearchRoute,
11891210
SpecialCharsChar45824Char54620Char48124Char44397Route:
11901211
SpecialCharsChar45824Char54620Char48124Char44397Route,
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { createFileRoute, useLocation } from '@tanstack/react-router'
2+
3+
export const Route = createFileRoute('/specialChars/hash')({
4+
component: RouteComponent,
5+
})
6+
7+
function RouteComponent() {
8+
const l = useLocation()
9+
return (
10+
<div data-testid="special-hash-heading">
11+
Hello "/specialChars/hash"!
12+
<span data-testid="special-hash">{l.hash}</span>
13+
</div>
14+
)
15+
}

e2e/react-start/basic/src/routes/specialChars/route.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,19 @@ function RouteComponent() {
3737
>
3838
Unicode search param
3939
</Link>{' '}
40+
<Link
41+
to="/specialChars/hash"
42+
activeOptions={{
43+
includeHash: true,
44+
}}
45+
activeProps={{
46+
className: 'font-bold',
47+
}}
48+
hash={'대|'}
49+
data-testid="special-hash-link"
50+
>
51+
Unicode Hash
52+
</Link>{' '}
4053
<Link
4154
to="/specialChars/malformed"
4255
activeProps={{

e2e/react-start/basic/tests/special-characters.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,54 @@ test.describe('Unicode route rendering', () => {
103103
})
104104
})
105105

106+
test.describe('Special characters in url hash', () => {
107+
test('should render route correctly on direct navigation', async ({
108+
page,
109+
baseURL,
110+
}) => {
111+
await expect(page.getByTestId('special-hash-link')).not.toHaveClass(
112+
'font-bold',
113+
)
114+
await page.goto('/specialChars/hash#대|')
115+
116+
await page.waitForURL(`${baseURL}/specialChars/hash#%EB%8C%80|`)
117+
await page.waitForLoadState('load')
118+
119+
await expect(page.getByTestId('special-hash-heading')).toBeInViewport()
120+
121+
const hashValue = await page.getByTestId('special-hash').textContent()
122+
123+
await expect(page.getByTestId('special-hash-link')).toHaveClass(
124+
'font-bold',
125+
)
126+
expect(hashValue).toBe('대|')
127+
})
128+
129+
test('should render route correctly on router navigation', async ({
130+
page,
131+
baseURL,
132+
}) => {
133+
await expect(page.getByTestId('special-hash-link')).not.toHaveClass(
134+
'font-bold',
135+
)
136+
const link = page.getByTestId('special-hash-link')
137+
138+
await link.click()
139+
140+
await page.waitForURL(`${baseURL}/specialChars/hash#%EB%8C%80|`)
141+
await page.waitForLoadState('load')
142+
143+
await expect(page.getByTestId('special-hash-heading')).toBeInViewport()
144+
145+
const hashValue = await page.getByTestId('special-hash').textContent()
146+
147+
await expect(page.getByTestId('special-hash-link')).toHaveClass(
148+
'font-bold',
149+
)
150+
expect(hashValue).toBe('대|')
151+
})
152+
})
153+
106154
test.describe('malformed paths', () => {
107155
test.use({
108156
whitelistErrors: [

e2e/react-start/basic/vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const prerenderConfiguration = {
2323
'/not-found/via-beforeLoad',
2424
'/not-found/via-loader',
2525
'/specialChars/search',
26+
'/specialChars/hash',
2627
'/specialChars/malformed',
2728
'/users',
2829
].some((p) => page.path.includes(p)),

e2e/solid-start/basic/src/routeTree.gen.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { Route as MultiCookieRedirectIndexRouteImport } from './routes/multi-coo
3232
import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
3333
import { Route as SpecialCharsChar45824Char54620Char48124Char44397RouteImport } from './routes/specialChars/대한민국'
3434
import { Route as SpecialCharsSearchRouteImport } from './routes/specialChars/search'
35+
import { Route as SpecialCharsHashRouteImport } from './routes/specialChars/hash'
3536
import { Route as SpecialCharsParamRouteImport } from './routes/specialChars/$param'
3637
import { Route as SearchParamsLoaderThrowsRedirectRouteImport } from './routes/search-params/loader-throws-redirect'
3738
import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/default'
@@ -181,6 +182,11 @@ const SpecialCharsSearchRoute = SpecialCharsSearchRouteImport.update({
181182
path: '/search',
182183
getParentRoute: () => SpecialCharsRouteRoute,
183184
} as any)
185+
const SpecialCharsHashRoute = SpecialCharsHashRouteImport.update({
186+
id: '/hash',
187+
path: '/hash',
188+
getParentRoute: () => SpecialCharsRouteRoute,
189+
} as any)
184190
const SpecialCharsParamRoute = SpecialCharsParamRouteImport.update({
185191
id: '/$param',
186192
path: '/$param',
@@ -382,6 +388,7 @@ export interface FileRoutesByFullPath {
382388
'/search-params/default': typeof SearchParamsDefaultRoute
383389
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
384390
'/specialChars/$param': typeof SpecialCharsParamRoute
391+
'/specialChars/hash': typeof SpecialCharsHashRoute
385392
'/specialChars/search': typeof SpecialCharsSearchRoute
386393
'/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
387394
'/users/$userId': typeof UsersUserIdRoute
@@ -431,6 +438,7 @@ export interface FileRoutesByTo {
431438
'/search-params/default': typeof SearchParamsDefaultRoute
432439
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
433440
'/specialChars/$param': typeof SpecialCharsParamRoute
441+
'/specialChars/hash': typeof SpecialCharsHashRoute
434442
'/specialChars/search': typeof SpecialCharsSearchRoute
435443
'/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
436444
'/users/$userId': typeof UsersUserIdRoute
@@ -489,6 +497,7 @@ export interface FileRoutesById {
489497
'/search-params/default': typeof SearchParamsDefaultRoute
490498
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
491499
'/specialChars/$param': typeof SpecialCharsParamRoute
500+
'/specialChars/hash': typeof SpecialCharsHashRoute
492501
'/specialChars/search': typeof SpecialCharsSearchRoute
493502
'/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
494503
'/users/$userId': typeof UsersUserIdRoute
@@ -546,6 +555,7 @@ export interface FileRouteTypes {
546555
| '/search-params/default'
547556
| '/search-params/loader-throws-redirect'
548557
| '/specialChars/$param'
558+
| '/specialChars/hash'
549559
| '/specialChars/search'
550560
| '/specialChars/대한민국'
551561
| '/users/$userId'
@@ -595,6 +605,7 @@ export interface FileRouteTypes {
595605
| '/search-params/default'
596606
| '/search-params/loader-throws-redirect'
597607
| '/specialChars/$param'
608+
| '/specialChars/hash'
598609
| '/specialChars/search'
599610
| '/specialChars/대한민국'
600611
| '/users/$userId'
@@ -652,6 +663,7 @@ export interface FileRouteTypes {
652663
| '/search-params/default'
653664
| '/search-params/loader-throws-redirect'
654665
| '/specialChars/$param'
666+
| '/specialChars/hash'
655667
| '/specialChars/search'
656668
| '/specialChars/대한민국'
657669
| '/users/$userId'
@@ -866,6 +878,13 @@ declare module '@tanstack/solid-router' {
866878
preLoaderRoute: typeof SpecialCharsSearchRouteImport
867879
parentRoute: typeof SpecialCharsRouteRoute
868880
}
881+
'/specialChars/hash': {
882+
id: '/specialChars/hash'
883+
path: '/hash'
884+
fullPath: '/specialChars/hash'
885+
preLoaderRoute: typeof SpecialCharsHashRouteImport
886+
parentRoute: typeof SpecialCharsRouteRoute
887+
}
869888
'/specialChars/$param': {
870889
id: '/specialChars/$param'
871890
path: '/$param'
@@ -1143,13 +1162,15 @@ const SpecialCharsMalformedRouteRouteWithChildren =
11431162
interface SpecialCharsRouteRouteChildren {
11441163
SpecialCharsMalformedRouteRoute: typeof SpecialCharsMalformedRouteRouteWithChildren
11451164
SpecialCharsParamRoute: typeof SpecialCharsParamRoute
1165+
SpecialCharsHashRoute: typeof SpecialCharsHashRoute
11461166
SpecialCharsSearchRoute: typeof SpecialCharsSearchRoute
11471167
SpecialCharsChar45824Char54620Char48124Char44397Route: typeof SpecialCharsChar45824Char54620Char48124Char44397Route
11481168
}
11491169

11501170
const SpecialCharsRouteRouteChildren: SpecialCharsRouteRouteChildren = {
11511171
SpecialCharsMalformedRouteRoute: SpecialCharsMalformedRouteRouteWithChildren,
11521172
SpecialCharsParamRoute: SpecialCharsParamRoute,
1173+
SpecialCharsHashRoute: SpecialCharsHashRoute,
11531174
SpecialCharsSearchRoute: SpecialCharsSearchRoute,
11541175
SpecialCharsChar45824Char54620Char48124Char44397Route:
11551176
SpecialCharsChar45824Char54620Char48124Char44397Route,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { createFileRoute, useLocation } from '@tanstack/solid-router'
2+
import { createEffect, createSignal } from 'solid-js'
3+
4+
export const Route = createFileRoute('/specialChars/hash')({
5+
component: RouteComponent,
6+
})
7+
8+
function RouteComponent() {
9+
const location = useLocation()
10+
const [getHash, setHash] = createSignal('')
11+
12+
createEffect(() => {
13+
setHash(location().hash)
14+
})
15+
16+
return (
17+
<div data-testid="special-hash-heading">
18+
Hello "/specialChars/hash"!
19+
<span data-testid="special-hash">{getHash()}</span>
20+
</div>
21+
)
22+
}

e2e/solid-start/basic/src/routes/specialChars/route.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,19 @@ function RouteComponent() {
3737
>
3838
Unicode search param
3939
</Link>{' '}
40+
<Link
41+
to="/specialChars/hash"
42+
activeOptions={{
43+
includeHash: true,
44+
}}
45+
activeProps={{
46+
class: 'font-bold',
47+
}}
48+
hash={'대|'}
49+
data-testid="special-hash-link"
50+
>
51+
Unicode Hash
52+
</Link>{' '}
4053
<Link
4154
to="/specialChars/malformed"
4255
activeProps={{

0 commit comments

Comments
 (0)