Skip to content

feat: per-tenant fonts via curated @font-face catalogue #1705

@davidpoblador

Description

@davidpoblador

Carved out from #1704, which lands per-tenant color theming via runtime CSS-variable
injection. Fonts don't compose the same way and deserve their own issue.

Why this is a different problem from #1704

Colors override cleanly because --color-primary already exists in bundle.css; runtime
injection just changes its value. Fonts don't: <style>:root { --font-display: "Foo"; }</style>
only renders correctly if a @font-face { font-family: "Foo"; … } rule has already loaded the
binary. You can't conjure a font at request time.

This pulls in an asset-hosting / catalogue surface that the colors story deliberately avoids.

Proposal

  • Curated pre-approved font catalogue. Apps declare
    FONT_CATALOGUE = [{family, subsets, woff2_url, ttf_url}, ...]. A vibetuner-shipped CSS
    partial (or config.css snippet) declares @font-face for every catalogue entry pointing
    at the shared media host. Browsers only download fonts when something actually references
    them, so listing N entries is essentially free.
  • TenantTheme (from feat: first-class per-tenant theming via runtime CSS-variable injection #1704) gains display_font: str | None and body_font: str | None,
    validated against the configured catalogue at the model layer (reject unknown families).
    The runtime injection then sets --font-display: "Foo Variable" per tenant.
  • Variable fonts only. Non-variable means per-weight files; catalogue size explodes.
  • Carry both woff2 and TTF per catalogue entry. Browsers want woff2; server-side
    compositors (thumbnail / video / OG-image generation) typically need TTF because most
    Python imaging libs don't grok woff2.
  • Don't ship user-uploaded fonts as a default feature. The upload + licensing + CSP
    surface is a significant cliff vs. the curated path. Apps that genuinely need it can
    layer it on top, but the framework default should be "pick from a list."

Why now

Radio's #383 (per-podcast fonts/favicons/logos) wants this. Codifying the curated catalogue
as the vibetuner-recommended path keeps multi-tenant apps consistent before a third app
diverges its own variant.

Depends on #1704 landing first (TenantTheme model + injection partial).

Filed by Claude Code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions