版本记录
这个站是怎么造出来的
传统书籍会在末页留一页版本记录,交代用了什么字体、什么纸、什么工艺。这一页是它的网页版本。
站点分成两层:主页是门面,博客是里屋。两边风格不同——主页柔、博客静——但共用同一套字体、同一个构建系统,主色也是同一支靛紫,接缝在那里。
标志#
ruuo 四个字母连成一条路径,从 r 的竖笔起笔,到 o 闭合收笔,中间不抬笔。买这个域名的理由就是这四个字母能连起来,所以标志应该把这件事画出来,而不是找一支字体把它排出来。
它不是手写坐标凑的,是按几何参数生成的:碗底是真半圆,竖笔切在圆的最左或最右点,因此每个转折都切线连续,弯是软的。o 比 x 高多探出 2.5 个单位——圆形字母在视觉上会显得比方形小,这是排版上的常规补偿。
最难的一处是 r。臂太宽就读成 n,太窄又只剩一根竖笔像 i。最后定在臂展 60(u 的碗跨度是 76),并且让肩缓缓抬起、到最右侧才陡然落下——n 的拱顶在正中,只要这个不对称还在,就不会认错。
页面载入时它会一笔写出来。因为整条路径本来就是连续的,这个动画演的正是“连得起来”这件事本身。
字体#
正文西文用 Inter,中文用思源黑体,等宽用 JetBrains Mono。主页的展示级文字另用未来荧黑——它把思源黑体几何化、收笔做柔,和圆头单线的标志同族。标准黑体是方头收笔,和标志正好相反,放在一起会打架。
四支字体在构建期按全站实际用字做子集化:
| 字体 | 原始 | 子集后 |
|---|---|---|
| 思源黑体 | 16.9 MB | 约 240 KB |
| 未来荧黑(两个字重) | 17.6 MB | 约 470 KB |
| Inter | 859 KB | 约 92 KB |
| JetBrains Mono | 297 KB | 约 41 KB |
变量轴与 OpenType 特性完整保留。字符集只增不减地累积,因此发新文章不会让回访读者重新下载字体。
预载按页面区分:主页首屏只有那一句中文、走的是圆体,思源黑体要到折叠线以下才用得上;文章页则相反。各取所需。
中文排版#
博客那边的正文处理,读者不会注意到,但做错就会一直制造摩擦:
- 中西文间距
- 汉字与拉丁字母、数字相邻时补约八分之一字宽。用零宽元素的外边距实现,复制文本不会带出多余空格。跨标签的接缝同样处理。
- 智能引号
- 直引号自动转弯引号,跨标签配对,段落边界重置。撇号与引号分开判断,
It's不会被当成开引号。 - 标点压缩
- 只施加在显示级字号上。正文里全角标点占满一个字宽是对的,但到了大标题,一个「,」撑开的空洞会把版面撕开。压多少随字号增大而增大,这是印刷排版的老规矩。
- 着重号
- 含中文的强调渲染为字下加点,而不是几何倾斜的伪斜体——汉字没有斜体设计。构建期判定,运行时零成本。
- 避头尾与悬挂标点
- 标点不落行首,开引号不落行尾;行首标点吊出版心,让左边界视觉齐平。
脚注在宽屏上渲染为页边注,浮在正文右侧空白处,与引用它的那一行齐平;窄屏收起,点编号就地展开。这套交互没有 JavaScript 参与。
动效#
主页的背景是五团颜色,由 SVG 的高斯模糊加 alpha 阈值滤镜做融合,靠近时像液体互相吞并——纯滤镜,没有 WebGL。花瓣会被光标推开,斥力随距离平方衰减。指针是一个滞后跟随的环加一个精确跟随的点。
缓动曲线是解阻尼弹簧方程导出的,不是手调的贝塞尔。位移幅度全部刻意压过一轮——廉价的特效感通常不来自“做了什么”,而来自“做过了头”。
入场是有先后的:头像浮现,标志一笔写完,然后那句话才开始打。主角先,配角后。
顶栏的毛玻璃过渡、左上角缩略标志的渐显都由 CSS 滚动驱动动画完成,跑在合成器上,没有一个滚动监听器。prefers-reduced-motion 下全部关闭。
实现#
站点是纯静态的,由一套自写的生成器产出。
- Markdown:
markdown-it-py,扩展了脚注、定义列表、任务列表、容器块 - 代码高亮:构建期由 Pygments 完成,页面不加载任何高亮脚本
- 模板:Jinja2 字体子集:
fontTools图像:Pillow - 搜索索引构建期生成,浏览器端做子串与二元组匹配——中文没有词边界,这比引入分词器更划算,而且断网可用
文章页的运行时 JavaScript 约 4 KB。全部关闭后页面依然可读、可导航:所有“先隐藏、等脚本点亮”的规则都挂在一个由内联脚本打上的标记下面,脚本没跑,页面自动退回全部可见。
写作与发布#
文章在站点自带的网页编辑器里写,预览走的是和生产完全相同的渲染管线与正式样式表,所以所见即所得是字节级的。
保存后触发一次完整构建,产物写入新的版本目录,全部就绪才原子地切换软链接。因此发布过程中不存在页面半新半旧的中间态,回滚也只是把软链接指回上一版。