Rendering pipeline.
Handles rendering hiccup to HTML and formatting Datastar SSE events.
Rendering pipeline. Handles rendering hiccup to HTML and formatting Datastar SSE events.
(apply-render-middleware render-fn middleware-chain)Wrap a render-fn with a middleware chain.
Middleware are functions of the form (fn [handler] (fn [req] ...)),
identical to Ring middleware. The chain is a sequence of middleware
where earlier entries wrap outermost (execute first).
Returns the wrapped render-fn, or the original render-fn if the chain is empty.
Wrap a render-fn with a middleware chain. Middleware are functions of the form `(fn [handler] (fn [req] ...))`, identical to Ring middleware. The chain is a sequence of middleware where earlier entries wrap outermost (execute first). Returns the wrapped render-fn, or the original render-fn if the chain is empty.
(fingerprint v)Compute a short hex fingerprint for a value using Clojure's hash.
Used to give each head element a stable, content-based identity so the
SSE head-update JS can diff by fingerprint rather than blindly removing
and re-appending every element on each render cycle.
Compute a short hex fingerprint for a value using Clojure's `hash`. Used to give each head element a stable, content-based identity so the SSE head-update JS can diff by fingerprint rather than blindly removing and re-appending every element on each render cycle.
(format-connected-event tab-id)Format the initial SSE connected event for a tab.
Format the initial SSE connected event for a tab.
(format-datastar-fragment html)Format HTML as a Datastar patch-elements SSE event.
Datastar expects Server-Sent Events in the format: event: datastar-patch-elements data: elements <html content>
For multi-line HTML content, emits multiple 'data: elements' lines that Datastar will concatenate. This prevents in HTML from prematurely terminating the SSE event.
Format HTML as a Datastar patch-elements SSE event. Datastar expects Server-Sent Events in the format: event: datastar-patch-elements data: elements <html content> For multi-line HTML content, emits multiple 'data: elements' lines that Datastar will concatenate. This prevents in HTML from prematurely terminating the SSE event.
(format-datastar-fragments html-strings)Format multiple HTML strings as separate Datastar patch-elements SSE events. Used for partial reactive renders where each fragment targets a different element.
Format multiple HTML strings as separate Datastar patch-elements SSE events. Used for partial reactive renders where each fragment targets a different element.
(format-head-update title extra-head-html)Build a self-removing <script> SSE event that imperatively updates the document title and diffs user-provided <head> elements.
Why not morph? Morphing <head> inner content via idiomorph can disconnect <style>/<link> elements from the browser's CSSOM — the nodes stay in the DOM but styles stop applying. By using JS to selectively remove/append we guarantee the browser re-evaluates only the elements that actually changed.
Each user-managed head element carries a data-hyper-head attribute
whose value is a content-based fingerprint (short SHA-256 hex). On
each SSE cycle the emitted JS:
[data-hyper-head] nodes in
the DOM.This avoids FOUC and prevents third-party scripts from re-executing when their content hasn't changed (issue #42).
The script tag uses Datastar's mode append + selector body pattern
(the SDK's ExecuteScript convention) with data-effect="el.remove()"
so it auto-cleans after execution.
Build a self-removing <script> SSE event that imperatively updates the document title and diffs user-provided <head> elements. Why not morph? Morphing <head> inner content via idiomorph can disconnect <style>/<link> elements from the browser's CSSOM — the nodes stay in the DOM but styles stop applying. By using JS to selectively remove/append we guarantee the browser re-evaluates only the elements that actually changed. Each user-managed head element carries a `data-hyper-head` attribute whose value is a content-based fingerprint (short SHA-256 hex). On each SSE cycle the emitted JS: 1. Collects existing fingerprints from `[data-hyper-head]` nodes in the DOM. 2. Removes any DOM node whose fingerprint is NOT in the new set (stale element). 3. Appends only elements whose fingerprint is NOT already in the DOM (new or changed element). This avoids FOUC and prevents third-party scripts from re-executing when their content hasn't changed (issue #42). The script tag uses Datastar's `mode append` + `selector body` pattern (the SDK's ExecuteScript convention) with `data-effect="el.remove()"` so it auto-cleans after execution.
(format-heartbeat)Format an SSE keepalive as a comment line. Lines beginning with ':' are comments per the SSE spec and are ignored by every conformant parser (including Datastar), so this carries no payload — its only job is to keep the stream warm through idle-timeout proxies and to surface a dead/half-open channel the next time the renderer writes.
Format an SSE keepalive as a comment line. Lines beginning with ':' are comments per the SSE spec and are ignored by every conformant parser (including Datastar), so this carries no payload — its only job is to keep the stream warm through idle-timeout proxies and to surface a dead/half-open channel the next time the renderer writes.
(get-render-fn app-state* tab-id)Get the render function for a tab.
Get the render function for a tab.
(mark-head-elements head-hiccup)Add {:data-hyper-head "<fingerprint>"} to each top-level hiccup element
in a resolved :head value. The fingerprint is a content-based hash of the
element's hiccup (excluding the data-hyper-head attr itself), giving each
element a stable identity.
On SSE re-renders, the head-update JS uses these fingerprints to diff against what's already in the DOM: unchanged elements are left alone, stale ones are removed, and only genuinely new/changed elements are appended. This avoids FOUC and prevents third-party scripts from re-executing when their content hasn't changed.
Handles:
[:style ...]([:link ...] [:style ...])[[:link ...] [:style ...]]Add `{:data-hyper-head "<fingerprint>"}` to each top-level hiccup element
in a resolved :head value. The fingerprint is a content-based hash of the
element's hiccup (excluding the data-hyper-head attr itself), giving each
element a stable identity.
On SSE re-renders, the head-update JS uses these fingerprints to diff
against what's already in the DOM: unchanged elements are left alone,
stale ones are removed, and only genuinely new/changed elements are
appended. This avoids FOUC and prevents third-party scripts from
re-executing when their content hasn't changed.
Handles:
- a single vector element `[:style ...]`
- a seq/list of elements `([:link ...] [:style ...])`
- a vector of elements `[[:link ...] [:style ...]]`(register-render-fn! app-state* tab-id render-fn)Register a render function for a tab.
Register a render function for a tab.
(render-tab app-state* session-id tab-id)(render-tab app-state* session-id tab-id base-req)Render the current view for a tab and return the rendered data.
Returns nil when no render-fn is registered for the tab, or one of:
A Ring response map (when the render-fn returns a map with :status), passed through as-is for redirects, error responses, etc.
A render result map with pre-serialized HTML strings: :title - resolved page title string, or nil :head-html - HTML string of marked <head> elements, or nil :body-html - HTML string of the rendered page body :url - current route URL string, or nil :declared-signals - vector of signal declaration maps for HTML injection
Binds context/*request* and context/*action-idx* for the duration
of both rendering and HTML serialization, so lazy hiccup sequences
(from for, map, etc.) that read *request* see the correct
bindings when realized by Chassis.
An optional base-req (Ring request map) can be provided for initial page loads so the render function sees the full Ring request context (headers, cookies, query-params, etc.). On SSE re-renders, base-req is nil and a synthetic request is built with only Hyper context keys. Accessing HTTP-only keys (e.g. :cookies, :headers) on the synthetic request returns nil and logs a warning, alerting developers to use middleware + cursors for data that must survive re-renders.
On each render, re-resolves the render-fn from live routes so that:
Title is resolved from route :title metadata via hyper.routes/resolve-title, supporting static strings, functions of the request, and deref-able values (cursors/atoms) so that title updates reactively with state changes.
Render the current view for a tab and return the rendered data.
Returns nil when no render-fn is registered for the tab, or one of:
- A Ring response map (when the render-fn returns a map with :status),
passed through as-is for redirects, error responses, etc.
- A render result map with pre-serialized HTML strings:
:title - resolved page title string, or nil
:head-html - HTML string of marked <head> elements, or nil
:body-html - HTML string of the rendered page body
:url - current route URL string, or nil
:declared-signals - vector of signal declaration maps for HTML injection
Binds `context/*request*` and `context/*action-idx*` for the duration
of both rendering and HTML serialization, so lazy hiccup sequences
(from `for`, `map`, etc.) that read `*request*` see the correct
bindings when realized by Chassis.
An optional base-req (Ring request map) can be provided for initial
page loads so the render function sees the full Ring request context
(headers, cookies, query-params, etc.). On SSE re-renders, base-req
is nil and a synthetic request is built with only Hyper context keys.
Accessing HTTP-only keys (e.g. :cookies, :headers) on the synthetic
request returns nil and logs a warning, alerting developers to use
middleware + cursors for data that must survive re-renders.
On each render, re-resolves the render-fn from live routes so that:
- Redefining the routes Var with new inline fns picks up the new function
- Var-based :get handlers (e.g. #'my-page) automatically deref to the latest
Title is resolved from route :title metadata via hyper.routes/resolve-title,
supporting static strings, functions of the request, and deref-able values
(cursors/atoms) so that title updates reactively with state changes.(safe-render render-fn req render-error-fn)Safely render a view with an error boundary.
On exception, logs via telemere and delegates UI rendering to
render-error-fn, a function (fn [error req] -> hiccup). The fn is
invoked as an IFn, so a Var pointing at a renderer (e.g.
#'my.app/error-page) works transparently and picks up redefinitions
without restarting the server.
Safely render a view with an error boundary. On exception, logs via telemere and delegates UI rendering to `render-error-fn`, a function `(fn [error req] -> hiccup)`. The fn is invoked as an `IFn`, so a Var pointing at a renderer (e.g. `#'my.app/error-page`) works transparently and picks up redefinitions without restarting the server.
(unwrap-body hiccup)Strip a top-level [:body ...] wrapper from user hiccup.
Hyper owns the <body> tag — it carries data-init (for the Datastar
SSE connection) and the SPA navigation scripts, with a <div id="hyper-app">
inside it as the morph target for SSE re-renders.
When a render function returns [:body ...], the nested <body> is
tolerated by browsers on initial load, but on SSE re-renders idiomorph
replaces #hyper-app content with HTML containing a <body>, which
corrupts the DOM and stops further updates (see issue #40).
This function detects the pattern, logs a warning, and returns only
the children so downstream code always receives hiccup without <body>.
Any attributes on the [:body] tag are discarded (the warning tells the
developer to remove it).
Strip a top-level `[:body ...]` wrapper from user hiccup. Hyper owns the `<body>` tag — it carries `data-init` (for the Datastar SSE connection) and the SPA navigation scripts, with a `<div id="hyper-app">` inside it as the morph target for SSE re-renders. When a render function returns `[:body ...]`, the nested `<body>` is tolerated by browsers on initial load, but on SSE re-renders idiomorph replaces `#hyper-app` content with HTML containing a `<body>`, which corrupts the DOM and stops further updates (see issue #40). This function detects the pattern, logs a warning, and returns only the children so downstream code always receives hiccup without `<body>`. Any attributes on the `[:body]` tag are discarded (the warning tells the developer to remove it).
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |