@gluonjs/ssr
v1.10.0
Published
DOM-independent server rendering and request isolation for Gluon.
Readme
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.
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.
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.
@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.
@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.
