feat(fonts): 新增六款自托管字族(体积闸门内)(5i T9)

用户拍板「字族扩展都补」。六款按三类各两款落地,全部单页增量 ≤150KB:
小薇(文学·宋体气质)、新晰黑(文学·现代黑)、得意黑(标题)、庆科黄油(标题·圆润)、
马善政毛笔与柳建毛草(书法),许可证均 OFL。

关键取舍:@fontsource 官方中文切片粒度太粗(每片约 33 字、单片 40KB+),一句话命中
7 片即 308–442KB/页,四款全部超闸门;即便用 subset-fonts.py 按站内用字重裁仍剩
205–346KB(笔画类字族单片就 50KB)。故改为从 google/fonts 取原始 TTF,按**字频序
细切**(约 19 字/片、单片 2–12KB),每族 99–100 片、族总量 0.5–1.2MB,浏览器只拉
命中的片。四个 @fontsource 依赖随之移除,package.json 净零变更。

实测单页增量(一句话场景):xiaowei 44.9KB / huangyou 27.1KB / mashanzheng 45.7KB /
liujianmaocao 30.0KB / smiley 27.1KB / neoxihei 30.0KB——全部 PASS;
六族同页共 105KB。

接线(一处漏就静默失效,踩过两次):
- rehype-class-whitelist 放行新类(不放行则 span 的 class 被整条剥掉,页面有字没脸)
- site-content 的 ALLOWED_PAGE_FONTS 与后台排版下拉同源补齐(三处下拉 + hint)
- font-needs 扩六个 key,保证「内容用到才加载」的按需契约继续成立
- global.css 六组规则含 .mom-text 作用域(5h 补的瞬间作用域不能漏)
- typography-classes.js(后台可视化面板单一事实源)35→42 项
- subset-fonts.py 新增 --split-ttf / --slices / --order-from(hwmc 的 --css 用法未动)

覆盖边界(已在 docs 与后台 hint 写明):切片只含站内用字(约 1900 字),
新写生僻字会回落系统字族,需按 docs 命令重切。

验证: 构建 50 页 exit 0;content-lint 51 页 0/0/0;config-check 211 字段/66 hint;
      六类逐项 getComputedStyle 首项 = ZCOOL XiaoWei / ZCOOL QingKe HuangYou /
      Ma Shan Zheng / Liu Jian Mao Cao / Smiley Sans / LXGW Neo XiHei(6/6);
      六族全部出现在 document.fonts;首页字体请求数保持 17(零回归)

Spec: 2026-11-02_blog-5i-fonts-polish (T9)

Co-authored-by: GLM-5.3
This commit is contained in:
wait4xandGLM-5.3 committed 2026-11-03 12:35:20 +08:00
1 parent 073e124344
commit 9df86cb05b
611 files changed
+962 -9

No files matched your search

+29
View File
@@ -6,6 +6,9 @@ import sansVar from '@fontsource-variable/noto-sans-sc/index.css?url';
import mono400 from '@fontsource/jetbrains-mono/400.css?url';
import mono700 from '@fontsource/jetbrains-mono/700.css?url';
import kuaile from '@fontsource/zcool-kuaile/index.css?url';
// 5i T9 字族扩展:四款 @fontsource 新字族**已按站内用字子集化**(原片粗 ~45KB/片,
// 一句话命中 7 片就 300-440KB,超 150KB/页闸门);产物落 public/fonts/<family>/,
// 由 scripts/build-perf/subset-fonts.py --css 生成,故这里走 public 路径而非 ?url。
import serifVar from '@fontsource-variable/noto-serif-sc/index.css?url';
import kai from 'lxgw-wenkai-screen-webfont/lxgwwenkaiscreen.css?url';
import '../styles/global.css';
@@ -93,6 +96,26 @@ const fontNeeds = await fontsForPage(rawPath, track);
{fontNeeds.has('huiwen') && (
<link rel="preload" as="style" href="/fonts/subset/hwmc/fonts.css" onload="this.rel='stylesheet'" />
)}
<!-- 5i T9:四款 @fontsource 新字族 + 两款手工切片(得意黑/霞鹜新晰黑,脚本:
scripts/build-perf/subset-fonts.py --split-ttf,借 hwmc 窗口作字频序) -->
{fontNeeds.has('xiaowei') && (
<link rel="preload" as="style" href="/fonts/xiaowei/fonts.css" onload="this.rel='stylesheet'" />
)}
{fontNeeds.has('huangyou') && (
<link rel="preload" as="style" href="/fonts/huangyou/fonts.css" onload="this.rel='stylesheet'" />
)}
{fontNeeds.has('mashanzheng') && (
<link rel="preload" as="style" href="/fonts/mashanzheng/fonts.css" onload="this.rel='stylesheet'" />
)}
{fontNeeds.has('liujianmaocao') && (
<link rel="preload" as="style" href="/fonts/liujianmaocao/fonts.css" onload="this.rel='stylesheet'" />
)}
{fontNeeds.has('smiley') && (
<link rel="preload" as="style" href="/fonts/smiley/fonts.css" onload="this.rel='stylesheet'" />
)}
{fontNeeds.has('neoxihei') && (
<link rel="preload" as="style" href="/fonts/neoxihei/fonts.css" onload="this.rel='stylesheet'" />
)}
<noscript>
<link rel="stylesheet" href={sansVar} />
<link rel="stylesheet" href={mono400} />
@@ -100,6 +123,12 @@ const fontNeeds = await fontsForPage(rawPath, track);
{fontNeeds.has('kai') && <link rel="stylesheet" href={kai} />}
{fontNeeds.has('kuaile') && <link rel="stylesheet" href={kuaile} />}
{fontNeeds.has('huiwen') && <link rel="stylesheet" href="/fonts/subset/hwmc/fonts.css" />}
{fontNeeds.has('xiaowei') && <link rel="stylesheet" href="/fonts/xiaowei/fonts.css" />}
{fontNeeds.has('huangyou') && <link rel="stylesheet" href="/fonts/huangyou/fonts.css" />}
{fontNeeds.has('mashanzheng') && <link rel="stylesheet" href="/fonts/mashanzheng/fonts.css" />}
{fontNeeds.has('liujianmaocao') && <link rel="stylesheet" href="/fonts/liujianmaocao/fonts.css" />}
{fontNeeds.has('smiley') && <link rel="stylesheet" href="/fonts/smiley/fonts.css" />}
{fontNeeds.has('neoxihei') && <link rel="stylesheet" href="/fonts/neoxihei/fonts.css" />}
</noscript>
<!-- PWA:manifest + 双方案 theme-color(明暗随系统)+ iOS 图标 -->
<link rel="manifest" href="/manifest.webmanifest" />
+8 -1
View File
@@ -27,7 +27,7 @@
import { getCollection, getEntry } from 'astro:content';
/** 可独立加载的字体 CSS 包(sans / mono 常驻,不在此列) */
export type FontKey = 'serif' | 'kai' | 'kuaile' | 'huiwen';
export type FontKey = 'serif' | 'kai' | 'kuaile' | 'huiwen' | 'xiaowei' | 'huangyou' | 'mashanzheng' | 'liujianmaocao' | 'smiley' | 'neoxihei';
/**
* 类名 → 需要的字体包。
@@ -48,6 +48,13 @@ export const FONT_CLASS_NEEDS: Record<string, FontKey[]> = {
'font-huiwen': ['huiwen'],
'font-kuaile': ['kuaile'],
'font-art-kuaile': ['kuaile'], // 同一个 ZCOOL KuaiLe 族
// 5i T9 字族扩展六款(2026-11-03):内容里写到才加载对应 CSS
'font-xiaowei': ['xiaowei'],
'font-huangyou': ['huangyou'],
'font-mashanzheng': ['mashanzheng'],
'font-liujianmaocao': ['liujianmaocao'],
'font-smiley': ['smiley'],
'font-neoxihei': ['neoxihei'],
'quote-line': ['kai'],
'drop-cap': ['serif'],
};
+7
View File
@@ -51,6 +51,13 @@ const ALLOWED = new Set([
'drop-cap',
'quote-line',
'fig-cap',
// 5i T9 字族扩展六款(2026-11-03):与 global.css / typography-classes.js 同步增补
'font-xiaowei',
'font-huangyou',
'font-mashanzheng',
'font-liujianmaocao',
'font-smiley',
'font-neoxihei',
]);
export default function rehypeClassWhitelist() {
+10
View File
@@ -174,6 +174,9 @@ const ALLOWED_PAGE_FONTS = new Set([
'font-serif', 'font-kai', 'font-sans', 'font-mono',
'font-song', 'font-kai-sys', 'font-hei-sys', 'font-fangsong',
'font-huiwen', 'font-kuaile',
// 5i T9 字族扩展:页面级排版覆盖(typography.font)也放行这六款
'font-xiaowei', 'font-huangyou', 'font-mashanzheng',
'font-liujianmaocao', 'font-smiley', 'font-neoxihei',
]);
const ALLOWED_PAGE_ACCENTS = new Set([
'ink-accent', 'ink-accent-soft',
@@ -204,6 +207,13 @@ function fontVarFor(cls: string): string {
'font-song': 'var(--font-serif)', 'font-kai-sys': 'var(--font-kai)',
'font-hei-sys': 'var(--font-sans)', 'font-fangsong': '"FangSong", STFangsong, serif',
'font-huiwen': '"WF-Hwmc", var(--font-serif)', 'font-kuaile': '"ZCOOL KuaiLe", var(--font-serif)',
// 5i T9 字族扩展:字体族名与 global.css / @font-face 三处完全一致
'font-xiaowei': '"ZCOOL XiaoWei", var(--font-serif)',
'font-huangyou': '"ZCOOL QingKe HuangYou", var(--font-sans)',
'font-mashanzheng': '"Ma Shan Zheng", var(--font-kai)',
'font-liujianmaocao': '"Liu Jian Mao Cao", var(--font-kai)',
'font-smiley': '"Smiley Sans", var(--font-sans)',
'font-neoxihei': '"LXGW Neo XiHei", var(--font-sans)',
};
return map[cls] ?? 'inherit';
}
+22
View File
@@ -225,6 +225,28 @@ pre code { background: none; border: none; padding: 0; font-size: inherit; }
.prose-reading .font-fangsong, .prose-tech .font-fangsong, .rich .font-fangsong, .mom-text .font-fangsong, .prose-body .font-fangsong {
font-family: FangSong, STFangsong, "FangSong_GB2312", serif;
}
/* 5i T9 字族扩展六款(均 OFL / 免费商用且允许 web 嵌入):作用域与上文同款五件套,
含 .mom-text(瞬间富文本容器)。字体族名必须与 @font-face 完全一致,
否则就是“class 在、字形不到”的静默失效(T1 修过的那类)。
四款 @fontsource 自带 unicode-range 切片;得意黑/霞鹜新晰黑 走 public/fonts 手工切片。 */
.prose-reading .font-xiaowei, .prose-tech .font-xiaowei, .rich .font-xiaowei, .mom-text .font-xiaowei, .prose-body .font-xiaowei {
font-family: 'ZCOOL XiaoWei', var(--font-serif);
}
.prose-reading .font-huangyou, .prose-tech .font-huangyou, .rich .font-huangyou, .mom-text .font-huangyou, .prose-body .font-huangyou {
font-family: 'ZCOOL QingKe HuangYou', var(--font-sans);
}
.prose-reading .font-mashanzheng, .prose-tech .font-mashanzheng, .rich .font-mashanzheng, .mom-text .font-mashanzheng, .prose-body .font-mashanzheng {
font-family: 'Ma Shan Zheng', var(--font-kai);
}
.prose-reading .font-liujianmaocao, .prose-tech .font-liujianmaocao, .rich .font-liujianmaocao, .mom-text .font-liujianmaocao, .prose-body .font-liujianmaocao {
font-family: 'Liu Jian Mao Cao', var(--font-kai);
}
.prose-reading .font-smiley, .prose-tech .font-smiley, .rich .font-smiley, .mom-text .font-smiley, .prose-body .font-smiley {
font-family: 'Smiley Sans', var(--font-sans);
}
.prose-reading .font-neoxihei, .prose-tech .font-neoxihei, .rich .font-neoxihei, .mom-text .font-neoxihei, .prose-body .font-neoxihei {
font-family: 'LXGW Neo XiHei', var(--font-sans);
}
/* 文学常用:首字下沉(书评开篇)与金句块(引文式的独立强调段) */
.prose-reading p.drop-cap::first-letter, .prose-tech p.drop-cap::first-letter, .rich p.drop-cap::first-letter, .mom-text p.drop-cap::first-letter, .prose-body p.drop-cap::first-letter {
float: left;