How to automate ligature previews and css in your build pipeline
- Step 1Pair the local runner once — On a Pro or Developer plan, install and pair the @jadapps/runner. CI jobs then dispatch to `127.0.0.1:9789` instead of the browser, so font bytes stay on the build machine. Confirm it's up with `GET 127.0.0.1:9789/v1/health`.
- Step 2Fetch the tool schema (it has one option) — `GET /api/v1/tools/ligature-toggler` returns the schema. For this tool the only input is `sampleText` (textarea, default `office difficult fluffy`). There are no feature flags to pass — the three preview rows are fixed.
- Step 3Detect features first with the inspector — For each font in `src/fonts/`, POST it to [opentype-features-inspector](/font-tools/opentype-features-inspector) (`/v1/tools/opentype-features-inspector/run`). Parse the returned JSON tag list to learn whether `liga`, `dlig`, `salt`, etc. are present. The Ligature Toggler can't tell you this — it only renders.
- Step 4Generate the preview artifact — POST the font plus `{ "sampleText": "office difficult fluffy" }` to `127.0.0.1:9789/v1/tools/ligature-toggler/run`. Save the returned HTML as `dist/ligature-previews/<font>.ligatures.html` and attach it to the build.
- Step 5Write the CSS from the detection result — Use the inspector's JSON (not the Toggler) to emit feature-aware CSS: body gets `"liga" 1, "calt" 1`; headlines add `"dlig" 1` only for fonts whose tag list includes `dlig`. Write to `dist/typography.css`.
- Step 6Add a CI guard — Fail the build if a font's detected feature set is missing a tag your tokens assume (e.g. a display font expected to have `dlig` doesn't). This forces an explicit decision instead of shipping dead CSS. Pair with [font-face-generator](/font-tools/font-face-generator) for the full @font-face pipeline.
Which tool does which job
The Ligature Toggler is a previewer, not a feature detector. Use the inspector for detection. Both are dispatchable via the local runner on paid tiers.
| Need | Tool | Output | Runner endpoint |
|---|---|---|---|
| Visual proof of liga/calt/dlig rendering | ligature-toggler | Self-contained HTML (3 rows + CSS) | /v1/tools/ligature-toggler/run |
| List of feature tags the font defines | opentype-features-inspector | JSON (tags + descriptions + CSS) | /v1/tools/opentype-features-inspector/run |
| Generate the @font-face block | font-face-generator | CSS | /v1/tools/font-face-generator/run |
| Font CSS custom properties / scale | css-variable-generator-font | CSS | /v1/tools/css-variable-generator-font/run |
ligature-toggler runner payload
The complete request shape. Verified against the registry schema in lib/font/font-tool-schemas.ts — sampleText is the only option.
| Field | Type | Default | Notes |
|---|---|---|---|
slug | string | — | ligature-toggler |
| font file | binary (multipart) | — | One TTF/OTF/WOFF/WOFF2; size capped by tier (5 MB / 50 MB / 1 GB) |
options.sampleText | string | office difficult fluffy | The exact string rendered in all three preview rows |
| response | HTML | — | <stem>.ligatures.html, MIME text/html, font base64-inlined |
Cookbook
Pipeline snippets for the runner-based workflow. Replace the curl with your CI step of choice; the payloads are the load-bearing part.
Generate a preview HTML for one font
ExampleThe minimal call: POST the font and sample text, get the self-contained preview back. Save it as a build artifact.
curl -s -X POST 127.0.0.1:9789/v1/tools/ligature-toggler/run \
-F 'file=@src/fonts/Brand-Display.woff2' \
-F 'options={"sampleText":"Affinity Office"}' \
-o dist/previews/Brand-Display.ligatures.html
# -> self-contained HTML, font base64-inlined, 3 rendered rowsDetect features, then decide the CSS
ExampleThe Toggler doesn't detect features — the inspector does. Use its JSON to drive whether you emit dlig.
# Step 1: detect (inspector returns JSON tag list)
curl -s -X POST 127.0.0.1:9789/v1/tools/opentype-features-inspector/run \
-F 'file=@src/fonts/Brand-Display.woff2' > feats.json
# Step 2: emit dlig only if present
if jq -e '.tags | index("dlig")' feats.json >/dev/null; then
echo 'h1,h2{font-feature-settings:"liga" 1,"calt" 1,"dlig" 1;}' >> dist/typography.css
else
echo 'h1,h2{font-feature-settings:"liga" 1,"calt" 1;}' >> dist/typography.css
fiLoop over the font folder for previews
ExampleEmit one preview per font so reviewers can eyeball ligature rendering across the whole family in a PR.
mkdir -p dist/previews
for f in src/fonts/*.woff2; do
name=$(basename "$f" .woff2)
curl -s -X POST 127.0.0.1:9789/v1/tools/ligature-toggler/run \
-F "file=@$f" \
-F 'options={"sampleText":"office difficult fluffy"}' \
-o "dist/previews/$name.ligatures.html"
doneCI guard: fail if an expected feature is missing
ExampleForce an explicit decision when a font swap drops a feature your tokens assume. The Toggler can't gate this — the inspector's JSON does.
# Expect every *-Display font to ship dlig
for f in src/fonts/*-Display.woff2; do
curl -s -X POST 127.0.0.1:9789/v1/tools/opentype-features-inspector/run \
-F "file=@$f" > feats.json
if ! jq -e '.tags | index("dlig")' feats.json >/dev/null; then
echo "::error::$f has no dlig but a display font is expected to"
exit 1
fi
donePixel-diff previews across versions
ExampleRender the same sample text against old and new font versions and diff the HTML/screenshot to catch unintended rendering changes.
# Render both versions with identical sample text
for v in v1 v2; do
curl -s -X POST 127.0.0.1:9789/v1/tools/ligature-toggler/run \
-F "file=@fonts/$v/Brand.woff2" \
-F 'options={"sampleText":"office difficult fluffy"}' \
-o "diff/$v.html"
done
# headless-render diff/v1.html vs diff/v2.html -> flag pixel deltasEdge cases and what actually happens
Every row below was probed against the live API. Some documented requirements (alphabetical axis order, numerical tuple order) are not actually enforced in practice — useful to know if you've been blaming the wrong thing for a 400.
Expecting the Toggler to return a feature list
Wrong toolThe Ligature Toggler returns HTML, never a list of feature tags — it renders three fixed rows and does not parse GSUB. If your script needs to know whether a font has dlig/salt/ss01, call opentype-features-inspector, which returns JSON. Wiring the Toggler into a detection step will fail silently (you'll be grepping HTML).
Runner not paired / not running
Connection refusedPOSTing to 127.0.0.1:9789 with no runner returns a connection error. Confirm the runner is up (GET /v1/health) and that you're on a Pro or Developer plan — the local-runner dispatch path is gated to paid tiers. On free tier the tool runs only in the browser, not via CI.
Passing a feature flag in options
IgnoredThe schema has exactly one option, sampleText. Extra keys like liga or dlig in the options object are ignored — there is no API to change which features the three rows show. Don't build a pipeline assuming you can request, say, an hlig-only row.
Font exceeds the tier file limit in CI
RejectedThe size cap (5 MB free, 50 MB Pro, 1 GB Developer) applies to the runner too. A huge CJK font on Pro near 50 MB will be rejected with a tier-limit error. Subset first with font-subsetter or upgrade the tier for the build account.
dlig row identical to standard row in the artifact
By designIf the font has no dlig glyphs, rows 2 and 3 of the generated HTML are identical — correct, not a render bug. Don't write a CI assertion that the two rows must differ; instead detect dlig via the inspector and only assert on fonts that should have it.
Committing the generated previews vs gitignoring them
Team choiceThe previews are deterministic given the same font + sample text, so committing them gives reproducible PR diffs; gitignoring keeps the repo lean if you trust CI to regenerate. Most teams attach them as build artifacts rather than committing, since fonts change rarely.
Variable font previewed at default instance only
ExpectedThe Toggler inlines the font as-is, so a variable font renders at its default axis values. If your typography uses a specific instance, freeze it first with variable-font-freezer and feed the static file to the pipeline so the preview matches production.
Non-deterministic font-display flash in headless screenshots
Timing-sensitiveThe inlined @font-face uses font-display: block, so a headless screenshot taken too early may capture invisible text mid-decode. Wait for fonts to load (document.fonts.ready) before snapping, or compare the raw HTML instead of a pixel render.
Frequently asked questions
Does the Ligature Toggler detect which features my font supports?
No. It is a previewer — it renders three fixed rows (liga/calt/dlig) from your sample text and outputs HTML. To detect the font's actual feature tags in a script, use opentype-features-inspector, which walks GSUB/GPOS and returns JSON.
What's the runner endpoint and payload?
POST to 127.0.0.1:9789/v1/tools/ligature-toggler/run with the font file (multipart) and options: { sampleText }. The response is the self-contained HTML preview. Get the schema with GET /api/v1/tools/ligature-toggler — the only option is sampleText.
Can I run this in CI on the free tier?
Not via the local runner — runner dispatch is gated to Pro and Developer plans. On free tier the tool runs only in the browser. For automated builds you'll want a paid plan with the @jadapps/runner paired so font bytes stay on your machine.
How do I generate feature-aware CSS automatically?
Detect features with the inspector (JSON), then emit CSS from that: body gets "liga" 1, "calt" 1; headlines add "dlig" 1 only when the inspector reports dlig. The Toggler supplies the visual proof artifact, not the detection logic.
Why would my dlig token silently stop working after a font update?
If the new font version dropped its dlig glyphs, "dlig" 1 becomes a no-op and headlines lose the swashes with no error. A CI guard that asserts the inspector still reports dlig for display fonts catches this regression before it ships.
Does the font leave my machine in this pipeline?
No, when you use the local runner. The dispatch goes to 127.0.0.1:9789, processing happens on your build machine, and nothing is uploaded to JAD's servers. This is the point of running it in CI via the runner rather than the hosted browser tool.
Can I change which features the preview rows show?
No — the three rows (off, standard-on, discretionary-on) are fixed in the handler. The only option is sampleText. If you need a custom feature combination rendered, generate the HTML, then template your own preview using the inspector's CSS output.
Does it work for variable fonts in CI?
Yes, but the preview renders at the default instance. Freeze the instance you ship with variable-font-freezer first if you want the artifact to match production exactly.
Should I commit the generated preview HTML?
It's deterministic, so committing gives reproducible PR diffs; many teams instead publish it as a build artifact and regenerate per build. Either is fine — base it on how often your fonts change and whether you pixel-diff in review.
How big a font can the runner process?
Up to the tier cap: 5 MB free, 50 MB Pro, 1 GB Developer per job. For oversized CJK/icon fonts, subset with font-subsetter before previewing, or run the build under a Developer-tier account.
What other font tools fit this build pipeline?
Pair the Toggler/inspector with font-face-generator for the @font-face block, css-variable-generator-font for tokens, and font-subsetter to keep payloads small. They all dispatch through the same local-runner endpoint pattern.
Why HTML output instead of just a CSS string?
The whole value of this tool is the rendered visual — three rows of your actual font showing the ligature states — which only HTML can carry. The CSS snippet is included inside the HTML's <pre>. For pure CSS generation, the feature-aware approach (inspector + your own template) is the right path.
Privacy first
Every JAD Font tool runs entirely in your browser using opentype.js and the wawoff2 WASM Brotli encoder. Your fonts never leave your device — verified by zero outbound network requests during processing.