Pages Router: do not return the `__appRouter` prefetch marker as route info on shallow navigation (#98187)
### What?
`Router.prefetch()` in the Pages Router evaluates the client router
filter (`_bfl`) against the `as` path, but stored the resulting `{
__appRouter: true }` marker in `router.components` under the `href`
pathname. `Router.change()` looked the marker up with the `href`
pathname as well. This PR keys the marker by the normalized `as`
pathname and effective locale, preserves existing cache entries, and
checks for markers before and after route resolution.
### Why?
`href` and `as` are the same URL for most links, so the mismatch was
invisible. They differ for the "route as modal" pattern
(`examples/with-route-as-modal`): `href` stays on the current route
(`router.pathname` + query) and `as` shows the pretty URL. When the
filter matches `as`, including a false positive, the marker could
replace the cached route info of the page the user is currently on:
- On a static route (`/`), the `change()` guard fired for later shallow
navigation and hard-navigated.
- On a dynamic route, the marker was stored under the pattern
(`/players/[name]`) but looked up with the concrete path
(`/players/alice`). The guard could miss it, and shallow navigation
could render without page props, causing missing content or an
application error.
A prefetch must also preserve already-loaded route info when its key
matches the current route. This can happen when the page was reached
through a rewrite and its canonical URL is prefetched. Hash-only
navigation renders that cached entry directly, so preserving it lets the
normal client-side hash update retain the page props and update
`router.asPath`.
Fixes #98180
### How?
- `getAppRouterMarkerKey(router, as, locale)` parses the pathname,
normalizes the trailing slash, and preserves the effective locale. It
expects a path without `basePath` and does not strip that prefix again.
An explicit locale prefix takes precedence over the supplied locale.
- The helper returns `null` for non-local URLs, leaving their existing
navigation handling unchanged.
- `prefetch()` stores the marker under that key only when the
corresponding cache entry is absent. It does not replace loaded route
info.
- `change()` checks both the normalized `as` key and the `href` route.
It checks the resolved route again after config rewrites, before
`getRouteInfo()` reads the cache.
- Non-shallow navigations continue to consult the client router filter
directly. Hash-only navigation retains its normal client-side path.
The complementary change in #98650 (adopts #98187) adds defensive checks
when `getRouteInfo()` reads marker entries.
### Tests
The e2e suites run in production (`next start`) and deploy modes.
`router.prefetch()` is a no-op in development.
`test/e2e/app-dir/pages-prefetch-as-app-route` covers:
- Hard navigation to an App Router destination after prefetching a link
whose `href` and `as` differ.
- Shallow navigation on static and dynamic Pages Router routes without a
reload or lost server-provided props.
- Hard navigation when the `href` route holds a marker and `as` differs,
or a config rewrite resolves to a marked route.
- Client-side hash navigation after an awaited prefetch of the current
route's canonical URL, with preserved props and an updated
`router.asPath`.
`test/e2e/app-dir/pages-prefetch-as-app-route-base-path` covers an
internal `/docs` route with `basePath: '/docs'`, including navigation to
`/docs/docs` and an unaffected shallow update on the index page.
`test/e2e/app-dir/pages-prefetch-as-app-route-i18n` covers a French-only
redirect prefetch without forcing an unrelated English navigation to
reload. Marker-dependent tests wait for the specific key they need,
rather than any marker.
Runtime behaviour was also verified against the reproduction in
https://github.com/Stanzilla/next-pages-router-prefetch-bloom-filter-repro
by patching the compiled `router.js`. The e2e suite was not run locally.
Disclosure: this change and its description were prepared with AI
assistance (Claude Code) on behalf of the author.