Context
Follow-up debt from #2116. That PR fixed a masked-route false positive in the Pages Router popstate handler by excluding masked entries (state.url !== state.as) from the hash-only fast path. It did not make hash handling generally equivalent to Next.js.
Problem
In packages/vinext/src/shims/router.ts (handlePagesRouterPopState), the hash-only detection is:
const isHashOnly = !isMaskedRoute && browserUrl === routerRuntimeState.lastPathnameAndSearch;
where browserUrl = window.location.pathname + window.location.search. This keys off path + search equality, not a real hash delta. Next.js's onlyAHashChange (.nextjs-ref/packages/next/src/shared/lib/router/router.ts) compares the actual hash fragments of the previous vs next URL and returns false for an identical no-hash as.
The isMaskedRoute guard added in #2116 closes the one case that mattered (a masked entry whose visible URL is unchanged). But the underlying approximation remains: any back/forward where path+search match but the hash relationship differs from Next's notion of a hash-only change can still be classified differently than Next.
Proposed work
Port onlyAHashChange faithfully — compare the previous and next URL hash fragments rather than path+search equality — so the popstate hash-only fast path matches Next.js exactly. Drop the isMaskedRoute special-case once the real hash-delta comparison subsumes it.
Acceptance
- Hash-only detection compares actual hash fragments (prev vs next), matching Next's
onlyAHashChange.
- Existing masked-popstate regression tests in
tests/pages-router-i18n-sticky-locale.test.ts stay green.
- Add cases for: identical no-hash URL (not hash-only), add/remove/change hash on same path (hash-only), hash change combined with search change (not hash-only).
References
Context
Follow-up debt from #2116. That PR fixed a masked-route false positive in the Pages Router popstate handler by excluding masked entries (
state.url !== state.as) from the hash-only fast path. It did not make hash handling generally equivalent to Next.js.Problem
In
packages/vinext/src/shims/router.ts(handlePagesRouterPopState), the hash-only detection is:where
browserUrl = window.location.pathname + window.location.search. This keys off path + search equality, not a real hash delta. Next.js'sonlyAHashChange(.nextjs-ref/packages/next/src/shared/lib/router/router.ts) compares the actual hash fragments of the previous vs next URL and returnsfalsefor an identical no-hashas.The
isMaskedRouteguard added in #2116 closes the one case that mattered (a masked entry whose visible URL is unchanged). But the underlying approximation remains: any back/forward where path+search match but the hash relationship differs from Next's notion of a hash-only change can still be classified differently than Next.Proposed work
Port
onlyAHashChangefaithfully — compare the previous and next URL hash fragments rather than path+search equality — so the popstate hash-only fast path matches Next.js exactly. Drop theisMaskedRoutespecial-case once the real hash-delta comparison subsumes it.Acceptance
onlyAHashChange.tests/pages-router-i18n-sticky-locale.test.tsstay green.References
packages/vinext/src/shims/router.ts—handlePagesRouterPopState,isHashOnlyChangeonlyAHashChange:packages/next/src/shared/lib/router/router.ts