Contents

Fonts

A widget can copy every token and every utility and still render in Times. The typeface is registered on the host, not in those catalogs. Project fonts let RuleCMS load the face and set it on the widget so Preview and the published embed match.

The typeface never lived in the token catalog

A gallery widget can copy every brand token and every utility class and still render in Times. The typeface was never in those catalogs. On a Next.js host, next/font mints a name like --font-geist-sans and puts Geist on body. A class named font-sans is only a pointer at that name. The widget that lands on a page that never ran that loader does not get the face.

CSS then makes it worse. If font-family asks for var(--font-geist-sans) and that name is missing, the browser drops the entire declaration. It does not walk the comma list to sans-serif. The fallback is Times.

Tokens cannot fetch a face. Class bodies wrap as a descendant rule, so a pasted @font-face is ignored. Project fonts are the third catalog: RuleCMS loads the face and, when you ask, sets font-family on the widget so Preview and the published embed match.

Where this is really useful

You wantWithout project fontsWith this catalog
A widget that matches a Tailwind / next/font gallery on a host that did not load GeistTokens point at --font-geist-sans. The name is missing. Type falls back to Times.RuleCMS registers Geist and defines the token on the widget. The embed matches Preview.
A system stack only — no network, never GeistThe widget inherits whatever the host body uses, or Times if that inheritance never arrives.Choose stack-only. The browser uses faces it already has. Nothing downloads.
A Google family with specific weights and subsetsSomeone pastes a gstatic URL that goes stale, or the face never loads on a partner page.You pick the family, weights, and subsets. RuleCMS builds the Google Fonts CSS URL. Authors never paste a file path.
A host that already runs next/font (or ships the same faces)Works today. Do not double-load the face.Turn published inclusion off. Composer and Preview still use the catalog so authors see the typeface while they work.
You do not have to choose between RuleCMS and your stylesheet. Most teams keep the same family name in both places. RuleCMS includes project fonts on published widgets by default so the embed looks correct even on a page that never loaded Geist. If that double load is a problem, turn inclusion off — the radios are described below.

When to use it / when not to

Use project fonts when RuleCMS must own type on every embed host — a landing page, a partner site, or any route that did not run your font loader. Fill the catalog once. Composer, Preview, and — by default — published widgets then show the same face.

Skip the catalog, or turn published inclusion off, when the host stylesheet already registers the face and sets font-family on the widget's ancestors. Pointers in tokens and classes still work. RuleCMS just does not ship a second copy of the files.

The project catalog

Open an environment, then the Fonts card. That page is the catalog for the project, not for one environment: Development, Staging, and Production share it. View opens the preview (the default). Edit is /fonts/edit.

  • Preview shows the CSS this project will emit — a Google Fonts stylesheet link when a face is loaded, plus the token and widget font-family on the render surface. That listing is for authors. Published widgets do not dump a :root rule onto your page.
  • Edit is a card per family. Invalid fields are refused per row. Catalog Save is explicit. You need permission to manage project CSS settings; viewing is a separate permission.

A project has no fonts row until someone saves for the first time. Until then, widgets rely entirely on the host typeface — the Times path if that host never registered the face.

What each field means

FieldWhat it is
FamilyThe face name — Geist, Inter. Not a CSS file. For Google, you pick from the allowlist.
CSS variable (optional)The token this face defines, for example --font-geist-sans. Utilities and class bodies may keep pointing at it. A token that says var(--font-geist-sans) without this entry is the Times bug.
FallbacksSegoe UI, system-ui, sans-serif. Comma-separated family names. RuleCMS builds the file URL; you pick the family — you never paste a network path here.
Source: stack-onlyUse faces the browser already has. No download. That matches “looks like a modern sans site.” It will never be Geist.
Source: GoogleAn allowlisted family plus weights and subsets. RuleCMS writes the Google Fonts CSS URL. You never paste a gstatic path.
Use as the widget's default typefaceAt most one row. Sets font-family on the widget's first real children. Other rows only mint a token and load the face, so a class or leaf can point at them.

