npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@real-router/navigation-plugin

v0.6.0

Published

Navigation API integration plugin for browser URL synchronization

Downloads

1,517

Readme

@real-router/navigation-plugin

npm npm downloads bundle size License: MIT

Navigation API integration for Real-Router. Drop-in replacement for browser-plugin with route-level history access.

Installation

npm install @real-router/navigation-plugin

Peer dependency: @real-router/core

Quick Start

import { createRouter } from "@real-router/core";
import { navigationPluginFactory } from "@real-router/navigation-plugin";

const router = createRouter([
  { name: "home", path: "/" },
  { name: "users", path: "/users/:id" },
]);

router.usePlugin(navigationPluginFactory());
await router.start(); // path inferred from browser location

Why Navigation API?

The Navigation API (~89% browser support) gives you access to the full session history as structured data. Unlike the History API, you can inspect every entry, check what routes the user has visited, and traverse directly to a specific past entry.

// Not possible with browser-plugin:
router.peekBack(); // what's one step back?
router.hasVisited("checkout"); // did the user visit checkout?
router.getVisitedRoutes(); // all routes in this session
router.traverseToLast("users.list"); // jump back to the last users list

Options

router.usePlugin(
  navigationPluginFactory({
    base: "/app", // Base path prefix for all routes
    forceDeactivate: false, // Respect canDeactivate guards on back/forward (default)
  }),
);

| Option | Type | Default | Description | | ----------------- | --------- | ------- | ---------------------------------------------------------------------- | | base | string | "" | Base path for all routes (e.g., "/app" → URLs start with /app/...) | | forceDeactivate | boolean | false | If true, browser back/forward skip canDeactivate guards. Default false respects guards — matches browser-plugin. |

Router Extensions

Compatible extensions (same as browser-plugin)

| Method | Returns | Description | | -------------------------------------------- | -------------------- | ------------------------------------------------ | | buildUrl(name, params?) | string | Build full URL with base path | | matchUrl(url) | State \| undefined | Parse URL to router state | | replaceHistoryState(name, params?) | void | Update browser URL without triggering navigation |

router.buildUrl("users", { id: "123" });
// => "/app/users/123" (with base "/app")

router.matchUrl("/app/users/123");
// => { name: "users", params: { id: "123" }, path: "/users/123" }

// Update URL silently (no transition, no guards)
router.replaceHistoryState("users", { id: "456" });

Exclusive extensions (Navigation API only)

| Method | Returns | Description | | ------------------------------- | ----------------------------- | ----------------------------------------------- | | peekBack() | State \| undefined | State of the previous history entry | | peekForward() | State \| undefined | State of the next history entry | | hasVisited(routeName) | boolean | Whether any history entry matches the route | | getVisitedRoutes() | string[] | Unique route names across all history entries | | getRouteVisitCount(routeName) | number | How many history entries match the route | | traverseToLast(routeName) | Promise<State> | Navigate to the last history entry for a route | | canGoBack() | boolean | Whether there's a previous history entry | | canGoForward() | boolean | Whether there's a next history entry | | canGoBackTo(routeName) | boolean | Whether any previous entry matches the route |

peekBack / peekForward

// Show a preview of where back/forward would take the user
const prev = router.peekBack();
if (prev) {
  console.log(`Back goes to: ${prev.name}`);
}

const next = router.peekForward();
if (next) {
  console.log(`Forward goes to: ${next.name}`);
}

hasVisited / getVisitedRoutes / getRouteVisitCount

// Check if the user has been to a route in this session
if (router.hasVisited("checkout")) {
  showResumeCheckoutBanner();
}

// Get all routes visited in this session
const visited = router.getVisitedRoutes();
// => ["home", "users.list", "users.view", "checkout"]

// How many times did the user visit the product page?
const count = router.getRouteVisitCount("products.view");

traverseToLast

// Jump directly to the last time the user was on users.list
// (skips intermediate entries — no back/forward stepping)
await router.traverseToLast("users.list");

canGoBack / canGoForward / canGoBackTo

// Disable back button when there's nowhere to go
const backDisabled = !router.canGoBack();
const forwardDisabled = !router.canGoForward();

// Show "back to list" only if the user actually came from the list
if (router.canGoBackTo("users.list")) {
  showBackToListButton();
}

