Typography presets

Change fonts through semantic roles instead of copying the theme’s component selectors.

OINK puts font choices behind seven semantic CSS custom properties. A site overrides those roles to change typography, without needing to know which selectors the theme uses internally.

Hugo compiles the whole mechanism into the same static stylesheet at build time: no JavaScript, no remote font service, and no runtime preset loader.

Two built-in presets

hugo.yaml
YAML
params:
  ui:
    typography:
      preset: technical # technical | system
Preset Effect
technical The default. Preserves the Chakra Petch and IBM Plex Mono treatment, with all font files bundled locally
system Resets the display, metadata, print, and monospace roles to platform stacks and requests no OINK brand fonts

The selected value is emitted as data-td-typography on <html>. An unsupported value fails the build rather than silently changing the site.

The seven semantic roles

--td-ui-font-family , CSS property

Navigation, controls, and general chrome. Seeded from --bs-body-font-family.

--td-body-font-family , CSS property

Article and blog prose. Inherits the UI role by default.

--td-heading-font-family , CSS property

Content headings. Takes $headings-font-family, falling back to the body role.

--td-code-font-family , CSS property

Code and terminal content. Takes $font-family-code.

--td-display-font-family , CSS property

Wordmarks and display titles. Chakra Petch, then the UI role.

--td-meta-font-family , CSS property

Technical labels and metadata. IBM Plex Mono, then the code role.

--td-print-font-family , CSS property

Print-only body copy.

Theme components consume these roles or a component alias such as --td-asciinema-font-family; they never name a font directly. The dependency direction is always:

TEXT
Bootstrap base variable  →  OINK semantic role  →  component alias

The reverse is prohibited — Bootstrap custom properties never reference OINK ones — which keeps the graph one-way and prevents custom-property cycles.

Using site-owned fonts

Put local .woff2 files under the site’s static/webfonts/, declare the faces in _styles_project.scss, and override the roles you need. Hugo loads that file after the theme styles:

assets/scss/_styles_project.scss
SCSS
@font-face {
  font-family: 'My Sans';
  font-display: swap;
  font-style: normal;
  font-weight: 400 800;
  src: url('../webfonts/my-sans-variable.woff2') format('woff2');
}

:root {
  --td-ui-font-family: 'My Sans', 'Noto Sans SC', sans-serif;
  --td-body-font-family: var(--td-ui-font-family);
  --td-heading-font-family: var(--td-ui-font-family);
  --td-display-font-family: var(--td-heading-font-family);
}

Remote URLs and arbitrary CSS are deliberately not accepted through YAML. Font files and stylesheets stay local, reviewable build inputs.

Per-content typography

Roles inherit normally, so giving one kind of content its own treatment needs neither a new global preset nor copied component selectors:

assets/scss/_styles_project.scss
SCSS
body.td-blog {
  --td-body-font-family: 'My Serif', 'Noto Serif SC', serif;
  --td-heading-font-family: var(--td-body-font-family);
}

Compatibility with existing configuration

Existing Sass customization remains the first input to this system. OINK reuses established variables rather than introducing parallel knobs:

Existing variable OINK interpretation
$font-family-base / $font-family-sans-serif Bootstrap body font, then the UI and body roles
$headings-font-family The heading role when explicitly configured
$font-family-monospace The Bootstrap monospace base
$font-family-code The code role, and ordinary code, pre, kbd, samp
$td-google-font-name The default print face

Declare these in assets/scss/_variables_project.scss, as in Docsy; they compile into the role defaults.

Custom-property overrides in _styles_project.scss run later and therefore stay available for contextual theming. Project settings intentionally take precedence over preset defaults.

Scope

This is the first token slice, and it covers typography only. Semantic colour, surface, radius, density, and appearance presets are not included; they should arrive separately, once their Bootstrap and shell-token contracts have matching regression coverage.

Next steps