Published widgets

On the project's Fonts page, the Published widgets accordion at the top holds two radios. Changing a radio saves immediately. Catalog Save is separate. The first is selected by default:

  • Include these fonts when published widgets are rendered. A non-empty catalog is sent with the widget and the renderer loads the faces inside the widget. This is how RuleCMS makes Preview and the live embed match.
  • My apps already load these fonts. Do not include them again when the widget is being rendered. Published widgets still point at --font-geist-sans, but RuleCMS does not ship the files. Your host must register the face. Use this when the app already runs next/font.
Composer and Preview still use the catalog when inclusion is off. Authors keep seeing the typeface while they work. Only the published (and Development-draft) embed on your site stops carrying the extra faces. Empty catalog plus the setting on is the same as off: there is nothing to include.
If you turn inclusion off, the host must register every face the widgets use. A missing var() on the live page drops the whole font-family — Times again. Check the published widget on a real route after you flip the setting.

What the renderer does

SurfaceWhat happens
Composer canvas and PreviewAlways injects when the catalog is non-empty. The published-inclusion flag is ignored here so authors still see the typeface while they work.
Published widget (React SDK and HTML custom element)The widget fetch includes the catalog when inclusion is on. The HTML custom element ships the same React bundle, so the emit is the same.

You do not republish a widget to change a face. The catalog is joined when the host fetches the widget. The next uncached fetch picks up the current families. (Published traffic may sit in widget-cache for a short TTL — the same delay itemList already has.)

Hosts already fetch the widget once. RuleCMS hangs fonts on that same response as a sibling of the widget, its collections, CSS variables, and class definitions — not folded into itemList.

{
  "success": true,
  "data": {
    "widget": { "publishedKey": "…", "itemList": "…", "…": "…" },
    "childCollections": { },
    "fontFaces": [
      {
        "family": "Geist",
        "cssVariable": "--font-geist-sans",
        "fallbacks": ["Segoe UI", "system-ui", "sans-serif"],
        "applyToWidget": true,
        "source": { "kind": "google", "weights": [400, 700], "subsets": ["latin"] }
      }
    ]
  }
}

fontFaces is omitted when the catalog is empty, when inclusion is off, or if the catalog cannot be read. When inclusion is off, the same response also sends includeFontFaces: false so renderers that still receive a list must not emit it. The widget still loads.

The React renderer (@rulecms/widget-react) and the HTML custom element turn a non-empty catalog into a Google stylesheet link when a face is loaded, plus one scoped <style> on the widget's first real children. @font-face (when emitted) is unscoped — a face has to register on the document. The token and font-family land on [data-rulecms-published-key] > *, not on :root:

[data-rulecms-published-key="env---widget-abc"] > * {
  --font-geist-sans: Geist, "Segoe UI", system-ui, sans-serif;
  font-family: var(--font-geist-sans);
}

The Development draft fetch (Development Integration) does the same join, so a dev. token preview on your site matches composer.

Good to know

  • Light DOM does not mean the host body wins. Once RuleCMS sets font-family on the widget's children, host type no longer inherits into that tree. Turn inclusion off if you want the host face to win.
  • One catalog per project. There is not a separate list per environment. Promote and publish the widgets; the faces stay project-wide.
  • No republish to restyle a face. Change the catalog. The next uncached widget fetch picks it up.
  • The composer font-family picker is a different tool. That drawer control is an allowlisted stack (system faces). It does not load Geist. This catalog is who defines --font-geist-sans.
  • React and the HTML embed, this pass. Other framework SDKs are not in this release.
  • Keep the list small. At most 12 fonts and 8 fallbacks per face. Google families, weights, and subsets come from an allowlist — RuleCMS builds the file URL; you pick the family.

Related docs