Skip to content

Conversation

@nlynzaad
Copy link
Contributor

@nlynzaad nlynzaad commented Jan 18, 2026

This PR addresses some issues when matching url hashes.

As part of #6389 it was noticed that when using encoded characters in hashes these would not be matched correctly. This is addressed by making sure the encoded hash fragment is decoded before matching for Link active properties and others.

It was also noticed that in the case of solid hash matching for Link active properties was not working when doing SSR. This is due to how hashes is handled between server and client and compounded with how Solid's reactivity works. When a request is sent to the server during SSR the hash is removed. When received on client the hash is then processed but a rerender does not occur and hence not applying the active props. This is addressed by ensuring that the hash check is only done on client and not accounted for on server.

Tests have been added for the above

Summary by CodeRabbit

  • New Features

    • Added a new route to exercise URL hash handling (non-ASCII and special chars) and hash-aware navigation links with active styling.
  • Bug Fixes

    • Decode hash fragments when parsing locations.
    • Avoid evaluating hash-based active state during server-side rendering.
  • Tests

    • Added end-to-end tests covering direct and router navigation with encoded URL hashes.
  • Chores

    • Excluded the new hash route from prerendering.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 18, 2026

📝 Walkthrough

Walkthrough

Adds /specialChars/hash routes across React, Solid, and Vue examples, decodes URL hash in core parseLocation, guards hash-based active comparisons during SSR for Solid links, updates tests and prerender exclusions, and documents client-only hash fragment caveats.

Changes

Cohort / File(s) Summary
React — route tree, component, navigation
e2e/react-start/basic/src/routeTree.gen.ts, e2e/react-start/basic/src/routes/specialChars/hash.tsx, e2e/react-start/basic/src/routes/specialChars/route.tsx
Adds /specialChars/hash route and component that renders location.hash; updates generated route tree and adds a navigation Link with includeHash: true and Unicode hash.
React — config & tests
e2e/react-start/basic/vite.config.ts, e2e/react-start/basic/tests/special-characters.spec.ts
Adds /specialChars/hash to prerender exclusion; new tests for direct and router navigation validating encoded Unicode/hash behavior and link active styling.
Solid — route tree, component, navigation
e2e/solid-start/basic/src/routeTree.gen.ts, e2e/solid-start/basic/src/routes/specialChars/hash.tsx, e2e/solid-start/basic/src/routes/specialChars/route.tsx
Adds Solid /specialChars/hash route and component with reactive hash sync; updates generated route tree and adds navigation Link using includeHash: true.
Solid — config & tests
e2e/solid-start/basic/vite.config.ts, e2e/solid-start/basic/tests/special-characters.spec.ts
Adds prerender exclusion and tests mirroring React for hash handling and link active state.
Vue — route tree, component, navigation
e2e/vue-start/basic/src/routeTree.gen.ts, e2e/vue-start/basic/src/routes/specialChars/hash.tsx, e2e/vue-start/basic/src/routes/specialChars/route.tsx
Adds Vue /specialChars/hash route and component that shows location.hash; updates generated route tree and navigation link.
Vue — config & tests
e2e/vue-start/basic/vite.config.ts, e2e/vue-start/basic/tests/special-characters.spec.ts
Adds prerender exclusion and tests for direct and router navigation verifying encoded hash and link active state.
Core router & link behavior
packages/router-core/src/router.ts, packages/solid-router/src/link.tsx
parseLocation now decodes the hash via decodePath; Solid link hash comparisons are skipped on server (guard added) to avoid SSR-only hash evaluation.
Docs
docs/router/framework/react/guide/navigation.md
Documents that the URL hash is client-only and warns about SSR hydration mismatches when rendering or gating UI on hash.

Sequence Diagram(s)

sequenceDiagram
  participant Browser as Browser (Client)
  participant RouterCore as Router Core
  participant Server as Server (SSR)
  participant SolidLink as Solid Link Component

  Browser->>RouterCore: Request URL (includes raw `#fragment`)
  RouterCore->>RouterCore: parseLocation -> decodePath(hash)
  RouterCore-->>Browser: Location object with decoded hash

  Note over Server,SolidLink: During SSR
  Server->>SolidLink: render
  SolidLink-->>Server: guard -> skip hash comparison (router.isServer)
  Server-->>Browser: HTML without hash-dependent active state

  Browser->>SolidLink: Hydrate / Navigate
  SolidLink->>RouterCore: read location.hash (client)
  SolidLink-->>Browser: compute active state using decoded hash
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • schiller-manuel
  • birkskyum

Poem

🐰 I nibble bytes and hop through hashes bright,
Unicode crumbs glowing in URL light,
Server skips the sniff, client reads the tune,
Links wake bold beneath the decoding moon. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix: encoded url hash fragment matching' directly aligns with the main changes: decoding hash fragments in parseLocation and adding hash comparison guards for SSR environments.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link

nx-cloud bot commented Jan 18, 2026

View your CI Pipeline Execution ↗ for commit 308cc5f

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 55s View ↗

