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

@vobs/ssr

v1.8.12

Published

Readme

@vobs/ssr

String renderer and hydration adapter for server-side rendering of Vobs applications.

Install

npm install @vobs/ssr

Quick start

// Server
import { renderToStringAsync, serializeState } from '@vobs/ssr'

const result = await renderToStringAsync(render, { resourceClient })
const page = `<!doctype html>
<div id="app">${result.html}</div>
<script>window.__VOBS_STATE__ = ${serializeState(result.state)}</script>`
// Client
import { hydrate } from '@vobs/ssr'

const app = hydrate(render, '#app', { state: window.__VOBS_STATE__, resourceClient })

API

| Signature | Description | | --- | --- | | renderToString(render, options?) | Render synchronously to an HTML string; options.plugins accepts VobsPlugin[]. | | renderToStringAsync(render, options?) | Await resourceClient.prefetchAll(), then flush until the HTML stops changing (options.maxFlushRounds, default 10), and return an AsyncSSRResult with html, dehydrated resources/dict, state, failedResources (only when some prefetch failed), and an optional server request debug snapshot (debug.captureRequests). | | hydrate(render, target, options?) | Reuse the server-rendered DOM, restore dehydrated state, and return the mounted VobsApp<Node>. options.strictHydration rejects a provisional claim of non-empty server text instead of only emitting a debug event. | | createState(options) | Build an SSRState from resource/dict/i18n/theme contexts. | | serializeState(state) | JSON-encode an SSRState for inline embedding (escapes <, >, &, and line separators). | | parseState(snapshot) | Validate and parse a string or object snapshot back into an SSRState. | | createSSRRenderer() | Low-level string renderer; mount into ssr.container and serialize with ssr.toHTML(). | | createHydrationRenderer(container) | Low-level renderer that adopts existing DOM; pass hydration.renderer to createVobs and call app.hydrate(container). | | prerenderRoutes(options) | SSG: render each route to HTML at build time (memory-history router per page, guards/loaders fully executed); returns pages with html/head/state — file writing stays with the caller. | | renderPage(page, options?) | Assemble a PrerenderPage into a full HTML document (default shell or custom template, entryScript support). | | serializeHeadTags(tags) | Serialize HeadTag[] (title/meta/link) into a <head> string with escaping. | | applyHead(tags) | Client-side: apply head tags to document (title replace; meta/link reused by identifying key). | | createHeadSync(router, headFor) | Client-side: keep head tags in sync on SPA navigation after hydration; returns an unregister function. |

Types

SSRRenderer, SSRNode, SSRElement, SSRText, SSRComment, HydrationRenderer, AsyncSSRResult, AsyncSSROptions, SSRState, SSRStateOptions, SSRDebugSnapshot, I18nSSRState, I18nSSRContext, ThemeSSRState, ThemeSSRContext, DictSSRContext, HeadTag, PrerenderPage, PrerenderOptions, PrerenderResult, PrerenderTemplateParts

Hydration strictness

When the server rendered non-empty text while the client wants an empty text node (a bare createText('') that a binding effect overwrites later), hydration can only claim that text provisionally. By default it does so and emits a hydrationProvisionalText runtime debug event; with strictHydration: true it throws a HydrationMismatchError instead (vobsHydration.kind === 'content', actual carrying the server text). The normal empty case — server text serialized as a <!----> placeholder comment — is unaffected.

// Client — reject a silently replaced value instead of debugging it later
hydrate(render, '#app', { state: window.__VOBS_STATE__, strictHydration: true })

Async SSR flushing and failures

renderToStringAsync awaits prefetchAll(), then keeps rendering until the HTML stops changing (bounded by maxFlushRounds, default 10). Content written one asynchronous hop after the data arrives — a timer or a second request inside a binding effect — therefore still lands in the final HTML. maxFlushRounds: 1 restores the old single-update() behavior.

prefetchAll() deliberately never rejects and dehydrate() drops failed entries, so a page can be rendered from partial data without any signal. The result surfaces that as failedResources — a snapshot of resourceClient.errors() ({ key, error }) — present only when non-empty, so a healthy render keeps its previous result shape.

const result = await renderToStringAsync(render, { resourceClient, maxFlushRounds: 10 })
if (result.failedResources) console.warn('SSR rendered from partial data', result.failedResources)

SSG (static site generation)

// scripts/prerender.mjs — build-time, Node only
import { writeFileSync, mkdirSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { prerenderRoutes, renderPage } from '@vobs/ssr'

const result = await prerenderRoutes({
  routes: ['/', '/pricing', '/download'],
  routeRecords: websiteRoutes,               // same records as the client router
  head: path => headMap[path] ?? []          // per-page title/meta/link
})

for (const page of result.pages) {
  const file = join('dist', page.path === '/' ? 'index.html' : page.path, 'index.html')
  mkdirSync(dirname(file), { recursive: true })
  writeFileSync(file, renderPage(page, {
    entryScript: '<script type="module" src="/assets/entry.js"></script>'
  }))
}
// Client entry — hydrate the pre-rendered page and keep <head> in sync on navigation
import { hydrate, createHeadSync } from '@vobs/ssr'
import { createRouter } from '@vobs/router'

const router = createRouter({ history: createBrowserHistory(), routes: websiteRoutes })
createHeadSync(router, path => headMap[path] ?? [])
hydrate(() => RouterView({ router }), '#app', { state: window.__VOBS_STATE__ })