You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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) gainsdisplay_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).
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-primaryalready exists inbundle.css; runtimeinjection 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 thebinary. You can't conjure a font at request time.
This pulls in an asset-hosting / catalogue surface that the colors story deliberately avoids.
Proposal
FONT_CATALOGUE = [{family, subsets, woff2_url, ttf_url}, ...]. A vibetuner-shipped CSSpartial (or
config.csssnippet) declares@font-facefor every catalogue entry pointingat 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) gainsdisplay_font: str | Noneandbody_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.compositors (thumbnail / video / OG-image generation) typically need TTF because most
Python imaging libs don't grok woff2.
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 (
TenantThememodel + injection partial).Filed by Claude Code.