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

@zoijs/ssr

v0.3.0

Published

Render Zoijs components to an HTML string on the server — no DOM, no dependencies, same security as the client. For SSR and static prerendering.

Readme

@zoijs/ssr

Render Zoijs components to an HTML string — on the server, with no DOM and zero dependencies. Use it for server-side rendering (fast first paint, SEO) and for static prerendering (build your site to flat HTML). The same component code runs on the server and the client.

npm i @zoijs/ssr   # peer: @zoijs/core ^1.5.0

Render to a string

import { html, createState } from "@zoijs/core";
import { renderToString } from "@zoijs/ssr";

function App() {
  const name = createState("world");
  return html`<main><h1>Hello, ${() => name.get()}!</h1></main>`;
}

renderToString(App); // → '<main><h1>Hello, world!</h1></main>'

Each dynamic value is read once (its current value) and serialized. The output is your component's markup; drop it into an HTML shell and serve it:

import { renderToString } from "@zoijs/ssr";
import { App } from "./App.js";

function page() {
  return `<!doctype html>
<html>
  <head><meta charset="utf-8"><title>My app</title></head>
  <body>
    <div id="app">${renderToString(App)}</div>
    <script type="module" src="/client.js"></script>
  </body>
</html>`;
}

On the client, hydrate — adopt the server DOM in place instead of re-creating it. Render the markup with { hydratable: true } so the client can find and reuse it:

// server
const body = renderToString(App, { hydratable: true });
// client.js
import { hydrate } from "@zoijs/ssr";
import { App } from "./App.js";

hydrate(App, "#app"); // reuses the server elements; attaches events + reactivity

hydrate() runs the component and adopts the existing elements inside the target: they're reused exactly (same nodes, never re-created), and their event handlers and reactive attributes are attached in place. Each dynamic content region is re-rendered into that existing structure — and because the values match the server, there's no visible change and no flash. It returns an unmount(), like mount.

Not hydrating (pure static output / SSG)? Omit { hydratable: true } for clean, marker-free HTML, and take over with a plain mount(App, "#app").

Passing data to the client (serialize)

renderToString is synchronous, so a @zoijs/resource renders its loading state on the server. To skip the client-side refetch (and the flash), render with the data you already fetched, then hand it to the client with serialize — a JSON serializer that is safe to embed in a <script> (it escapes <, >, &, and the U+2028/U+2029 line terminators, so a </script> in your data can't break out):

import { renderToString, serialize } from "@zoijs/ssr";

// server: fetch, render with the value, embed it
const data = { user: await getUser() };
const body = renderToString(() => App(data), { hydratable: true });
res.end(`<div id="app">${body}</div>
  <script>window.__DATA__ = ${serialize(data)}</script>
  <script type="module" src="/client.js"></script>`);
// client: seed the resource — it starts settled and does NOT refetch
import { resource } from "@zoijs/resource";
const user = resource(() => fetch("/api/user").then((r) => r.json()),
                      { initial: window.__DATA__.user });

Because the server rendered with the same value the client seeds with, the markup matches and hydration is seamless. (Wiring this per-request automatically — loaders — is a separate, planned step; serialize + { initial } are the primitive.)

Static prerendering (SSG)

Because renderToString needs no DOM and no server, you can run it at build time to emit flat HTML for each route — no runtime needed at all:

import { writeFileSync } from "node:fs";
import { renderToString } from "@zoijs/ssr";
import { routes } from "./routes.js";

for (const [path, Page] of Object.entries(routes)) {
  writeFileSync(`dist${path}.html`, shell(renderToString(Page)));
}

Safe by the same rules as the client

@zoijs/ssr reuses the exact security predicates from @zoijs/core/server, so server output and client output make identical decisions — there's no second escaping implementation to drift:

  • Text is escaped (<, >, &), so interpolated data can't inject markup.
  • Attribute values are escaped (", &) — no breaking out of a quoted attribute.
  • URL attributes are scheme-checked (href, src, …): javascript: and other dangerous schemes are dropped; data: is allowed only for raster images.
  • Event handlers and refs are dropped — they're wired on the client by mount.
  • Unsafe attribute names (on*, srcdoc) are refused.

Scope

renderToString covers components built from html, each, conditionals, nested templates, and reactive values — i.e. the normal Zoijs view. It does not serialize a raw DOM Node returned from a component (there's no DOM on the server; it throws with a clear message — return html\…`` instead).

How hydration works (and its one trade-off)

hydrate() adopts the server DOM in place — the page's element structure (the expensive, layout-affecting part) is reused exactly, and events + reactive attributes attach to those live nodes. Dynamic content regions (a text slot, an each list, a nested template) are cleared and re-rendered into that structure; since the values match the server, this is invisible (no flash). So the static shell is adopted byte-for-byte, and only dynamic leaves re-render. If the server markup doesn't match what the component produces, hydration degrades gracefully (that region just isn't made reactive) rather than corrupting the page. See RFC 0008.

License

MIT © Zoijs contributors