版本记录

这个站是怎么造出来的

传统书籍会在末页留一页版本记录,交代用了什么字体、什么纸、什么工艺。这一页是它的网页版本。

站点分成两层:主页是门面,博客是里屋。两边风格不同——主页柔、博客静——但共用同一套字体、同一个构建系统,主色也是同一支靛紫,接缝在那里。

标志#

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。全部关闭后页面依然可读、可导航:所有“先隐藏、等脚本点亮”的规则都挂在一个由内联脚本打上的标记下面,脚本没跑,页面自动退回全部可见。

写作与发布#

文章在站点自带的网页编辑器里写,预览走的是和生产完全相同的渲染管线与正式样式表,所以所见即所得是字节级的。

保存后触发一次完整构建,产物写入新的版本目录,全部就绪才原子地切换软链接。因此发布过程中不存在页面半新半旧的中间态,回滚也只是把软链接指回上一版。

esc