Appearance
Navigation policy — one back model app-wide (#1016)
The app previously mixed three unrelated "back" mechanisms — hardcoded canonical hrefs posing as back links, component-state steppers invisible to browser history, and undocumented goto() push/replace semantics — making back behavior unpredictable on every screen. This is the single policy all navigation follows, with the shared helpers in src/lib/nav.ts.
The back rule
An in-app "back" affordance behaves like history.back() whenever in-app history exists, and falls back to its canonical parent URL only on a fresh entry / deep link. Returning to the previous view preserves everything that view held in its URL — filters, pagination, search, tab state — because it is literally the previous history entry, not a reconstructed canonical page.
Use it via:
backOr(fallback)— imperative form (buttons, programmatic back).backAction(fallback)— click handler for<a href={fallback}>back links: plain left-clicks get the history-aware behavior; modified clicks (new tab) and JS-off keep native anchor semantics on the canonical href.
The tracker is fed by the root layout's afterNavigate — no per-page wiring.
Breadcrumbs are not back links. A breadcrumb (DetailHeader) names the canonical hierarchy and always navigates canonically — that is its contract. Anything labeled back ("← Admin", "Back to pages") follows the back rule.
The push vs. replace rule for goto()
| Navigation | Mode | Why |
|---|---|---|
| New logical view: route change, opening a detail page, advancing a flow step | push (default) | Back must return to the previous view/step |
| View-state refinement of the same view: tab switch, filter/sort/param sync, compare column swap, results-view param clearing | replaceState: true | Back must step through views, never through intermediate query-param permutations |
Rule of thumb: if the user would call it "a different place", push; if they would call it "the same place, adjusted", replace.
Flows and browser back
A multi-step flow (the guided selection stepper) must make its steps visible to browser history via shallow routing (pushState + page.state), so the browser back button steps back one question and only exits the flow from its first step. In-app "previous" affordances inside a flow use the same history entries — the two back buttons never disagree.