Skip to content

@gluonjs/ssr ​

The official DOM-independent Gluon server renderer consumes the same public html, functional component, application, Store, Router, async built-in, and registered GluonElement definitions used by the browser.

Definitions created by defineGluonElement() use the same registered-class path. Server rendering runs setup in a request-local effect scope, derives the ShadowRoot template, and stops the scope without browser connection lifecycle. Streaming and SSG consume that template unchanged; hydration reruns setup under the browser connection owner and binds the declarative ShadowRoot through the normal element hydrator.

@gluonjs/ssr at a glance ​

Runtime: node · Release: 1.13.0

Documentation guide · npm · Source

Public API: @gluonjs/ssr · @gluonjs/ssr/eleventy · @gluonjs/ssr/hydration · @gluonjs/ssr/static · @gluonjs/ssr/streaming · @gluonjs/ssr/tenant

Install ​

sh
npm install @gluonjs/ssr

Quick start ​

ts
import { renderToString } from '@gluonjs/ssr';
import { html } from '@gluonjs/core';

const markup = await renderToString(html`<main>Hello</main>`);

Choose this package when ​

  • Server rendering, streaming, static generation, and hydration handoff.
  • Request-local effect scopes and declarative ShadowRoot output.
  • Shared public component definitions between browser and server.

Choose another boundary when:

  • Does not replace browser ownership or client hydration logic.
  • Rendered output still depends on public component definitions.
ts
import { createApp, html } from '@gluonjs/core';
import { renderRequest } from '@gluonjs/ssr';

const response = await renderRequest({
  url: '/products/orbit-lamp',
  routes,
  createApp: ({ router, store }) => createApp(() =>
    html`<main>${router.currentRoute.value.path} ${store.dehydrate().version}</main>`,
  ),
});

Every renderRequest() call owns a memory Router, Store manager, application, and detached effect scope. All resources are disposed in finally, including failed and concurrent renders. Browser mount, connection, update, disconnect, and event/directive hooks do not run.

renderToString() escapes child and attribute data, rejects unsafe URL protocols, omits event bindings, resolves async built-in server contracts, and honors explicit unsafeHTML()/trustedHTML()/unsafeURL() values. When an application provides app.config.trustedTypes, hydration resolves that same policy and direct hydrate() or applyProgressivePatch() calls accept it explicitly. The handoff covers browser parser sinks under the tested Chromium Trusted Types CSP without changing SSR serialization or ordinary child-string escaping. Gluon does not create a global policy and does not sanitize arbitrary untrusted HTML. renderElement() emits open Declarative Shadow DOM for a class registered through defineElement(). Its deterministic comment and temporary data-gluon-h-* markers let @gluonjs/ssr/hydration reconstruct client bindings without replacing matching nodes. Each element root also carries inert data-gluon-hydration="v1:<start>:<end>" metadata. The range is local to that ShadowRoot; host property and light-DOM child markers remain in the enclosing template range. This lets hydrateApplication() retain the host before the child hydrator adopts its ShadowRoot at the transported offset. Nested and adjacent roots remain deterministic and independently validatable in streamed output. hydrateElement() removes transport and temporary host markers only after retained hydration; malformed, missing-required, or tampered transport fails closed without root replacement. Direct legacy hydrateElement() calls without the attribute remain compatible.

renderRequest() derives exact component-style IDs from the resolved request tree and merges them between shared UI and application-owned sheets. Style manifests use deterministic IDs and ordered CSS text for initial carriers and browser handoff.

Tenant resolution is request-scoped and optional. Pass resolveTenant() (or an already resolved tenant context) to renderRequest(); the result is exposed to load() and createApp(), serialized in the state handoff, and returned as result.tenant. @gluonjs/ssr/tenant provides JSON-checked serializeTenantContext()/deserializeTenantContext() and installTenant(). No tenant data is stored in a process-wide singleton.

Nested component composition participates in the same traversal. A public defineMolecule() wrapper that calls a public Atom such as Radio() still contributes the Atom's exact stylesheet dependency to the resolved request tree before the hydrated view becomes measurable or interactive.

SsrRequestOptions.signal is optional. When supplied, the exact signal is available as SsrRequestContext.signal and reaches request loading, async boundaries, serialization, and progressive work. Abort rejects with the original signal.reason or a platform AbortError; the request-local Router, Store, application, and effect scope are disposed exactly once for success, failure, and abort. Calls without a signal keep the existing behavior.

