{
 "version": "https://jsonfeed.org/version/1.1",
 "title": "ruuo",
 "home_page_url": "https://ruuo.de/",
 "feed_url": "https://ruuo.de/feed.json",
 "description": "ruuo 的个人站点。",
 "language": "zh-Hans",
 "authors": [
  {
   "name": "ruuo",
   "url": "https://ruuo.de"
  }
 ],
 "items": [
  {
   "id": "https://ruuo.de/posts/on-writing-less/",
   "url": "https://ruuo.de/posts/on-writing-less/",
   "title": "少写一点",
   "summary": "写得少不是产量问题，是筛选问题。真正值得写下来的事情，本来就不多。",
   "date_published": "2026-08-16T16:00:00Z",
   "tags": [
    "随笔"
   ],
   "content_html": "<p>更新频率是个陷阱。</p>\n<p>一旦开始按周或者按月给自己派任务，写作的驱动力就从“这件事想清楚了”变成了“这周该发了”。前者产出的东西有密度，后者产出的东西有字数。</p>\n<p>问题在于，只有作者本人能分辨这两者的区别。读者看到的都是一篇文章，标题一样体面，段落一样整齐。差别要读到第三段才显出来——想清楚了的那篇会持续给出新信息，凑数的那篇在原地重复第一段已经说完的话。</p>\n<div class=\"callout callout-pull\">\n<p>写得慢不是因为敲字慢，是因为想清楚一件事本来就慢。</p>\n</div>\n<p>值得写下来的事情有个粗糙的判据：<strong>你能不能说出一句别人没说过的话</strong>。不是观点更激进，也不是措辞更漂亮，而是确实提供了一个新的角度或者一条新的信息。</p>\n<p>按这个标准筛，大部分选题会被筛掉。这是正常的。一年里真正想明白的事情，本来也就那么几件。</p>\n<p>剩下的时间不写，不是浪费。是在攒。</p>\n"
  },
  {
   "id": "https://ruuo.de/posts/building-this-site/",
   "url": "https://ruuo.de/posts/building-this-site/",
   "title": "为什么这个博客没有用现成的生成器",
   "summary": "现成的静态站点生成器足够好，但它们都不解决中文排版和字体体积这两个问题。这篇记录从零写一套的取舍过程。",
   "date_published": "2026-08-15T16:00:00Z",
   "tags": [
    "工程",
    "排版"
   ],
   "content_html": "<p>选择自己写一套静态站点生成器，通常是个坏决定。Hugo、Astro、Eleventy 都成熟、快、生态完整，而自己写的那套永远缺功能、有 bug、只有一个用户。</p>\n<p>所以值得先说清楚：什么情况下这个坏决定会变成对的。</p>\n<h2 id=\"现成方案不解决的两件事\">现成方案不解决的两件事<a class=\"hlink\" href=\"#现成方案不解决的两件事\" aria-label=\"本节链接\">#</a></h2>\n<p><strong>第一件是中文排版。</strong> 主流生成器的排版能力围绕拉丁文字构建。中西文间距、标点挤压、着重号、避头尾——这些要么没有，要么依赖运行时的 JavaScript 在页面加载后再改一遍 DOM。后者意味着读者会先看到没处理过的版面，然后眼睁睁看着文字跳动一次。</p>\n<p>这类工作本该在构建期完成。文章内容是静态的，处理结果也是静态的，没有任何理由推迟到读者的浏览器里做。</p>\n<p><strong>第二件是中文字体。</strong> 思源黑体覆盖三万余字，原始文件接近 17 MB。这个体积不可能直接上生产。</p>\n<p>通行的做法是按 Unicode 区段切成上百个分片，浏览器按需下载。这是个聪明的方案，但它把字体托管绑定到了特定的 CDN 上。</p>\n<p>另一条路是：既然文章内容在构建时就完全确定，那么全站到底用到哪些字也完全确定。只把这些字打进字体文件即可。</p>\n<figure class=\"code\" data-lang=\"python\" data-lines=\"7\"><figcaption class=\"code-head\"><span class=\"code-title\">按用字子集化</span><span class=\"code-lang\">Python</span></figcaption><pre><code class=\"lang-python\"><span class=\"t-n\">used</span> <span class=\"t-o\">=</span> <span class=\"t-nb\">set</span><span class=\"t-p\">()</span>\n<span class=\"t-k\">for</span> <span class=\"t-n\">page</span> <span class=\"t-ow\">in</span> <span class=\"t-n\">rendered_pages</span><span class=\"t-p\">:</span>\n    <span class=\"t-n\">used</span><span class=\"t-o\">.</span><span class=\"t-n\">update</span><span class=\"t-p\">(</span><span class=\"t-n\">strip_markup</span><span class=\"t-p\">(</span><span class=\"t-n\">page</span><span class=\"t-p\">))</span>\n\n<span class=\"t-n\">subsetter</span> <span class=\"t-o\">=</span> <span class=\"t-n\">subset</span><span class=\"t-o\">.</span><span class=\"t-n\">Subsetter</span><span class=\"t-p\">(</span><span class=\"t-n\">options</span><span class=\"t-o\">=</span><span class=\"t-n\">options</span><span class=\"t-p\">)</span>\n<span class=\"t-n\">subsetter</span><span class=\"t-o\">.</span><span class=\"t-n\">populate</span><span class=\"t-p\">(</span><span class=\"t-n\">text</span><span class=\"t-o\">=</span><span class=\"t-s2\">\"\"</span><span class=\"t-o\">.</span><span class=\"t-n\">join</span><span class=\"t-p\">(</span><span class=\"t-nb\">sorted</span><span class=\"t-p\">(</span><span class=\"t-n\">used</span><span class=\"t-p\">)))</span>\n<span class=\"t-n\">subsetter</span><span class=\"t-o\">.</span><span class=\"t-n\">subset</span><span class=\"t-p\">(</span><span class=\"t-n\">font</span><span class=\"t-p\">)</span>\n</code></pre></figure>\n<p>这套站点上的实际效果：</p>\n<table>\n<thead>\n<tr>\n<th>字体</th>\n<th>原始</th>\n<th>子集后</th>\n<th>压缩比</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>思源黑体</td>\n<td>16.9 MB</td>\n<td>约 110 KB</td>\n<td>99.4%</td>\n</tr>\n<tr>\n<td>Inter</td>\n<td>859 KB</td>\n<td>约 92 KB</td>\n<td>89.3%</td>\n</tr>\n<tr>\n<td>JetBrains Mono</td>\n<td>297 KB</td>\n<td>约 41 KB</td>\n<td>86.2%</td>\n</tr>\n</tbody>\n</table>\n<p>子集化本身不难，难的是三条不能破的底线：</p>\n<dl>\n<dt>变量轴</dt>\n<dd>子集之后 <code>wght</code>、<code>opsz</code> 轴必须还在。否则一套字体退化成若干静态字重，体积反而更大，也失去了大字紧、小字松的光学补偿。</dd>\n<dt>排版特性</dt>\n<dd><code>palt</code>、<code>halt</code> 决定标点能否挤压，<code>kern</code>、<code>calt</code> 决定西文的字偶距与连字。子集化工具默认会丢掉大部分 OpenType 特性，必须显式保留。</dd>\n<dt>系统兜底</dt>\n<dd>字体栈里始终留着 <code>PingFang SC</code>、<code>Microsoft YaHei</code> 这一层。万一某个字没进子集，读者看到的只是字形略有出入，而不是一个方框。</dd>\n</dl>\n<h2 id=\"一个容易踩的坑\">一个容易踩的坑<a class=\"hlink\" href=\"#一个容易踩的坑\" aria-label=\"本节链接\">#</a></h2>\n<p>子集是按用字算的，用字变了，文件内容就变了，内容指纹跟着变，缓存全部失效。也就是说，每发一篇新文章，所有回访读者都要重新下载一遍中文字体。</p>\n<p>解决办法是让字符集<strong>只增不减</strong>：把历次构建用到的字累积记在一个账本里，删掉旧文章不会让字体缩小。</p>\n<figure class=\"code\" data-lang=\"json\" data-lines=\"1\"><figcaption class=\"code-head\"><span class=\"code-title\">var/charset.json</span><span class=\"code-lang\">JSON</span></figcaption><pre><code class=\"lang-json\"><span class=\"t-p\">[</span><span class=\"t-s2\">\"一\"</span><span class=\"t-p\">,</span><span class=\"t-w\"> </span><span class=\"t-s2\">\"丁\"</span><span class=\"t-p\">,</span><span class=\"t-w\"> </span><span class=\"t-s2\">\"七\"</span><span class=\"t-p\">,</span><span class=\"t-w\"> </span><span class=\"t-s2\">\"万\"</span><span class=\"t-p\">,</span><span class=\"t-w\"> </span><span class=\"t-s2\">\"丈\"</span><span class=\"t-p\">,</span><span class=\"t-w\"> </span><span class=\"t-s2\">\"三\"</span><span class=\"t-p\">,</span><span class=\"t-w\"> </span><span class=\"t-s2\">\"上\"</span><span class=\"t-p\">,</span><span class=\"t-w\"> </span><span class=\"t-s2\">\"下\"</span><span class=\"t-p\">,</span><span class=\"t-w\"> </span><span class=\"t-s2\">\"不\"</span><span class=\"t-p\">,</span><span class=\"t-w\"> </span><span class=\"t-s2\">\"与\"</span><span class=\"t-p\">,</span><span class=\"t-w\"> </span><span class=\"t-err\">...</span><span class=\"t-p\">]</span>\n</code></pre></figure>\n<p>这样文章写到二三十篇之后，新增文章几乎不会引入新字，字体指纹自然稳定下来，缓存长期有效。</p>\n<h2 id=\"关于动效的克制\">关于动效的克制<a class=\"hlink\" href=\"#关于动效的克制\" aria-label=\"本节链接\">#</a></h2>\n<p>页面上有几处动效：导航栏滚动时从透明变成毛玻璃，内容分节在进入视口时轻微上浮，页面之间跳转时标题会形变。</p>\n<p>它们有两个共同点。</p>\n<p>其一，全部由 CSS 完成。滚动驱动动画用的是 <code>animation-timeline</code>，页面切换用的是 View Transitions，没有一个滚动监听器。这不只是代码量的问题——JavaScript 驱动的滚动动画会和主线程抢时间，页面一忙就掉帧，而 CSS 的滚动时间轴跑在合成器上，不受影响。</p>\n<p>其二，位移量都很小。入场上浮只有 18 像素。</p>\n<ul class=\"contains-task-list\">\n<li class=\"task-list-item enabled\"><input class=\"task-list-item-checkbox\" checked=\"checked\"  type=\"checkbox\"> 缓动曲线用真实的阻尼弹簧解算，不用 <code>ease-out</code></li>\n<li class=\"task-list-item enabled\"><input class=\"task-list-item-checkbox\" checked=\"checked\"  type=\"checkbox\"> 单次动效时长控制在 400 毫秒以内</li>\n<li class=\"task-list-item enabled\"><input class=\"task-list-item-checkbox\" checked=\"checked\"  type=\"checkbox\"> <code>prefers-reduced-motion</code> 下全部关闭</li>\n<li class=\"task-list-item enabled\"><input class=\"task-list-item-checkbox\"  type=\"checkbox\"> 视差滚动——不打算做</li>\n</ul>\n<p>第三条不是可选项。动效在一部分人身上会引发前庭不适，这是无障碍要求，不是偏好设置。</p>\n<h2 id=\"那么这个坏决定值不值\">那么这个坏决定值不值<a class=\"hlink\" href=\"#那么这个坏决定值不值\" aria-label=\"本节链接\">#</a></h2>\n<p>如果目标是尽快把文章发出去，用 Hugo，半小时搞定，不要多想。</p>\n<p>如果排版本身也是想做的事情之一——那么自己写一套的理由就成立了。中西文混排、标点、着重号、字体子集，这些都不是配置项能解决的，它们要求你对内容处理的每一步都有控制权。</p>\n<p>代价是要自己维护。收益是，页面上每一处细节都是有理由的。</p>\n"
  },
  {
   "id": "https://ruuo.de/posts/chinese-web-typography/",
   "url": "https://ruuo.de/posts/chinese-web-typography/",
   "title": "中文网页排版里，那些一眼看不出但一直在起作用的细节",
   "summary": "中西文之间的那点空隙、标点该不该占满一个字宽、强调用什么形式——这些细节单看都微不足道，叠加起来却决定了一页中文的成色。",
   "date_published": "2026-08-13T16:00:00Z",
   "tags": [
    "排版",
    "设计"
   ],
   "content_html": "<p>一段中文读起来舒不舒服，通常不取决于用了什么字体。字体是最容易被讨论、也最容易被高估的一环。真正在起作用的，是一批读者永远不会主动注意、但只要做错就会持续制造摩擦的细节。</p>\n<p>它们的共同点是：做对了没人夸，做错了没人说得上来哪里不对，只会觉得“这个网站看着有点累”。</p>\n<h2 id=\"中西文之间的那道缝\">中西文之间的那道缝<a class=\"hlink\" href=\"#中西文之间的那道缝\" aria-label=\"本节链接\">#</a></h2>\n<p>汉字是方块字，字面几乎撑满字身框；拉丁字母是比例字，两侧天然带留白。这两种字挨在一起时，视觉上会挤成一团。</p>\n<p>传统排版对此有明确规定：中西文之间加约四分之一个汉字宽的空隙<label class=\"sn-toggle\" for=\"sn-1\" id=\"fnref1\" role=\"doc-noteref\" aria-label=\"注释 1\">1</label><small>（这个空隙的学名很多：中西文混排间距、盘古之白、hanzi-western spacing。宽度在四分之一到八分之一汉字宽之间取值，取决于正文字号和字体的字面率。）</small>。这条规则在纸质书里执行了几十年，到了网页上却大面积失传，因为它需要在文本里插入一点东西，而大多数内容管理系统不提供这个能力。</p>\n<p>常见的做法是直接插入空格字符。这解决了视觉问题，却制造了新问题：读者复制这段文字时，会带出一堆本不属于原文的空格。</p>\n<div class=\"callout callout-note\"><p class=\"callout-title\">更干净的做法</p>\n<p>不插入字符，而是插入一个宽度为零、但左右带外边距的空元素。视觉上有间距，文本内容里却什么都没多出来。复制粘贴出去的是干净的原文。</p>\n</div>\n<p>这个站上你现在读到的每一处中西文交界，都由构建期的一遍扫描处理过。<code>Claude Code</code> 和 <code>Hugo</code> 这样的词紧贴汉字时，缝隙是自动补上的——包括它们被包在链接或者行内代码里的时候，那种跨标签的接缝最容易漏掉。</p>\n<h2 id=\"标点不该总是占满一个字宽\">标点不该总是占满一个字宽<a class=\"hlink\" href=\"#标点不该总是占满一个字宽\" aria-label=\"本节链接\">#</a></h2>\n<p>全角标点在字体里的设计宽度是一个完整汉字。这在竖排和格稿纸时代是对的，因为每个格子必须等宽。</p>\n<p>但横排正文里，连续出现两个标点时，两个字宽的空白会在版面上撕开一个洞：</p>\n<blockquote>\n<p>他问：“你确定吗？”——没有人回答。</p>\n</blockquote>\n<p>这里的 <code>？</code>、<code>&quot;</code>、<code>——</code> 连成一片，如果每个都占满字宽，这一行会明显比其他行松。专业排版的处理叫<strong>标点挤压</strong>：根据前后文压缩标点的实际占位。</p>\n<p>现在的浏览器已经支持这件事，只需要一行样式：</p>\n<figure class=\"code\" data-lang=\"css\" data-lines=\"3\"><figcaption class=\"code-head\"><span class=\"code-title\">正文的标点挤压</span><span class=\"code-lang\">CSS</span></figcaption><pre><code class=\"lang-css\"><span class=\"t-nt\">body</span><span class=\"t-w\"> </span><span class=\"t-p\">{</span>\n<span class=\"t-w\">  </span><span class=\"t-n\">text-spacing-trim</span><span class=\"t-p\">:</span><span class=\"t-w\"> </span><span class=\"t-n\">trim-start</span><span class=\"t-p\">;</span>\n<span class=\"t-p\">}</span>\n</code></pre></figure>\n<p>对应地，还有一条<strong>避头尾</strong>规则：句号、逗号、右引号不能出现在行首，左引号不能出现在行尾。CSS 的 <code>line-break: strict</code> 会处理大部分情况。</p>\n<h2 id=\"中文没有斜体\">中文没有斜体<a class=\"hlink\" href=\"#中文没有斜体\" aria-label=\"本节链接\">#</a></h2>\n<p>这是被误用得最多的一条。</p>\n<p><code>&lt;em&gt;</code> 在浏览器里默认渲染成斜体。拉丁字母有专门设计的斜体字形，笔画结构会随之改变，不只是倾斜。汉字没有。浏览器遇到中文的 <code>&lt;em&gt;</code>，只能把正体字形做几何倾斜，结果是笔画粗细失衡、重心歪斜。</p>\n<p>中文强调的传统形式是<strong>着重号</strong>——在字的下方（横排）或右侧（竖排）加点。国家标准里有明确规定<label class=\"sn-toggle\" for=\"sn-2\" id=\"fnref2\" role=\"doc-noteref\" aria-label=\"注释 2\">2</label><small>（GB/T 15834《标点符号用法》。着重号的位置在横排时位于字的下方。）</small>。</p>\n<p>问题在于，CSS 判断不了一个 <code>&lt;em&gt;</code> 里装的是汉字还是拉丁字母。所以这件事必须在生成页面的时候做：扫一遍文档结构，含中文的强调标一个记号，样式再据此分流。</p>\n<table>\n<thead>\n<tr>\n<th>内容</th>\n<th>呈现方式</th>\n<th>原因</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>中文强调</td>\n<td>着重号</td>\n<td>汉字没有斜体设计</td>\n</tr>\n<tr>\n<td>西文强调</td>\n<td>斜体</td>\n<td>有专门设计的斜体字形</td>\n</tr>\n<tr>\n<td>混排强调</td>\n<td>着重号</td>\n<td>以主体文字为准</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"行长与行距\">行长与行距<a class=\"hlink\" href=\"#行长与行距\" aria-label=\"本节链接\">#</a></h2>\n<p>中文的合理行长比西文短。西文常用的经验值是每行 65 到 75 个字符，换算成汉字大约是 32 到 38 个字。超过这个范围，眼睛从行尾跳回下一行行首时容易串行。</p>\n<p>行距则相反，中文需要得更多。汉字字面大、笔画密，字与字之间没有西文那样天然的疏密节奏，行距不足会让整段糊成一片。西文正文常用 1.5 到 1.6 倍，中文建议 1.75 到 1.9 倍。</p>\n<h2 id=\"这些细节值多少\">这些细节值多少<a class=\"hlink\" href=\"#这些细节值多少\" aria-label=\"本节链接\">#</a></h2>\n<p>单独看，每一条都微不足道。四分之一个字宽的空隙、几个标点的挤压、强调用点还是用斜——任何一条都不足以让读者说出“这个页面排得好”。</p>\n<p>但它们是叠加的。一页中文里有几百个中西文交界、几十处标点、若干处强调。全部做对和全部不做，读完一篇长文之后的疲劳程度是能感觉出来的——尽管读者说不清原因。</p>\n<p>排版做到位的标志，从来不是被夸奖，而是被忘记。</p>\n\n\n"
  }
 ]
}