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)

  • RenderInput
    • profile: identity + settings (theme, plan, analytics flag, etc.)
    • links: ordered list of enabled links
    • options: environment + base URLs + flags (e.g., branding footer)
  • RenderResult
    • html: full HTML document
    • meta: { 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 destination href

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.

  • 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.

Each profile link is rendered as an <a class="sb-button">.

Analytics disabled

  • href points directly to the link URL.

Analytics enabled (server-side redirect)

  • href points 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.

The renderer may include a branding footer for free-tier profiles:

  • included only when options.showBrandingFooter is true AND the user plan is FREE
  • omitted for Lifetime

The footer is a normal <footer> element and is styled by theme CSS.

Accessibility

Baseline requirements for the skeleton:

  • .sb-links uses nav with an aria-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 stored url.
  • 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).