Rendering Pipeline: High and Low Level
This page explains how SimplyPages turns Java objects into HTML.
High-Level Pipeline
- Controller receives request.
- Service loads domain data.
- Controller maps data to
RenderContext. TemplateorComponentrenders HTML.- Response returns full page or fragment.
Both render paths resolve SlotKey values from RenderContext. The template path is preferred for
reusing shared render structure across requests.
sequenceDiagram
participant B as Browser
participant C as Controller
participant S as Service
participant T as Template/Component
B->>C: HTTP request
C->>S: Load domain data
S-->>C: Domain object(s)
C->>T: render(context)
T-->>C: HTML
C-->>B: HTTP response (HTML)
Low-Level: Template Compilation
Template.of(root) compiles a component tree into segments (or use root.compile()):
- Static string segments
- Dynamic slot segments (
SlotKeylookups) - Opaque component segments (fallback component rendering)
Modules are built before compilation (module.build()). Generated child streams are traversed.
Custom render(RenderContext) overrides, including inherited overrides, remain live delegates
so their behavior is preserved. A subclass declaring render() is also retained as an opaque
component; context-aware rendering remains the canonical dispatch contract.
This means Template wraps the compiled component/module tree; callers typically interact with the
template for repeated renders rather than mutating the original tree per request.
Custom Renderer Contract
Components with custom markup behavior must implement render(RenderContext) as the primary render
path. Zero-argument render() should delegate to render(RenderContext.empty()).
This keeps behavior consistent when the component is:
- rendered directly
- nested inside a parent component
- rendered with a populated
RenderContext - compiled into a reusable
Template
For custom renderers that contain child components, pass the supplied context to child renders
instead of calling zero-argument render().
Low-Level: Slot Resolution
During template.render(context) (or template.render(Map<SlotKey<?>, Object>)):
- For each segment, renderer appends output to a single
StringBuilder. - For a slot segment:
- if entry is compiled, append compiled HTML
- if entry is live, render value now
- if key missing, evaluate slot default provider
- Text values are escaped; component values render as nested components.
Compile-On-First-Hit Behavior
With RenderPolicy.COMPILE_ON_FIRST_HIT, explicit live slot entries can be persisted as compiled entries in that context object.
sequenceDiagram
participant App
participant T as Template
participant RC as RenderContext
App->>T: render(RC)
T->>RC: getEntry(slotKey)
RC-->>T: LiveEntry(value)
T->>T: render value -> html
T->>RC: putCompiled(slotKey, html)
T-->>App: html
App->>T: render(RC) again
T->>RC: getEntry(slotKey)
RC-->>T: CompiledEntry(html)
T-->>App: html (no re-render)
Escaping Model
withInnerText(...)escapes text.Slottext values are escaped.withUnsafeHtml(...)bypasses escaping and must only receive trusted content.- Tag names and attribute names are validated before rendering; attribute values are HTML-attribute encoded.
addStyle(...)is the hardened inline-style path. It rejects declaration breakout and replaces exact style properties without matching substrings such aswidthinsidemax-width.
HTMX Edit OOB Flow
sequenceDiagram
participant U as User
participant B as Browser/HTMX
participant E as Edit Endpoint
U->>B: Click Save
B->>E: POST /modules/{id}/save
E-->>B: OOB HTML (close modal + update module)
B->>B: Apply OOB swaps
Practical Debug Checklist
- Wrong value: verify
SlotKeynames and context keys match. - Stale value: check context reuse and compile policy.
- Escaping surprises: confirm
withInnerTextvswithUnsafeHtmlusage. - Missing module content: verify lifecycle assumptions around
buildContent(). - Missing wrapper/custom markup in templates: verify the component declares context-aware custom rendering.