为什么这个博客没有用现成的生成器
一次从零开始的取舍记录
目录
选择自己写一套静态站点生成器,通常是个坏决定。Hugo、Astro、Eleventy 都成熟、快、生态完整,而自己写的那套永远缺功能、有 bug、只有一个用户。
所以值得先说清楚:什么情况下这个坏决定会变成对的。
现成方案不解决的两件事#
第一件是中文排版。 主流生成器的排版能力围绕拉丁文字构建。中西文间距、标点挤压、着重号、避头尾——这些要么没有,要么依赖运行时的 JavaScript 在页面加载后再改一遍 DOM。后者意味着读者会先看到没处理过的版面,然后眼睁睁看着文字跳动一次。
这类工作本该在构建期完成。文章内容是静态的,处理结果也是静态的,没有任何理由推迟到读者的浏览器里做。
第二件是中文字体。 思源黑体覆盖三万余字,原始文件接近 17 MB。这个体积不可能直接上生产。
通行的做法是按 Unicode 区段切成上百个分片,浏览器按需下载。这是个聪明的方案,但它把字体托管绑定到了特定的 CDN 上。
另一条路是:既然文章内容在构建时就完全确定,那么全站到底用到哪些字也完全确定。只把这些字打进字体文件即可。
used = set()
for page in rendered_pages:
used.update(strip_markup(page))
subsetter = subset.Subsetter(options=options)
subsetter.populate(text="".join(sorted(used)))
subsetter.subset(font)
这套站点上的实际效果:
| 字体 | 原始 | 子集后 | 压缩比 |
|---|---|---|---|
| 思源黑体 | 16.9 MB | 约 110 KB | 99.4% |
| Inter | 859 KB | 约 92 KB | 89.3% |
| JetBrains Mono | 297 KB | 约 41 KB | 86.2% |
子集化本身不难,难的是三条不能破的底线:
- 变量轴
- 子集之后
wght、opsz轴必须还在。否则一套字体退化成若干静态字重,体积反而更大,也失去了大字紧、小字松的光学补偿。 - 排版特性
palt、halt决定标点能否挤压,kern、calt决定西文的字偶距与连字。子集化工具默认会丢掉大部分 OpenType 特性,必须显式保留。- 系统兜底
- 字体栈里始终留着
PingFang SC、Microsoft YaHei这一层。万一某个字没进子集,读者看到的只是字形略有出入,而不是一个方框。
一个容易踩的坑#
子集是按用字算的,用字变了,文件内容就变了,内容指纹跟着变,缓存全部失效。也就是说,每发一篇新文章,所有回访读者都要重新下载一遍中文字体。
解决办法是让字符集只增不减:把历次构建用到的字累积记在一个账本里,删掉旧文章不会让字体缩小。
["一", "丁", "七", "万", "丈", "三", "上", "下", "不", "与", ...]
这样文章写到二三十篇之后,新增文章几乎不会引入新字,字体指纹自然稳定下来,缓存长期有效。
关于动效的克制#
页面上有几处动效:导航栏滚动时从透明变成毛玻璃,内容分节在进入视口时轻微上浮,页面之间跳转时标题会形变。
它们有两个共同点。
其一,全部由 CSS 完成。滚动驱动动画用的是 animation-timeline,页面切换用的是 View Transitions,没有一个滚动监听器。这不只是代码量的问题——JavaScript 驱动的滚动动画会和主线程抢时间,页面一忙就掉帧,而 CSS 的滚动时间轴跑在合成器上,不受影响。
其二,位移量都很小。入场上浮只有 18 像素。
- 缓动曲线用真实的阻尼弹簧解算,不用
ease-out - 单次动效时长控制在 400 毫秒以内
-
prefers-reduced-motion下全部关闭 - 视差滚动——不打算做
第三条不是可选项。动效在一部分人身上会引发前庭不适,这是无障碍要求,不是偏好设置。
那么这个坏决定值不值#
如果目标是尽快把文章发出去,用 Hugo,半小时搞定,不要多想。
如果排版本身也是想做的事情之一——那么自己写一套的理由就成立了。中西文混排、标点、着重号、字体子集,这些都不是配置项能解决的,它们要求你对内容处理的每一步都有控制权。
代价是要自己维护。收益是,页面上每一处细节都是有理由的。