Renderer
This page documents the pure HTML renderer used to generate public profile pages.
The renderer is designed to be:
- Pure: no I/O, no network, no database calls inside the render function
- Deterministic: same input → identical HTML output
- No-JS (public pages): emits no
<script>tags - Safe-by-default: user content is HTML-escaped
Renderer API
The renderer accepts a fully-resolved input object and returns a complete HTML document as a string plus metadata.
Types (conceptual)
RenderInputprofile: identity + settings (theme, plan, analytics flag, etc.)links: ordered list of enabled linksoptions: environment + base URLs + flags (e.g., branding footer)
RenderResulthtml: full HTML documentmeta:{ version, contentHash, generatedAt }
Determinism contract
To keep rendering deterministic:
- Inputs must be fully resolved before calling the renderer (no lookups inside).
- The renderer must not read from globals that vary across requests.
- Any generated values must be derived from the input only.
The only non-content metadata is generatedAt in the returned meta, not embedded in HTML.
HTML escaping and XSS
All user-supplied strings are escaped before being embedded in HTML:
displayName,username,bio- link
label, link destinationhref
The renderer does not allow raw HTML in any user field. This is intentional:
- reduces XSS risk
- improves auditability
- keeps the DOM contract stable across themes
Output structure (DOM contract)
Public profiles should render from a stable skeleton that themes style via CSS.
Default skeleton
.sb-card-shell.sb-card(main container).sb-avatar(optional).sb-name.sb-handle.sb-bio(optional).sb-links(nav).sb-button(anchor).sb-button-text(span wrapper for styling markers separately)
.sb-footer(optional branding)
Themes should avoid requiring theme-specific markup. Prefer:
- CSS variables
- scoped overrides (
.theme-<id> ...) - pseudo-elements (
::before,::after)
Controlled exceptions
Some themes may need minimal wrapper markup to preserve layout invariants. Example: Minimalist wraps the avatar in a fixed-height wrapper to prevent visual "jump" when toggling typography variants.
Exceptions should be:
- rare
- isolated
- documented in the theme implementation
Theme selection and classes
Theme class location
The active theme is expressed as a class on the root element (typically <html>):
theme-<themeId>(e.g.,theme-glass,theme-minimalist)
Theme variants are expressed as additional classes (example):
- Minimalist plaintext variant:
is-mono
Compatibility mapping
If legacy theme ids exist (e.g., "light"), the renderer maps them to the canonical theme id (e.g., "paper") at render time for backwards compatibility.
CSS inclusion (base + theme)
Public profiles load:
/css/base.<hash>.css/css/theme-<themeId>.<hash>.css
These paths are resolved via a build-time CSS manifest that maps each theme id to a fingerprinted file.
Why links instead of inline CSS?
- Linked, fingerprinted CSS is cacheable across profiles.
- It allows many themes without shipping all theme CSS with every page.
- It keeps HTML payload small and stable.
Export environment
In export mode, the renderer may inline base + theme CSS into a single <style> tag to produce a self-contained HTML artifact.
Link rendering and analytics
Each profile link is rendered as an <a class="sb-button">.
Analytics disabled
hrefpoints directly to the link URL.
Analytics enabled (server-side redirect)
hrefpoints to a same-origin redirect endpoint (e.g.,/r/<linkId>)- The redirect endpoint records a click event server-side and returns an HTTP redirect
This enables click tracking without any public-page JavaScript.
Branding footer (free tier)
The renderer may include a branding footer for free-tier profiles:
- included only when
options.showBrandingFooteris true AND the user plan isFREE - omitted for Lifetime
The footer is a normal <footer> element and is styled by theme CSS.
Accessibility
Baseline requirements for the skeleton:
.sb-linksusesnavwith anaria-label- Focus states are visible via
:focus-visible - Interactive elements are anchors (not div buttons)
Themes should not remove focus affordances. If a theme changes focus styling, it must remain clearly visible.
Caching and content hashes
The renderer returns a meta.contentHash derived from the full HTML output. This can be used for:
- ETag generation
- cache keys
- debugging "did this profile change?"
Public HTML caching strategy is an implementation choice outside the renderer, but the renderer provides stable primitives to support it.
Known tradeoffs
- Strict escaping limits formatting. This is a deliberate safety + auditability constraint.
- Stable DOM limits theme expressiveness. Themes must work within a constrained skeleton; this keeps the system maintainable.
- Analytics is coarse by design. Server-side redirects can track clicks but not client-side engagement (no JS).
- Export mode trades cacheability for portability. Inline CSS produces a single file at the cost of reuse.
Contract tests (renderer-level)
These invariants should be regression-tested:
- Public HTML contains 0
<script>tags. - The root element includes exactly one
theme-<id>class matching the selected theme. - The document includes ≤ 2 stylesheet links (base + theme) in non-export environments.
- With analytics disabled, rendered link
hrefs equal the storedurl. - With analytics enabled, rendered link
hrefs use the redirect base + link id. - User-provided strings are escaped (no raw
<,>,&, quotes injected). - Identical inputs produce identical HTML output (byte-for-byte).