cloudflare / cloudflare/vinext
App Router client navigation: strip hash from stored canonical URL to avoid double-hash (`#foo#bar`) on cross-hash same-route nav
- Dominant language
- TypeScript
- Stars
- 8.8k
- Forks
- 406
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 120
Description
## Next.js Change
**Commit:** [`459617a`](https://github.com/vercel/next.js/commit/459617a12598de63c34cec4d5c267207c591d13f)
**PR:** [#93132](https://github.com/vercel/next.js/pull/93132)
## What changed
Fixes a client-navigation bug where the URL hash is appended twice, producing a malformed URL like `/abc#foo#bar`.
Repro (from the PR):
1. From `/`, navigate to `/abc#foo`
2. On `/abc#foo`, click a link to `/abc#bar`
3. The URL becomes `/abc#foo#bar` instead of `/abc#bar`
Fixes vercel/next.js#93126 and vercel/next.js#95551.
### Mechanism (from the diff)
The fix changes what gets stored in the segment cache for a freshly-navigated route. When creating the cache entry for an unknown route, the canonical URL is now stored **without** its hash:
```ts
// packages/next/src/client/components/segment-cache/navigation.ts
createRouteCacheEntry(
navigationSeed.routeTree,
metadataVaryPath,
couldBeIntercepted,
// Store a hashless canonical URL: the entry is shared across hashes, and
// a later same-route hash nav appends `url.hash` to it.
createHrefFromUrl(canonicalUrl, false), // <-- second arg `false` strips the hash
supportsPerSegmentPrefetching,
false
)
```
`createHrefFromUrl(url, includeHash = true)` normally includes the hash. Passing `false` stores a hashless canonical href. Because a single route cache entry is **shared across all hash targets** of the same route, a subsequent same-route hash navigation appends the new `url.hash` to the stored canonical href. When the stored href already carried a hash (`#foo`), the new hash (`#bar`) was appended on top of it, yielding `#foo#bar`.
Unlike the earlier fix attempt (vercel/next.js#93855, which sliced away an existing hash), this fix prevents the bad state at the source by never storing the hash in the shared entry.
## Impact on vinext
vinext implements App Router client-side navigation, including same-route hash navigation and canonical-URL tracking. If vinext stores a hash-bearing canonical href in a per-route cache entry and appends `url.hash` on a subsequent same-route hash navigation, it will reproduce the same `#foo#bar` doubling.
What to check/do:
1. **Store the canonical href without the hash** wherever a per-route navigation/cache entry is keyed or shared across hash targets. The hash is a per-navigation property, not a per-route one.
2. **Append the current navigation's hash at navigation time**, not at cache-store time, so `/abc#foo` -> `/abc#bar` replaces rather than concatenates.
3. **Port the regression test.** The Next.js test (`test/e2e/app-dir/navigation/app/hash-cross-path-push/`) navigates `/` -> `/abc#foo` -> `/abc#bar` (via `router.push`) and asserts the final URL is `/abc#bar`, not `/abc#foo#bar`. vinext should add an equivalent E2E case for cross-hash, same-path `router.push` navigation.
### Notes
- This is a general client-navigation correctness bug independent of Cache Components / Partial Prefetching. It applies to any vinext App Router app that uses hash links or `router.push` with a hash.
- The fix is in the navigation store, not a display-time slice, so make sure vinext fixes it at the point the canonical href is stored rather than post-processing the URL bar.
## Related
- (none — no existing hash-navigation tracking issue)
Contributor guide
Research direction
Start by locating vinext's App Router client-navigation and per-route cache implementation, then compare it with packages/next/src/client/components/segment-cache/navigation.ts and createHrefFromUrl. Port the regression scenario from test/e2e/app-dir/navigation/app/hash-cross-path-push/, navigating / to /abc#foo to /abc#bar with router.push. Done means the final URL is /abc#bar rather than /abc#foo#bar.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- next.js, typescript
- Domain
- frontend, web-dev
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 62/100