Navigation Metadata

Navigation metadata is available on state.context.navigation after each transition. The plugin writes it via the claim-based State Context API, and it is frozen (Object.freeze) for mutation protection.

// In subscribe callbacks
router.subscribe((state) => {
  const meta = state.context.navigation;
  console.log(meta?.navigationType); // "push" | "replace" | "traverse" | "reload"
  console.log(meta?.userInitiated);  // true if user clicked back/forward/link
  console.log(meta?.direction);      // "forward" | "back" | "unknown"
  console.log(meta?.sourceElement);  // the DOM element that initiated the nav, or null
  console.log(meta?.info);           // data passed via navigation.navigate({ info })
});

In guards during browser-initiated navigation, meta is available on toState.context.navigation (written in onTransitionStart):

import { getLifecycleApi } from "@real-router/core/api";

const lifecycle = getLifecycleApi(router);
lifecycle.addActivateGuard("checkout", () => (toState) => {
  const meta = toState.context.navigation;
  if (meta?.userInitiated) {
    // user clicked back/forward or a link
  }
  return true;
});

In framework components, access via the route's context:

// React example
const { route } = useRoute();
const meta = route.context.navigation;

NavigationMeta

| Field | Type | Description | | ----------------- | -------------------------------------------------- | ------------------------------------------------------ | | navigationType | "push" \| "replace" \| "traverse" \| "reload" | Type of navigation | | userInitiated | boolean | Whether the user clicked back/forward/link | | direction | "forward" \| "back" \| "unknown" | Direction in the history stack | | sourceElement | Element \| null | DOM element that initiated the navigation, or null | | info | unknown | Ephemeral data from navigation.navigate({ info }) |

NavigationDirection

type NavigationDirection = "forward" | "back" | "unknown";

Exported from the package for use in type annotations.

buildUrl vs buildPath

router.buildPath("users", { id: 1 }); // "/users/1"       — core, no base
router.buildUrl("users", { id: 1 }); // "/app/users/1"   — plugin, with base

replaceHistoryState vs navigate({ replace: true })

router.replaceHistoryState(name, params); // URL only, no transition
router.navigate(name, params, { replace: true }); // Full transition + URL update

Feature Detection

Use navigationPluginFactory when the Navigation API is available, fall back to browserPluginFactory otherwise:

import { browserPluginFactory } from "@real-router/browser-plugin";
import { navigationPluginFactory } from "@real-router/navigation-plugin";

const plugin =
  "navigation" in globalThis
    ? navigationPluginFactory({ base })
    : browserPluginFactory({ base });

router.usePlugin(plugin);

Form Protection

canDeactivate guards run on browser back/forward by default — no extra configuration needed:

router.usePlugin(navigationPluginFactory());

import { getLifecycleApi } from "@real-router/core/api";

const lifecycle = getLifecycleApi(router);
lifecycle.addDeactivateGuard(
  "checkout",
  (router, getDep) => (toState, fromState) => {
    return !hasUnsavedChanges(); // false blocks back/forward
  },
);

SSR Support

The plugin is SSR-safe. In a non-browser environment it falls back to no-ops via createNavigationFallbackBrowser:

// Server-side — no errors, methods return safe defaults
router.usePlugin(navigationPluginFactory());
router.buildUrl("home"); // returns path without base
router.matchUrl("/home"); // returns State — matchUrl is pure and works in SSR
router.matchUrl("/does-not-exist"); // returns undefined when no route matches

matchUrl is environment-agnostic — it delegates to api.matchPath() over the route tree. What the SSR fallback disables is the browser-side side effects: navigate, replaceState, traverseTo, addNavigateListener all become no-ops (with a one-time warn). buildUrl / matchUrl remain fully functional.

Documentation

Full documentation: Wiki — navigation-plugin

Related Packages

| Package | Description | | ---------------------------------------------------------------------------------------- | ---------------------------------------------- | | @real-router/core | Core router (required peer dependency) | | @real-router/browser-plugin | History API fallback (broader browser support) | | @real-router/hash-plugin | Hash-based routing (#/path) | | @real-router/react | React integration | | @real-router/logger-plugin | Development logging |

Contributing

See contributing guidelines for development setup and PR process.

License

MIT © Oleg Ivanov