Renderer & HTML Output
The renderer is the heart of static.bio's "static" claim. It's a pure function that takes profile data and returns HTML—no database, no I/O, no environment reads.
Renderer Shape
The renderer is a standalone package (@static-bio/render) with zero framework dependencies:
export function renderProfilePage(input: RenderInput): RenderResult;
type RenderInput = {
profile: {
id: string;
username: string;
displayName: string;
bio?: string;
avatarUrl?: string;
themeId: string;
plan: "FREE" | "LIFETIME";
analyticsEnabled: boolean;
};
links: Array<{
id: string;
label: string;
url: string;
enabled: boolean;
order: number;
}>;
options: {
environment: "production" | "preview" | "export";
baseUrl: string;
trackingBaseUrl: string;
showBrandingFooter: boolean;
};
};
type RenderResult = {
html: string; // Full HTML document
meta: {
version: string; // "v1"
contentHash: string; // SHA-256 hash of HTML
generatedAt: string; // ISO timestamp
};
};Constraints:
- Pure: No reading from ENV, no network calls
- Deterministic: Same input → same HTML + contentHash
- Side-effect free: Caller handles writing to disk / HTTP / caching
HTML & CSS Strategy
Inline CSS
All CSS is inlined in a <style> tag within the HTML document. There are no external stylesheets, no <link> tags pointing to CSS files.
- CSS comes from
packages/render/src/css.ts(PUBLIC_CSS constant) - Dashboard CSS (Tailwind) is completely separate—never loaded on public pages
- Size: ~2KB of CSS inlined (well under 15KB budget)
System Font Stack
We use only system fonts:
font-family: system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;This eliminates font downloads (typically 50-200KB, 100-300ms) and ensures instant rendering.
Minimal HTML Structure
HTML structure is hand-written with minimal markup:
- Simple semantic HTML (
<main>,<nav>,<section>) - No unnecessary wrapper divs
- No framework-generated markup overhead
Result: HTML structure is ~1-2KB before gzip, ~1.88KB gzipped total (HTML + CSS).
Escaping & Safety
User Input Escaping
All user-controlled content is HTML-escaped:
displayName,bio,link.labelare escaped- URLs are validated and sanitized before rendering
- Avatar URLs are user-controlled but rendered in
<img src>(browser handles escaping)
XSS Prevention
XSS is prevented through multiple layers:
- No JavaScript: Even if HTML injection occurs, no scripts can execute
- Careful escaping: All user input is HTML-escaped before rendering
- Content Security Policy: Can be added via headers (future enhancement)
Versioning
The renderer is versioned (v1, v2, etc.) to allow gradual migration:
- Each renderer version has a stable API
- New versions can be added without breaking old profiles
- Profiles can be migrated from v1 → v2 at our pace
- Version is included in
meta.versionandX-Render-Versionheader
Future-Proofing
This design makes static export trivial:
// Future export script
const profiles = await getAllProfiles();
for (const profile of profiles) {
const { html } = renderProfilePage({
profile,
links: profile.links,
options: { environment: "export", ... }
});
await writeFile(`out/${profile.username}/index.html`, html);
}The same renderer function that runs at request time can be called at build time to generate static HTML files. This is the "framework apocalypse" escape hatch: if we need to move away from Next.js, we can export all profiles as static files and host them anywhere.