Skip to content

test(app-router): cover cross-path hash push URL replacement - #2926

Closed
NathanDrake2406 wants to merge 1 commit into
cloudflare:mainfrom
NathanDrake2406:nathan/fix-2809-double-hash-nav
Closed

test(app-router): cover cross-path hash push URL replacement#2926
NathanDrake2406 wants to merge 1 commit into
cloudflare:mainfrom
NathanDrake2406:nathan/fix-2809-double-hash-nav

Conversation

@NathanDrake2406

Copy link
Copy Markdown
Contributor

Summary

  • port the Next.js hash-cross-path-push navigation regression as an App Router browser test
  • add the matching nextjs-compat fixture routes (start page + destination page)
  • cover both navigation entry points: <Link> and router.push

Closes #2809.

Context

vercel/next.js#93132 fixes a client-navigation bug where the URL hash is
appended twice (/abc#foo#bar). Its cause is specific to the Next.js segment
cache: createRouteCacheEntry stored one canonical URL per route entry, shared
across every hash target of that route, and a later same-route hash navigation
appended url.hash to it. The upstream fix stores a hashless canonical href.

Triage

The defect does not reproduce in vinext, and the reason is structural rather
than incidental.

  • vinext has no per-route canonical URL. rg 'canonicalUrl' packages/vinext/src
    matches only the Sass plugin.
  • Every navigation resolves its own target against the live location
    (toBrowserNavigationHref -> resolveRelativeHref, shims/url-utils.ts), and
    the committed history write uses that resolved target
    (AppBrowserHistoryController.commitNavigationHistory,
    createCanonicalBrowserHistoryHref). Nothing concatenates a new hash onto a
    stored href from an earlier navigation.
  • Reproduced the upstream scenario against main (<Link> clicks,
    router.push, and a search+hash variant): all landed on the correct single
    hash.

Existing coverage did not reach this case. hash-popstate-scroll.spec.ts and
hash-rsc-requests.browser.spec.ts only change hashes after a direct document
load of the route; the upstream regression requires the route entry to be
created by a cross-path client push that already carries a hash.

Since the behavior is correct, this follows the precedent of #2758: a
tracking issue resolved with focused regression coverage rather than a
production change.

Verification

  • vp check on the five new files: pass (format, lint, types)
  • pre-commit full check + knip: pass
  • PLAYWRIGHT_PROJECT=app-router npx playwright test hash-cross-path-push.spec.ts: 2/2 passed
  • repeated 5x (--repeat-each=5 --retries=0): 10/10 passed
  • neighbouring suites hash-popstate-scroll.spec.ts + router-autoscroll.spec.ts: 38/38 passed

Mutation check (the tests have real signal). Temporarily injected the
upstream failure mechanism into navigateClientSide's sameDocumentScroll
branch — committing location.pathname + location.search + location.hash + intent.hash instead of the resolved target — then rebuilt vinext. Both new
tests failed with exactly the reported malformed URL:

Expected: ".../hash-cross-path-push/destination#baz"
Received: ".../hash-cross-path-push/destination#foo#baz"

The mutation was reverted and vinext rebuilt before committing; the working
tree contains no production changes (git diff against packages/ is empty).

Scope

Test and fixture only — no runtime behavior changes, so no changeset. The
Pages Router hash lifecycle (shims/router.ts, hashChangeStart) is a separate
navigation path and is out of scope for this upstream port.

Port the Next.js `hash-cross-path-push` navigation regression: a cross-path
client push carrying a hash, followed by a same-pathname hash change, must
replace the hash rather than concatenate it (`/destination#foo#baz`).

vinext already behaves correctly because it resolves every navigation target
against the live location and writes that resolved href to history, instead of
keeping a per-route canonical URL shared across hash targets. Cover both
navigation entry points (`<Link>` and `router.push`) so the property stays
enforced.
@NathanDrake2406

Copy link
Copy Markdown
Contributor Author

@codex review

@pkg-pr-new

pkg-pr-new Bot commented Aug 15, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2926
npm i https://pkg.pr.new/create-vinext-app@2926
npm i https://pkg.pr.new/@vinext/types@2926
npm i https://pkg.pr.new/vinext@2926

commit: 8b7e91a

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Unknown error
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@NathanDrake2406

Copy link
Copy Markdown
Contributor Author

Closing: this turned out to be parity coverage for a defect vinext does not have, and it is not worth a PR on its own.

The triage summary is on #2809. In short, the Next.js bug is specific to its segment cache storing one canonical URL per route entry shared across every hash target; vinext resolves each navigation target against the live location and writes that resolved href to history, so the hash is replaced rather than concatenated. The upstream scenario was reproduced against main and behaved correctly.

No production changes were proposed here.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

App Router client navigation: strip hash from stored canonical URL to avoid double-hash (#foo#bar) on cross-hash same-route nav

1 participant