四个命令,用于运行自建字体 CDN。分片器位于 git submodule font-split。
fonts-download下载选定的六个上游项目到.cache/fonts-raw/。fonts-build读取cdn.toml,构建十二个配置家族到dist/<Family>/。fonts-upload按manifest.json校验本地产物,再上传到 Cloudflare R2 桶。font-sharder把单个源字体拆分为 Unicode-aware WOFF2 分片、CSS 与manifest.json。
分片是静态且完整的,不按页面内容生成。浏览器只请求 unicode-range 命中页面字符的分片。四个 Source Han 与四个 Resource Han Rounded 语言家族各自把本语言子集与完整 SC 字体中缺失的码点合并到同一 CSS family 下。
上游字体 ──fonts-download──▶ .cache/fonts-raw/ ──fonts-build──▶ dist/<Family>/ ──fonts-upload──▶ R2
要求 Python ≥ 3.11 与 uv。克隆时必须初始化子模块:
git clone --recurse-submodules https://github.com/SharpDotNUT/fonts-cdn.git
cd fonts-cdn
uv sync
uv run fonts-download --list
uv run fonts-download
uv run fonts-build --dry-run
uv run fonts-build --skip-baseline
uv run fonts-upload --dry-run已经克隆但缺少子模块时执行 git submodule update --init。uv 通过 workspace 同时安装主包与 font-split,因此 font-sharder 命令也可直接使用。
每个 dist/<Family>/ 包含 <Family>.css、manifest.json 与内容哈希命名的 WOFF2 分片。构建产物上传到 Cloudflare R2 后由 https://fonts-cdn.sharpdotnut.com 对外提供。例如:
<link rel="stylesheet" href="https://fonts-cdn.sharpdotnut.com/SourceHanSansCN/SourceHanSansCN.css">浏览器按 CSS 里的 unicode-range 自动请求命中的分片,页面只需引这一个 <link>。家族 slug、完整地址清单、缓存与 CORS 配置、排错方法见 USAGE.md。
可变家族会在 CSS 中发布 fvar 字重范围。Maple Mono NF 与 LXGW WenKai 是仅 Regular 的静态构建;Inter 不包含 italic。
cdn.toml 定义十二个输出家族:
| CSS family | 源文件 | Fallback |
|---|---|---|
| Source Han Sans CN/TW/HK/JP | SourceHanSans<lang>-VF.ttf |
SourceHanSansSC-VF.ttf |
| Resource Han Rounded CN/TW/HK/JP | ResourceHanRounded<lang>-VF.otf |
ResourceHanRoundedSC-VF.otf |
| Inter | InterVariable.ttf |
— |
| Maple Mono | MapleMono[wght].ttf |
— |
| Maple Mono NF | MapleMono-NF-Regular.ttf |
— |
| LXGW WenKai | LXGWWenKai-Regular.ttf |
— |
每个语言家族都是独立的 CSS family。fallback 只补充该语言源 cmap 中不存在的码点,因此浏览器仍能从同一个 font-family 解析仅 SC 才有的字符。
从 CDN 引用一个 CSS 文件,再用其中的 family 名设置 font-family:
<link rel="stylesheet" href="https://fonts-cdn.sharpdotnut.com/Inter/Inter.css">
<style>
body {
font-family: "Inter", system-ui, sans-serif;
}
</style>CSS 内的分片使用相对路径,浏览器按 unicode-range 只为页面实际出现的字符加载对应 WOFF2,不需要手工引 <link rel="preload">。地址为 https://fonts-cdn.sharpdotnut.com/<slug>/<slug>.css,slug 与上表家族同名(如 SourceHanSansCN、MapleMonoNF)。
完整的家族地址列表、缓存与 CORS 配置、自托管流程与排错见 USAGE.md。
| 参数 | 含义 |
|---|---|
-c, --config PATH |
配置文件(默认 fonts.toml) |
-o, --output DIR |
覆盖 settings.output_dir |
--cache-dir DIR |
覆盖 settings.cache_dir |
-j, --jobs N |
并行下载数(默认 settings.concurrency) |
--only ID |
匹配字体 id(可重复,逗号分隔的 glob) |
--exclude ID |
跳过匹配的字体 id |
-f, --force |
重新下载并解压已存在的文件 |
-n, --dry-run |
只解析计划,不下载 |
-l, --list |
列出配置的字体 |
退出码:0 成功,1 配置错误,2 一个或多个字体失败,130 中断。除非指定 --force,已存在的文件会被跳过;下载与解压均为原子操作。GITHUB_TOKEN(或 GH_TOKEN)可提高 GitHub API 速率限制。
fonts.toml 恰好声明六个下载项:inter、source-han-sans-vf、resource-han-rounded-vf、maple-mono、maple-mono-nf、lxgw-wenkai。其路径相对于配置文件。压缩包的 include/exclude 模式匹配成员文件名。
uv run fonts-build [--config cdn.toml] [--only ID] [--skip-baseline]| 参数 | 含义 |
|---|---|
-c, --config PATH |
构建配置(默认 cdn.toml) |
-o, --output DIR |
覆盖 settings.output_dir |
--only ID / --exclude ID |
用可重复、逗号分隔的 fnmatch 模式选择家族 id |
-f, --force |
重建所选家族的全部分片 |
-n, --dry-run |
打印计划,不写输出 |
-j, --jobs N |
覆盖并行 worker 数 |
--skip-baseline / --no-skip-baseline |
覆盖 baseline 编码设置 |
--no-verify |
跳过产物契约检查 |
--report PATH |
Markdown 报告路径(默认 <output_dir>/build-report.md) |
--no-report |
不写构建报告 |
配置加载器会拒绝未知键、重复的家族 id/slug、非法类型与非法值。家族路径相对于 cdn.toml;-o/--output 相对于当前工作目录。请先运行 fonts-download 再运行 fonts-build;两个命令刻意保持分离。
除非指定 --no-report,每次运行都会写入 <output_dir>/build-report.md,包括 dry-run 与部分失败。报告记录每个家族的状态、分片数、字节数与第一条校验错误。汇总中的 reused 表示该家族没有编码任何分片;built 表示至少编码了一个分片。复用受构建指纹约束(主源与 fallback 源摘要、影响字节的配置、流水线版本),一次完成的重建还会删除该家族目录中的过期分片。
uv run fonts-upload [--config cdn.toml] [--only ID] [--dry-run] [--force]| 参数 | 含义 |
|---|---|
-c, --config PATH |
构建配置(默认 cdn.toml) |
-o, --output DIR |
覆盖 settings.output_dir |
--only ID / --exclude ID |
用可重复、逗号分隔的 fnmatch 模式选择家族 id |
-n, --dry-run |
列出远端缺失的对象,不上传 |
-f, --force |
即便远端分片大小一致也重传 |
-j, --jobs N |
并发 PUT 数(默认 8) |
凭据从进程环境读取;当前工作目录下的 .env(已在 .gitignore 中)会被自动加载,但已存在的进程环境变量优先:
uv run fonts-upload --only Inter # 自动读取 ./.env
uv run --env-file ../secrets/.env fonts-upload --only Inter # 或显式指定文件.env 每行一个 KEY=VALUE,支持 # 注释、export 前缀与成对引号;未知键会被忽略。也可用 setx 把变量持久化到当前用户环境。
| 变量 | 含义 |
|---|---|
R2_ENDPOINT |
https://<account-id>.r2.cloudflarestorage.com,必须是 http(s) URL |
R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY |
R2 API token 的 Access Key ID 与 Secret |
R2_BUCKET |
目标桶名 |
R2_REGION |
可选,默认 auto |
上传前先按 manifest 校验本地目录:schema 必须为 2、slug 必须与目录名一致、每个分片的 bytes 与 blake2b 必须同时匹配。任何不一致都会在发出第一个 PUT 之前失败。对象键为 <slug>/<file>,与 dist/<Family>/ 同构;分片带 font/woff2 与 Metadata.blake2b,CSS 为 text/css; charset=utf-8,manifest 为 application/json; charset=utf-8。
分片文件名本身含内容哈希,因此远端已存在且大小一致的分片默认跳过;CSS 与 manifest.json 是稳定路径,每次运行都会覆盖。该命令不设置 Cache-Control、不删除远端对象、不配置 ACL 或 CORS——缓存策略与桶级配置请另行处理。
上传期间为每个家族显示一条进度条:已完成对象数 / 总数、已上传字节、已用时间与预计剩余时间;对象失败不会中断其余对象,失败明细在家族结束时逐条列出。
退出码:0 成功,1 配置或客户端错误,2 至少一个对象或家族失败,130 中断。
该命令由子模块 font-split 提供;它也可以脱离本仓库独立使用。
uv run font-sharder <font.ttf> [-o dist] [--css-family "Family Name"] [--fallback full-coverage.ttf]选项包括 --family、--css-family、--fallback、--chunk-size、--features、--deps、--areas、--include-cjk-ext、--jobs、--force、--skip-baseline、--no-verify、--dry-run、--manifest-out 与 --font-number。--fallback 只补充 fallback cmap 中存在、而主 cmap 中缺失的码点;这些分片排在主分片之后,位于同一目录并属于同一 CSS family。
分片命名为 <Family>-<order>-<tag>.<content-hash>.woff2。内容哈希使分片 URL 在多次构建之间保持 immutable。manifest 记录每个分片的 origin(primary 或 fallback)以及 fallback 源元数据。
WOFF2 分片使用:
Content-Type: font/woff2
Cache-Control: public, max-age=31536000, immutableCSS 与 manifest.json 使用 Cache-Control: public, max-age=300;稳定的家族路径会在重建时覆盖,而内容哈希分片名可避免残留旧字体字节。为浏览器字体请求配置 CORS。
fonts-upload 只设置 Content-Type(分片另加 Metadata.blake2b),不写入上述 Cache-Control;这两条缓存头需要在桶侧用缓存规则或自定义域策略配置。
项目没有单元测试套件。sharder 在每次真实构建中校验自身产物(覆盖、互斥、unicode-range 往返、布局约束、manifest 摘要、字体可读性与 shaping、CSS 引用、体积预测)。全量构建即是端到端检查:
uv run fonts-build --dry-run # 只规划,不写文件
uv run fonts-build --skip-baseline # 构建全部十二个家族
uv run fonts-build --skip-baseline # 再跑一次:应全部复用
uv tool run ruff check src font-split/src font-split/tools
uv tool run ruff format --check src font-split/src font-split/tools--no-verify 会跳过八项契约检查,仅在调试失败家族时使用。用 --only <id> 重建单个家族。
分片器的完整文档见 font-split README;更细的契约与实现说明见 产物契约、实现说明 与 背景研究。主仓库开发约定见 AGENTS.md。