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/ssr-utils

v0.3.0

Published

Router-level SSR/SSG/hydration helpers for Real-Router

Readme

@real-router/ssr-utils

npm npm downloads bundle size License: MIT

Router-level SSR/SSG/hydration helpers for Real-Router. Serialize state for transport, hydrate on the client, enumerate static paths, isolate per-request router clones.

Installation

npm install @real-router/ssr-utils

Requires @real-router/core as a peer dependency.

Quick Start

// Server
import { serializeRouterState } from "@real-router/ssr-utils";

const state = await router.start(req.url);
const html = `<script>window.__SSR_STATE__=${serializeRouterState(state)}</script>`;

// Client
import { hydrateRouter } from "@real-router/ssr-utils";

await hydrateRouter(router, window.__SSR_STATE__);

API

| Function | Description | | ------------------------------------------ | ------------------------------------------------------------------ | | serializeState(data, opts?) | XSS-safe JSON serialization for embedding in HTML <script> tags | | serializeRouterState(state, opts?) | XSS-safe State serializer — strips transition, keeps context | | hydrateRouter(router, source, opts?) | Hydrate a fresh router from server-serialized state | | getHydrationState(router) | The state an in-flight hydrateRouter call deposited, or null | | getStaticPaths(router, entries?) | Enumerate leaf routes and build URLs for SSG pre-rendering | | createRequestScope(request, base, deps?) | Per-request SSR isolation via a cloned router |

serializeRouterState(state, options?)

const json = serializeRouterState(state);

// Strip a non-JSON-serializable plugin namespace (e.g. an RSC ReactNode tree)
const json = serializeRouterState(state, { excludeContext: ["rsc"] });

// Non-JSON types (Date / Map / Set / RegExp / BigInt) via devalue
import * as devalue from "devalue";
const json = serializeRouterState(state, { serialize: devalue.stringify });

hydrateRouter(router, source, options?)

const router = createAppRouter();
router.usePlugin(browserPluginFactory());
await hydrateRouter(router, window.__SSR_STATE__);

// Pair with a custom serializer
await hydrateRouter(router, window.__SSR_STATE__, {
  deserialize: devalue.parse,
});

SSR loader plugins (@real-router/ssr-data-plugin, @real-router/rsc-server-plugin) automatically skip their post-hydration re-fetch when the server-resolved value is already present in the hydrated state — no extra wiring needed.

getHydrationState(router)

For plugin authors: returns what the in-flight hydrateRouter call deposited for router, or null outside one. Read it from a start interceptor — the value is restored when hydrateRouter's start() settles, so later starts read null. There is no way to write it: only hydrateRouter does.

getPluginApi(router).addInterceptor("start", async (next, path) => {
  const state = await next(path);
  const hydrated = getHydrationState(router); // SerializedRouterState | null

  if (hydrated?.name === state.name) {
    // reuse hydrated.context instead of loading again
  }

  return state;
});

⚠ The plugin and hydrateRouter must resolve the same copy of @real-router/ssr-utils — two copies hold two scratchpads, and the read returns null without an error.

getStaticPaths(router, entries?)

const paths = await getStaticPaths(router);
// ["/", "/about", "/users/1", "/users/2", ...]

// Per-route entry sets for dynamic segments. An entry names its CHANNELS —
// `params` for path slots, `search` for `?`-declared query names, both optional.
const paths = await getStaticPaths(router, {
  "users.profile": async () => [
    { params: { id: "1" } },
    { params: { id: "2" } },
  ],
  // `/list?sort&page` — the query channel varies the page
  list: async () => [
    { search: { sort: "asc", page: "1" } },
    { search: { sort: "desc", page: "1" } },
  ],
  // `/doc/:id?rev` — both channels at once
  doc: async () => [{ params: { id: "a" }, search: { rev: "1" } }],
});

A key that cannot reach the URL throws rather than silently collapsing pages: a name the route declares with ? handed in params, or one it declares nowhere that the active queryParamsMode will not print, would make every entry differing only in it generate the same file (#1580).

A leaf that declares forwardTo throws too, unless the manifest also carries its target. A <Link> renders where the click lands, so the href names the target's URL — an entry supplied for the source alone writes a file nobody visits and leaves the one they do visit missing (#2256). Enumerate the target, or drop the source from entries and let the link resolve at runtime.

createRequestScope(request, base, deps?)

export async function render(url: string, req: IncomingMessage) {
  const scope = createRequestScope(req, baseRouter, { currentUser });
  try {
    scope.router.usePlugin(ssrDataPluginFactory(loaders));
    return await renderShell(scope.router, url);
  } finally {
    await scope.dispose();
  }
}

// `await using` (Node 24+, Bun, Deno, modern browsers)
export async function render(url: string, req: IncomingMessage) {
  await using scope = createRequestScope(req, baseRouter, { currentUser });
  return await renderShell(scope.router, url);
}

Binds an AbortSignal to the request lifetime (Node "close" event / Web request.signal), injected into the clone's dependencies under abortSignal — loaders read getDep("abortSignal") for cooperative cancellation.

Documentation

Full documentation: Wiki — ssr-utils

Related Packages

| Package | Description | | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | | @real-router/core | Core router | | @real-router/ssr-data-plugin | Per-route data loading — composes with the hydration scratchpad | | @real-router/rsc-server-plugin | Per-route ReactNode (RSC) loading — same composition |

Contributing

See contributing guidelines for development setup and PR process.

License

MIT © Oleg Ivanov