[PR #755] [MERGED] feat: track previousNextUrl for intercepted App Router entries #819

Closed
opened 2026-05-06 13:10:16 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/755
Author: @NathanDrake2406
Created: 4/2/2026
Status: Merged
Merged: 4/12/2026
Merged by: @james-elicx

Base: mainHead: feat/layout-persistence-pr-5-previous-next-url


📝 Commits (7)

  • c34d0a5 docs: refresh stale app browser entry comments
  • 252f3d0 feat: track previousNextUrl for intercepted App Router entries
  • 64342b1 refactor: bind getBrowserRouterState() once in refresh case
  • 8974afb refactor: drop dead history-state interception-context read path
  • 873cb62 test(e2e): cover back/forward restoration of intercepted modal view
  • fae429f docs: reword stale 'belongs to PR 5' comments
  • 96dad49 fix: distinguish explicit null from undefined in previousNextUrl fallback

📊 Changes

9 files changed (+323 additions, -75 deletions)

View changed files

📝 packages/vinext/src/global.d.ts (+1 -0)
📝 packages/vinext/src/server/app-browser-entry.ts (+84 -58)
📝 packages/vinext/src/server/app-browser-state.ts (+65 -0)
📝 packages/vinext/src/shims/navigation.ts (+6 -12)
📝 tests/app-browser-entry.test.ts (+102 -0)
📝 tests/e2e/app-router/advanced.spec.ts (+48 -0)
📝 tests/fixtures/app-basic/app/feed/@modal/(...)photos/[id]/page.tsx (+2 -0)
tests/fixtures/app-basic/app/feed/@modal/(...)photos/[id]/refresh-button.tsx (+13 -0)
📝 tests/prefetch-cache.test.ts (+2 -5)

📄 Description

Summary

Part of #726 (PR 5). This builds on PR 4's interception-aware payload IDs and cache keys by tracking previousNextUrl in the App Router browser state. Intercepted navigations now remember the URL they came from, so refresh/back-forward behavior can distinguish soft interception from direct loads.

The practical effect is that /feed -> /photos/42 modal navigations now behave correctly across the three important cases: browser reload breaks out to the full page, back/forward restores the intercepted modal view from history state, and router.refresh() keeps the intercepted modal instead of silently switching to the direct page tree.

What changed

  • Browser router stateapp-browser-state now carries previousNextUrl alongside elements, interceptionContext, routeId, and rootLayoutTreePath, with helpers for persisting and reading it from history state
  • History state persistence — App Router entries now store __vinext_previousNextUrl instead of the old interception-context field, preserving the URL active before a soft navigation while keeping the existing scroll restoration state intact
  • Navigation request context — soft navigate records the current URL as previousNextUrl, traverse reconstructs interception context from event.state, and refresh reconstructs it from committed router state so refresh preserves interception without changing hard-load behavior
  • Hard-load normalization — boot clears any carried-over previousNextUrl from the active history entry so full document reloads still break out of interception even though browsers preserve history.state across reloads
  • Redirect recursion — redirected RSC navigations carry the original previousNextUrl forward so replacement navigations do not lose the source-route provenance
  • Fixtures and tests — adds unit coverage for previousNextUrl state/history helpers plus E2E coverage for reload, back/forward restoration, and router.refresh() on intercepted routes. The modal fixture now exposes a small router.refresh() button to exercise the public API
  • Comment cleanup — refreshes a few stale app-browser-entry comments so they match the current dispatch/control-flow names after the refactor

What this does NOT do

Server action interception-request parity is still intentionally deferred. This PR preserves the browser-side previousNextUrl state during server-action merges, but it does not add interception headers to the action POST itself.

Test plan

  • vp test run tests/app-browser-entry.test.ts
  • vp test run tests/app-router.test.ts
  • vp run test:e2e tests/e2e/app-router/advanced.spec.ts --grep 'hard reload after intercepted navigation renders the full page|back then forward restores intercepted modal view|router.refresh preserves intercepted modal view'
  • git commit pre-commit hook (vp check --fix)

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/755 **Author:** [@NathanDrake2406](https://github.com/NathanDrake2406) **Created:** 4/2/2026 **Status:** ✅ Merged **Merged:** 4/12/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `feat/layout-persistence-pr-5-previous-next-url` --- ### 📝 Commits (7) - [`c34d0a5`](https://github.com/cloudflare/vinext/commit/c34d0a52b569ed25703fa400d10836fbaa46acf5) docs: refresh stale app browser entry comments - [`252f3d0`](https://github.com/cloudflare/vinext/commit/252f3d0dad1344e5669a7f1bb09fe023d35cd66a) feat: track previousNextUrl for intercepted App Router entries - [`64342b1`](https://github.com/cloudflare/vinext/commit/64342b1d4b021cd4d986a5ed24c9ff4d4af3e036) refactor: bind getBrowserRouterState() once in refresh case - [`8974afb`](https://github.com/cloudflare/vinext/commit/8974afb300af3b0659df4d5fa28d8e769d86116b) refactor: drop dead history-state interception-context read path - [`873cb62`](https://github.com/cloudflare/vinext/commit/873cb626e02daa075aee0dffcf5efac00a4a99a2) test(e2e): cover back/forward restoration of intercepted modal view - [`fae429f`](https://github.com/cloudflare/vinext/commit/fae429fbdabf26dee35b399af18253d08875d874) docs: reword stale 'belongs to PR 5' comments - [`96dad49`](https://github.com/cloudflare/vinext/commit/96dad49e1bdebacc43003c059c246bfa0aff813e) fix: distinguish explicit null from undefined in previousNextUrl fallback ### 📊 Changes **9 files changed** (+323 additions, -75 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/global.d.ts` (+1 -0) 📝 `packages/vinext/src/server/app-browser-entry.ts` (+84 -58) 📝 `packages/vinext/src/server/app-browser-state.ts` (+65 -0) 📝 `packages/vinext/src/shims/navigation.ts` (+6 -12) 📝 `tests/app-browser-entry.test.ts` (+102 -0) 📝 `tests/e2e/app-router/advanced.spec.ts` (+48 -0) 📝 `tests/fixtures/app-basic/app/feed/@modal/(...)photos/[id]/page.tsx` (+2 -0) ➕ `tests/fixtures/app-basic/app/feed/@modal/(...)photos/[id]/refresh-button.tsx` (+13 -0) 📝 `tests/prefetch-cache.test.ts` (+2 -5) </details> ### 📄 Description ## Summary Part of #726 (PR 5). This builds on PR 4's interception-aware payload IDs and cache keys by tracking `previousNextUrl` in the App Router browser state. Intercepted navigations now remember the URL they came from, so refresh/back-forward behavior can distinguish soft interception from direct loads. The practical effect is that `/feed -> /photos/42` modal navigations now behave correctly across the three important cases: browser reload breaks out to the full page, back/forward restores the intercepted modal view from history state, and `router.refresh()` keeps the intercepted modal instead of silently switching to the direct page tree. ### What changed - **Browser router state** — `app-browser-state` now carries `previousNextUrl` alongside `elements`, `interceptionContext`, `routeId`, and `rootLayoutTreePath`, with helpers for persisting and reading it from history state - **History state persistence** — App Router entries now store `__vinext_previousNextUrl` instead of the old interception-context field, preserving the URL active before a soft navigation while keeping the existing scroll restoration state intact - **Navigation request context** — soft `navigate` records the current URL as `previousNextUrl`, `traverse` reconstructs interception context from `event.state`, and `refresh` reconstructs it from committed router state so refresh preserves interception without changing hard-load behavior - **Hard-load normalization** — boot clears any carried-over `previousNextUrl` from the active history entry so full document reloads still break out of interception even though browsers preserve `history.state` across reloads - **Redirect recursion** — redirected RSC navigations carry the original `previousNextUrl` forward so replacement navigations do not lose the source-route provenance - **Fixtures and tests** — adds unit coverage for `previousNextUrl` state/history helpers plus E2E coverage for reload, back/forward restoration, and `router.refresh()` on intercepted routes. The modal fixture now exposes a small `router.refresh()` button to exercise the public API - **Comment cleanup** — refreshes a few stale `app-browser-entry` comments so they match the current dispatch/control-flow names after the refactor ### What this does NOT do Server action interception-request parity is still intentionally deferred. This PR preserves the browser-side `previousNextUrl` state during server-action merges, but it does not add interception headers to the action POST itself. ## Test plan - [x] `vp test run tests/app-browser-entry.test.ts` - [x] `vp test run tests/app-router.test.ts` - [x] `vp run test:e2e tests/e2e/app-router/advanced.spec.ts --grep 'hard reload after intercepted navigation renders the full page|back then forward restores intercepted modal view|router.refresh preserves intercepted modal view'` - [x] `git commit` pre-commit hook (`vp check --fix`) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:10:16 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#819
No description provided.