☁️ Nx Cloud last updated this comment at 2026-01-19 14:12:53 UTC

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@e2e/solid-start/basic/vite.config.ts`:
- Line 28: The prerender exclusion is using the wrong path string
'/search-params/hash' so the intended route '/specialChars/hash' will still be
prerendered; update the exclusion entry in the vite config (where the prerender
exclude array contains '/search-params/hash') to the correct route
'/specialChars/hash' so the hash route is properly skipped during prerendering.
🧹 Nitpick comments (2)
e2e/solid-start/basic/src/routes/specialChars/hash.tsx (1)

8-21: Consider simplifying the reactive hash access.

The createEffect to sync location().hash to a signal adds indirection. Since location() already returns a reactive accessor, you could potentially access the hash directly in JSX:

function RouteComponent() {
  const location = useLocation()
  
  return (
    <div data-testid="special-hash-heading">
      Hello "/specialChars/hash"!
      <span data-testid="special-hash">{location().hash}</span>
    </div>
  )
}

However, if the effect pattern is intentional for SSR/client synchronization (as mentioned in the PR objectives about hash checks being client-only), the current implementation is acceptable.

e2e/solid-start/basic/tests/special-characters.spec.ts (1)

123-128: Inconsistent assertion pattern for class check.

This test uses evaluate() with classList.value and toContain(), while the router navigation test at lines 150-152 uses toHaveClass() directly. For consistency and better error messages, prefer toHaveClass():

♻️ Proposed fix for consistency
-      const el = await page
-        .getByTestId('special-hash-link')
-        .evaluate((e) => e.classList.value)
-
-      await expect(el).toContain('font-bold')
+      await expect(page.getByTestId('special-hash-link')).toHaveClass(
+        'font-bold',
+      )

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 18, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@6416

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/eslint-plugin-router@6416

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@6416

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/nitro-v2-vite-plugin@6416

@tanstack/react-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router@6416

@tanstack/react-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-devtools@6416

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-ssr-query@6416

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@6416

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@6416

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@6416

@tanstack/router-cli

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-cli@6416

@tanstack/router-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-core@6416

@tanstack/router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools@6416

@tanstack/router-devtools-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools-core@6416

@tanstack/router-generator

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-generator@6416

@tanstack/router-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-plugin@6416

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-ssr-query-core@6416

@tanstack/router-utils

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-utils@6416

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-vite-plugin@6416

@tanstack/solid-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router@6416

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-devtools@6416

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-ssr-query@6416

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@6416

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@6416

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@6416

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@6416

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-fn-stubs@6416

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@6416

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@6416

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-static-server-functions@6416

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@6416

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@6416

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@6416

@tanstack/vue-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-router@6416

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-router-devtools@6416

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-router-ssr-query@6416

@tanstack/vue-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start@6416

@tanstack/vue-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start-client@6416

@tanstack/vue-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start-server@6416

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@6416

commit: 308cc5f

@github-actions github-actions bot added the documentation Everything documentation related label Jan 18, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@docs/router/framework/react/guide/navigation.md`:
- Around line 293-301: The doc text contains inconsistent casing for "URL" —
locate the paragraph that begins with "When directly navigating to a URL with a
hash fragment..." in navigation.md and change "Url" (or any non-URL casing) to
the uppercase "URL" everywhere in that paragraph and the surrounding bullet
examples so casing is consistent; ensure the sentence and examples read "URL"
(e.g., "the browser does not send the fragment to the server as part of the
request URL") and update any other instances in the same section.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@nlynzaad
Copy link
Contributor Author

@birkskyum when using hash fragments in url's these aren't available server side (the browser does not send this to the server), so during SSR the hash value is blank, but the value is provided once we are client side.

However Solid does not treat this update in a reactive manner, i.e while the hash is updated, the location accessor does not notify subscribers to this change. This creates problems for cases when using the hash value i.e. Link active props, conditional rendering, etc.

I've added a hacky workaround in the attached Solid e2e for this and made a change to the Link component to exclude the hash when checking if the hash should be used in the active props when server side, which solves the problem for Link active props.

Ideally the location should update all the subscribers once the hash value is received, my knowledge of Solid is just too broken to comprehend where or what needs to change to do this the right way.

Do you have any recommendations on what needs to change in solid-router for this or is this the best way to deal with this update?

@nlynzaad nlynzaad requested a review from birkskyum January 19, 2026 00:18
@birkskyum
Copy link
Member

birkskyum commented Jan 19, 2026

@nlynzaad I think this workaround to Link looks reasonable for solid v1.

@nlynzaad nlynzaad merged commit 3da0227 into main Jan 19, 2026
7 of 8 checks passed
@nlynzaad nlynzaad deleted the hash-encoding branch January 19, 2026 14:39

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) {
Copy link

@theoludwig theoludwig Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fix should be backported to the React package as well.

As there is the same hydratation issue in React, even after upgrading to TanStack Start v1.153.1.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, could you create a reproducer in a new issue. i will look at it a bit later. We have tests for this specifically so would need to see what it is we missed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants