用户拍板「字族扩展都补」。六款按三类各两款落地,全部单页增量 ≤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
86 lines
2.4 KiB
TypeScript
86 lines
2.4 KiB
TypeScript
/**
|
|
* rehype 排版类白名单:文章里允许用 `<span class="...">` / `<p class="fig-cap">` 做受限排版,
|
|
* 但**只放行白名单类名**,其余 class 一律剔除;同时剥离 style 与 on* 事件属性(纵深防御)。
|
|
*
|
|
* 为什么需要:Astro 默认不 sanitize markdown 里的 raw HTML——若直接放行 class,作者(或
|
|
* Sveltia 后台协作者)可写入任意类名/颜色破坏灰阶骨架,甚至借 style/on* 注入。
|
|
* 白名单与 global.css 的「受限排版类」段一一对应,加类时两处同改。
|
|
*/
|
|
|
|
const ALLOWED = new Set([
|
|
// 颜色(走 tokens,明暗双态可读)
|
|
'ink-1',
|
|
'ink-2',
|
|
'ink-3',
|
|
'ink-muted',
|
|
'ink-accent',
|
|
'ink-accent-soft',
|
|
'ink-warm',
|
|
'ink-cool',
|
|
'ink-faint',
|
|
'c-blue',
|
|
'c-blue-soft',
|
|
'c-teal',
|
|
'c-teal-soft',
|
|
'c-green',
|
|
'c-green-soft',
|
|
'c-yellow',
|
|
'c-yellow-soft',
|
|
'c-orange',
|
|
'c-orange-soft',
|
|
'c-red',
|
|
'c-red-soft',
|
|
'c-pink',
|
|
'c-pink-soft',
|
|
'c-purple',
|
|
'c-purple-soft',
|
|
// 字族(与分轨排版共存的四个既有字族)
|
|
'font-serif',
|
|
'font-kai',
|
|
'font-sans',
|
|
'font-mono',
|
|
// 系统字族回退类(不下载字体,命中访客本机字体)
|
|
'font-song',
|
|
'font-kai-sys',
|
|
'font-hei-sys',
|
|
'font-fangsong',
|
|
'font-huiwen',
|
|
'font-kuaile',
|
|
'font-art-kuaile',
|
|
// 文学排版
|
|
'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() {
|
|
return (tree: any) => {
|
|
const walk = (node: any) => {
|
|
if (node?.type === 'element') {
|
|
const props = node.properties ?? (node.properties = {});
|
|
// 剥离内联样式与事件属性:排版能力只经白名单类提供
|
|
delete props.style;
|
|
for (const key of Object.keys(props)) {
|
|
if (/^on/i.test(key)) delete props[key];
|
|
}
|
|
const raw = props.className;
|
|
if (raw !== undefined) {
|
|
const list = Array.isArray(raw) ? raw : String(raw).split(/\s+/);
|
|
const kept = list.map((c: string) => String(c).trim()).filter((c: string) => ALLOWED.has(c));
|
|
if (kept.length > 0) props.className = kept;
|
|
else delete props.className;
|
|
}
|
|
}
|
|
for (const child of node?.children ?? []) walk(child);
|
|
};
|
|
walk(tree);
|
|
};
|
|
}
|