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

Open
#2,809 0 comments 0 reactions 0 assignees View on GitHub
nextjs-tracking
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.