Build pipeline for self-hosted OFL CJK webfonts that the popular CDNs (Google Fonts, Fontsource)
don't carry yet. Each font is subset and chunked with
cn-font-split into unicode-range-gated .woff2, then
published to npm under @krosdai and served over jsDelivr:
https://cdn.jsdelivr.net/npm/@krosdai/<font>@<version>/...
Browsers download only the glyph chunks a page actually uses.
| Package | font-family |
Weights | Styles | License | Upstream | jsDelivr |
|---|---|---|---|---|---|---|
@krosdai/lxgw-bright |
LXGW Bright |
300, 400, 500 | normal, italic | OFL-1.1 | lxgw/LxgwBright@v5.528 | index.css |
@krosdai/lxgw-bright-gb |
LXGW Bright GB |
300, 400, 500 | normal, italic | OFL-1.1 | lxgw/LxgwBright@v5.528 | index.css |
@krosdai/lxgw-bright-tc |
LXGW Bright TC |
300, 400, 500 | normal, italic | OFL-1.1 | lxgw/LxgwBright@v5.528 | index.css |
Served from npm via jsDelivr: https://cdn.jsdelivr.net/npm/@krosdai/<name>@<version>/... — subset with cn-font-split (wrapper 7.4.2, core 7.6.8). Pin an exact version.
Pick a package from the table above. Each package ships a grid of CSS entry points so you only
load what you use — every file is still lazy per unicode-range, and .woff2 bytes are shared across
files (mixing entry points never double-downloads a glyph):
| File | Contains |
|---|---|
index.css |
all weights × all styles |
weight-<weight>.css |
one weight, all styles (e.g. weight-400.css) |
style-<style>.css |
one style, all weights (e.g. style-italic.css) |
<weight>-<style>.css |
exactly one cut (e.g. 400-italic.css) |
Via jsDelivr (no install):
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@krosdai/lxgw-bright@2.0.0/index.css"
/>
<style>
body {
font-family: "LXGW Bright", serif;
}
</style>Swap index.css for any narrower entry point, e.g. .../weight-400.css or .../400-italic.css.
Via npm (bundlers):
npm install @krosdai/lxgw-brightimport "@krosdai/lxgw-bright"; // full bundle (index.css)
import "@krosdai/lxgw-bright/weight-400.css"; // one weight, all styles
import "@krosdai/lxgw-bright/style-italic.css"; // one style, all weights
import "@krosdai/lxgw-bright/400-italic.css"; // exactly one cutPin an exact version. The CSS already contains the @font-face rules; you never hand-write them.
Build the packages, then pack them into gitignored artifacts/*.tgz:
pnpm pack:fonts # build:fonts --clean + verify:fonts + npm pack each package
npm install ./artifacts/krosdai-lxgw-bright-2.0.0.tgzOr point a dependency straight at a built package folder (run pnpm build:fonts first so the
generated css/woff2 exist):
Git history is append-only, so committing the generated .woff2 would bloat the repo permanently
with every font update. Publishing to npm keeps this repo small (tooling + per-package manifests only),
and jsDelivr serves npm packages with the same immutable version pinning. The generated .woff2 is
gitignored and ships only inside the published package (via each package.json files allowlist).
Keep packages lean and versions infrequent. Flooding npm with large, many-versioned font packages is the abuse pattern that got accounts blocked from jsDelivr in Dec 2025 — a handful of real, rarely-updated subset fonts (the Fontsource model) is exactly what this is.
packages/<font>/ = one npm package, @krosdai/<font>
├── package.json name, version, exports, files allowlist (generated)
├── README.md per-package usage (generated)
├── OFL.txt license
├── metadata.json provenance + css entry-point map
├── index.css full bundle: every weight × style inlined
├── weight-<weight>.css one weight, all styles (e.g. weight-400.css)
├── style-<style>.css one style, all weights (e.g. style-italic.css)
├── <weight>-<style>.css one cut, @font-face → ./<weight>-<style>/<hash>.woff2
└── <weight>-<style>/ e.g. 400-normal, 500-italic
└── <hash>.woff2 (×N) gitignored; shipped in the npm package; shared by every css above
Every CSS entry point references the same content-hashed .woff2 chunks (no byte duplication — only
the small @font-face text repeats per file).
Each package uses independent SemVer (starting 1.0.0); the upstream font version is recorded in
metadata.json, the package README, and the CHANGELOG. Bump the package version in
fonts.config.ts on any change to the (font source + pipeline) tuple.
- Make the source reachable — set
upstream.releaseUrl(a tagged tarball URL) and/or keep a local clone atupstream.localPath.sourceMode: "auto"(default) uses the clone if present, else the release tarball, so CI builds from a clean checkout. - Add/adjust an entry in
fonts.config.ts:slug(= npm name + jsDelivr path — choose it once),cssFamily,license,upstream,instances, and an independent SemVerversion. Bumpversionwhenever the font source or pipeline changes. - Locally:
pnpm build:fonts→pnpm verify:fonts(thenpnpm format). Commit, PR, merge. - Note the upstream↔package version mapping in
CHANGELOG.md.
Merging a version bump to main triggers
.github/workflows/publish.yml, which publishes any package whose
version isn't on npm yet (scripts/ci-publish.ts queries the registry; unchanged packages are
skipped). @krosdai/* is scoped, so all publishes use --access public.
- New package (not yet on npm) → first publish with the
NPM_TOKENsecret — OIDC can't bootstrap a package that doesn't exist yet. - Existing package, new version → OIDC trusted publishing (no token, automatic provenance).
One-time setup:
- Create a granular npm token — scope
@krosdai, Read and write to packages, ≤90-day expiry (the Dec 2025 cap; set a rotation reminder). Add it as the repo secretNPM_TOKEN(Settings → Secrets and variables → Actions). Required before the first publish. - After a package's first (token) publish, add a Trusted Publisher in its npmjs.com settings →
repository
krosdai/webfonts, workflowpublish.yml. Subsequent bumps then publish via OIDC without the token.
Manual alternative (local): npm login, then pnpm release — builds with --clean, verifies, and
runs pnpm -r publish --access public.
pnpm install
pnpm build:fonts # generate packages/**
pnpm verify:fonts # structural checks
pnpm catalog # regenerate this README's table only
pnpm lint # eslint + prettierEach font keeps its upstream license (see each packages/<font>/OFL.txt and the catalog above). Fonts
are redistributed under the SIL Open Font License 1.1 with attribution to their original authors. The
build tooling in this repo is provided as-is.