Fonts
Self-hosted local fonts — declared once in app.config.ts, delivered as hashed woff2 with @font-face, a size-adjusted fallback face (the CLS guard), a preload link and opt-in subsetting. No font CDN request ever leaves the browser.
Declare local font files once and the framework does the rest: content-hashed
self-hosting, @font-face with font-display, a size-adjusted fallback
face so the swap moves nothing, a <link rel="preload"> in the shell head,
and opt-in unicode-range subsetting.
// app.config.ts (web)
export default {
type: 'web' as const,
name: 'MyApp',
fonts: [{
family: 'Inter',
src: [
{ path: 'src/fonts/Inter-Variable.woff2', weight: '100 900' }, // variable range
// …or discrete faces — Regular+Bold is any real project's minimum:
// { path: 'src/fonts/Inter-400.woff2', weight: 400 },
// { path: 'src/fonts/Inter-700.woff2', weight: 700 },
// { path: 'src/fonts/Inter-Italic.woff2', weight: 400, style: 'italic' },
],
display: 'swap',
subsets: ['latin'],
fallback: 'Arial',
}],
}import { localFont } from '@voltro/web'
const inter = localFont('Inter') // { variable: '--font-inter', fontFamily: 'var(--font-inter)' }
export default function Page(): React.ReactElement {
return <main style={{ fontFamily: inter.fontFamily }}>…</main>
}The shell defines --font-inter as 'Inter', 'Inter Fallback', Arial, sans-serif — use it from any CSS. Every render mode ships the same head tags
(the CSS + preload are baked into the ONE generated shell that dev, static
prerender, SSR streaming and voltro start all serve).
Why the fallback face matters (CLS)
Until the web font arrives, text renders in the fallback — and a fallback
with different metrics reflows the page when the swap happens. The pipeline
reads the font file's real metrics (fontkit) and emits an 'Inter Fallback'
face: local('Arial') with size-adjust, ascent-override,
descent-override and line-gap-override computed by the capsize formula so
the fallback occupies the SAME space. The swap becomes invisible; CLS ≈ 0.
Subsetting
subsets: ['latin'] (and/or 'latin-ext') rewrites each face to just that
unicode range via subset-font, declared with a matching unicode-range so
the browser only downloads what the page's characters need. Measured in the
framework's own e2e: the subset ships at well under half the source size.
GDPR — no font CDN, ever
Nothing here talks to Google Fonts (or any font host) at runtime — the files ship from YOUR origin, content-hashed and immutable. That is the compliance answer German courts made concrete (LG München, remote Google-Fonts embedding): the user's IP never reaches a font CDN because no request leaves your domain. The framework's e2e asserts exactly that — a full page load with zero foreign-host requests.
Getting the files: there is deliberately no Google-Fonts download helper
(license terms differ per family — that step stays yours). The manual path:
download the family from fonts.google.com (or the foundry), drop the
woff2/ttf into src/fonts/, declare it. Done once, committed with the
repo.
Tooling (optional, degrades loudly)
fontkit (metrics) and subset-font (subsetting) ship as optional
dependencies of @voltro/cli — script-free, nothing to approve. If your
installer omits optional dependencies: fonts still self-host with
@font-face + preload, but the fallback metrics and subsets are skipped —
with ONE named warning each, and voltro doctor reports which half is
missing and the fix. Never a silent downgrade.
Native server rendering (API)
An API that renders text through Fontconfig/Pango (an SVG → PNG share card via
sharp, for example) can declare the same fonts: array in its app.config.ts:
export default {
type: 'api' as const,
name: 'cards',
fonts: [{ family: 'Inter', src: [{ path: '../shared/fonts/Inter.woff2', weight: 400 }] }],
}voltro build validates the actual font and writes hashed, native-readable
TTF/OTF files plus a relocatable Fontconfig configuration under
.framework/native-fonts. WOFF/WOFF2 is converted to SFNT; TTF/OTF bytes stay
unchanged. There is no second subsetting: the browser-only subsets, display
and fallback-metric options do not alter server glyphs. A declared family alias
maps to the font's embedded family name; the native font's metadata decides its
face, so choose weight/style with your renderer.
voltro dev prepares and activates these fonts before handlers and startups.
voltro serve and the built direct Node entry only verify and activate the
built artefacts, before startups or HTTP readiness — they need neither the
original font paths nor fontkit/fontverter, downloads or build tools.
Missing, corrupted or declaration-stale artefacts refuse boot with a named
error. Ship the complete .framework directory. A slim image has no host
fonts; without this the renderer drew replacement boxes while PNG magic and
dimensions looked correct — only the pixels showed it.
The declaration owns the process-wide native font configuration: the
framework sets FONTCONFIG_FILE and PANGOCAIRO_BACKEND=fontconfig, and a
conflicting FONTCONFIG_FILE, FONTCONFIG_PATH, FONTCONFIG_SYSROOT or Pango
backend override is refused, not silently overwritten. Run independent APIs in
independent processes. Native libraries cache their fonts, so restart the dev
worker after editing font files. The XML references only declared files, never
host fonts; preserve the fonts' licence notices, and provide faces for every
script you render — declaring a font does not invent missing glyphs.
The native cache uses a writable XDG_CACHE_HOME/fontconfig; when unset, a
directory under the OS temp dir. Image layers and font artefacts may stay
read-only. The renderer must support Fontconfig — this is not a font loader for
other backends; web ogImage's explicit font-byte path stays separate, and an
API without fonts: is untouched. For API builds fontkit validates and
fontverter converts WOFF/WOFF2; both are optional CLI dependencies, and
omitting one when it is needed is a build error, never a silent downgrade.
Production caching
Generated font URLs under /assets/fonts/ contain a twelve-character content hash. The static response policy treats these fingerprinted fonts as immutable assets with a one-year cache lifetime. Unhashed public files keep bounded caching, and HTML keeps its separate freshness policy. Disabling immutable asset caching also applies to generated fonts.
Pages using Islands keep generated font definitions and font preloads in their initial HTML, including server-rendered pages. Global CSS can reference the generated font variables before hydration.