serializeSsrState() accepts finite JSON data made from plain objects and arrays and escapes HTML-significant characters plus U+2028/U+2029. The request result includes the serialized value and a safe data-gluon-state script.

hydrateTemplate(), hydrateApplication(), and hydrateElement() validate server DOM before binding events, refs, application context, and reactive updates. Diagnostics distinguish text, attribute, structure, state, and style mismatches. The default recovery replaces the root once; recovery: 'throw' aborts without mutation. Suppressed categories remain recorded but do not call the diagnostic callback. When a standalone template contains renderElement() roots, pass hydrateElements: true to hydrateTemplate() so its nested Declarative Shadow DOM roots are validated and hydrated as part of the same retained handoff.

@gluonjs/ssr/streaming exposes ordered chunks, byte ReadableStreams, and progressive rendering. Shell and boundary records include newly required exact component styles, and the stream writes their carriers before dependent HTML. Resolved nested boundaries arrive as inert patch records or templates. An external AbortSignal cancels pending response work and reaches async sources.

For browser consumers, applyProgressivePatch(root, boundary, { styleRoot }) or applyProgressivePatchTemplate(root, template) is the public handoff for a resolved boundary record or the inert <template data-gluon-async-patch="id"> envelope from renderProgressiveReadableStream. It validates the matching <!--gluon:async:id--> pair, replaces only that fallback range, preserves any nested boundary markers for later records, and installs each new style carrier once after validating its digest and CSS text. Missing, duplicate, or malformed boundary markers fail closed instead of appending content at an unrelated location.

The helper accepts an AbortSignal and throws ProgressivePatchError with a stable code (...ABORTED, ...BOUNDARY, ...INVALID_PATCH, or ...STYLE) before mutating the target. Consumers can therefore distinguish navigation cancellation from malformed transport and report recovery through their own stream lifecycle.

createStyleManifest() accepts either an ordered sheet array or Core's named StyleSheetSelection. Named entries retain their public ID and optional scope in the carrier, so @gluonjs/atoms can serialize createUiStyleSelection(theme) and validate the exact selection during browser installation without a second hand-maintained manifest.

For generated global CSS that must also style Shadow DOM (such as Tailwind), the Vite universal manifest may contain shadowStyles. renderToString(), prepareForHydration(), and renderRequest() emit one compact stylesheet link per Declarative Shadow DOM root instead of copying CSS into every template. Pass the same shadowStyles array to hydrateElement() or hydrateApplication(): the browser verifies the ID, URL, and digest, loads an asset once per document, adopts one constructed stylesheet identity into every root, and removes the links only after retained hydration. Failed validation or loading leaves the original links untouched.

@gluonjs/ssr/static prerenders explicit route URLs and records dynamic fallbacks without rewriting components. renderRequest() can receive the Vite asset manifest, document styles, and a request nonce; its head contains resource hints, the module entry, and temporary style carriers. Hydration validates component carrier count, identity, order, digest, content, and target, then lets the renderer adopt exact client sheet objects before removing carriers. Component sheets release with the hydrated render owner.

@gluonjs/ssr/eleventy registers a custom Eleventy template format through gluonEleventyPlugin(). Route files contain only public URLs; every page calls an application-supplied request factory with route data, the asset manifest, nonce, and an isolated abort signal. The adapter transports the unchanged SSR result, optional CSP input and hydration entry, always disposes the request, and publishes configured dynamic fallbacks as Eleventy global data. It has no runtime dependency on Eleventy and can be tested directly with renderEleventyPage().

The maintained create-gluon --ui --ssr application composes these ownership paths without a second aggregate manifest: createUiStyleSelection() supplies the shared carriers, request rendering derives the exact Button carrier, and a named application selection supplies the starter sheet. On the client, installUi({ hydrate: true }) consumes the shared carriers before hydrateApplication() validates the application selection and usage-derived Button sheet. Its generated browser test requires retained DOM, zero mismatches, recovered: false, one instance of each exact sheet, and release on unmount.

License ​

MIT License, Copyright © 2026 Marc Malerei.

Version-matched documentation. Examples are checked against the public package contracts.