<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>ruuo</title>
<link>https://ruuo.de/</link>
<description>ruuo 的个人站点。</description>
<language>zh-Hans</language>
<atom:link href="https://ruuo.de/rss.xml" rel="self" type="application/rss+xml"/>
<lastBuildDate>Tue, 18 Aug 2026 18:05:29 +0000</lastBuildDate>
<item>
<title>少写一点</title>
<link>https://ruuo.de/posts/on-writing-less/</link>
<guid isPermaLink="true">https://ruuo.de/posts/on-writing-less/</guid>
<pubDate>Sun, 16 Aug 2026 16:00:00 +0000</pubDate>
<description>写得少不是产量问题，是筛选问题。真正值得写下来的事情，本来就不多。</description>
<category>随笔</category>
<content:encoded><![CDATA[<p>更新频率是个陷阱。</p>
<p>一旦开始按周或者按月给自己派任务，写作的驱动力就从“这件事想清楚了”变成了“这周该发了”。前者产出的东西有密度，后者产出的东西有字数。</p>
<p>问题在于，只有作者本人能分辨这两者的区别。读者看到的都是一篇文章，标题一样体面，段落一样整齐。差别要读到第三段才显出来——想清楚了的那篇会持续给出新信息，凑数的那篇在原地重复第一段已经说完的话。</p>
<div class="callout callout-pull">
<p>写得慢不是因为敲字慢，是因为想清楚一件事本来就慢。</p>
</div>
<p>值得写下来的事情有个粗糙的判据：<strong>你能不能说出一句别人没说过的话</strong>。不是观点更激进，也不是措辞更漂亮，而是确实提供了一个新的角度或者一条新的信息。</p>
<p>按这个标准筛，大部分选题会被筛掉。这是正常的。一年里真正想明白的事情，本来也就那么几件。</p>
<p>剩下的时间不写，不是浪费。是在攒。</p>
]]></content:encoded>
</item>
<item>
<title>为什么这个博客没有用现成的生成器</title>
<link>https://ruuo.de/posts/building-this-site/</link>
<guid isPermaLink="true">https://ruuo.de/posts/building-this-site/</guid>
<pubDate>Sat, 15 Aug 2026 16:00:00 +0000</pubDate>
<description>现成的静态站点生成器足够好，但它们都不解决中文排版和字体体积这两个问题。这篇记录从零写一套的取舍过程。</description>
<category>工程</category>
<category>排版</category>
<content:encoded><![CDATA[<p>选择自己写一套静态站点生成器，通常是个坏决定。Hugo、Astro、Eleventy 都成熟、快、生态完整，而自己写的那套永远缺功能、有 bug、只有一个用户。</p>
<p>所以值得先说清楚：什么情况下这个坏决定会变成对的。</p>
<h2 id="现成方案不解决的两件事">现成方案不解决的两件事<a class="hlink" href="#现成方案不解决的两件事" aria-label="本节链接">#</a></h2>
<p><strong>第一件是中文排版。</strong> 主流生成器的排版能力围绕拉丁文字构建。中西文间距、标点挤压、着重号、避头尾——这些要么没有，要么依赖运行时的 JavaScript 在页面加载后再改一遍 DOM。后者意味着读者会先看到没处理过的版面，然后眼睁睁看着文字跳动一次。</p>
<p>这类工作本该在构建期完成。文章内容是静态的，处理结果也是静态的，没有任何理由推迟到读者的浏览器里做。</p>
<p><strong>第二件是中文字体。</strong> 思源黑体覆盖三万余字，原始文件接近 17 MB。这个体积不可能直接上生产。</p>
<p>通行的做法是按 Unicode 区段切成上百个分片，浏览器按需下载。这是个聪明的方案，但它把字体托管绑定到了特定的 CDN 上。</p>
<p>另一条路是：既然文章内容在构建时就完全确定，那么全站到底用到哪些字也完全确定。只把这些字打进字体文件即可。</p>
<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>
<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>
    <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>

<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>
<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>
<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>
</code></pre></figure>
<p>这套站点上的实际效果：</p>
<table>
<thead>
<tr>
<th>字体</th>
<th>原始</th>
<th>子集后</th>
<th>压缩比</th>
</tr>
</thead>
<tbody>
<tr>
<td>思源黑体</td>
<td>16.9 MB</td>
<td>约 110 KB</td>
<td>99.4%</td>
</tr>
<tr>
<td>Inter</td>
<td>859 KB</td>
<td>约 92 KB</td>
<td>89.3%</td>
</tr>
<tr>
<td>JetBrains Mono</td>
<td>297 KB</td>
<td>约 41 KB</td>
<td>86.2%</td>
</tr>
</tbody>
</table>
<p>子集化本身不难，难的是三条不能破的底线：</p>
<dl>
<dt>变量轴</dt>
<dd>子集之后 <code>wght</code>、<code>opsz</code> 轴必须还在。否则一套字体退化成若干静态字重，体积反而更大，也失去了大字紧、小字松的光学补偿。</dd>
<dt>排版特性</dt>
<dd><code>palt</code>、<code>halt</code> 决定标点能否挤压，<code>kern</code>、<code>calt</code> 决定西文的字偶距与连字。子集化工具默认会丢掉大部分 OpenType 特性，必须显式保留。</dd>
<dt>系统兜底</dt>
<dd>字体栈里始终留着 <code>PingFang SC</code>、<code>Microsoft YaHei</code> 这一层。万一某个字没进子集，读者看到的只是字形略有出入，而不是一个方框。</dd>
</dl>
<h2 id="一个容易踩的坑">一个容易踩的坑<a class="hlink" href="#一个容易踩的坑" aria-label="本节链接">#</a></h2>
<p>子集是按用字算的，用字变了，文件内容就变了，内容指纹跟着变，缓存全部失效。也就是说，每发一篇新文章，所有回访读者都要重新下载一遍中文字体。</p>
<p>解决办法是让字符集<strong>只增不减</strong>：把历次构建用到的字累积记在一个账本里，删掉旧文章不会让字体缩小。</p>
<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>
</code></pre></figure>
<p>这样文章写到二三十篇之后，新增文章几乎不会引入新字，字体指纹自然稳定下来，缓存长期有效。</p>
<h2 id="关于动效的克制">关于动效的克制<a class="hlink" href="#关于动效的克制" aria-label="本节链接">#</a></h2>
<p>页面上有几处动效：导航栏滚动时从透明变成毛玻璃，内容分节在进入视口时轻微上浮，页面之间跳转时标题会形变。</p>
<p>它们有两个共同点。</p>
<p>其一，全部由 CSS 完成。滚动驱动动画用的是 <code>animation-timeline</code>，页面切换用的是 View Transitions，没有一个滚动监听器。这不只是代码量的问题——JavaScript 驱动的滚动动画会和主线程抢时间，页面一忙就掉帧，而 CSS 的滚动时间轴跑在合成器上，不受影响。</p>
<p>其二，位移量都很小。入场上浮只有 18 像素。</p>
<ul class="contains-task-list">
<li class="task-list-item enabled"><input class="task-list-item-checkbox" checked="checked"  type="checkbox"> 缓动曲线用真实的阻尼弹簧解算，不用 <code>ease-out</code></li>
<li class="task-list-item enabled"><input class="task-list-item-checkbox" checked="checked"  type="checkbox"> 单次动效时长控制在 400 毫秒以内</li>
<li class="task-list-item enabled"><input class="task-list-item-checkbox" checked="checked"  type="checkbox"> <code>prefers-reduced-motion</code> 下全部关闭</li>
<li class="task-list-item enabled"><input class="task-list-item-checkbox"  type="checkbox"> 视差滚动——不打算做</li>
</ul>
<p>第三条不是可选项。动效在一部分人身上会引发前庭不适，这是无障碍要求，不是偏好设置。</p>
<h2 id="那么这个坏决定值不值">那么这个坏决定值不值<a class="hlink" href="#那么这个坏决定值不值" aria-label="本节链接">#</a></h2>
<p>如果目标是尽快把文章发出去，用 Hugo，半小时搞定，不要多想。</p>
<p>如果排版本身也是想做的事情之一——那么自己写一套的理由就成立了。中西文混排、标点、着重号、字体子集，这些都不是配置项能解决的，它们要求你对内容处理的每一步都有控制权。</p>
<p>代价是要自己维护。收益是，页面上每一处细节都是有理由的。</p>
]]></content:encoded>
</item>
<item>
<title>中文网页排版里，那些一眼看不出但一直在起作用的细节</title>
<link>https://ruuo.de/posts/chinese-web-typography/</link>
<guid isPermaLink="true">https://ruuo.de/posts/chinese-web-typography/</guid>
<pubDate>Thu, 13 Aug 2026 16:00:00 +0000</pubDate>
<description>中西文之间的那点空隙、标点该不该占满一个字宽、强调用什么形式——这些细节单看都微不足道，叠加起来却决定了一页中文的成色。</description>
<category>排版</category>
<category>设计</category>
<content:encoded><![CDATA[<p>一段中文读起来舒不舒服，通常不取决于用了什么字体。字体是最容易被讨论、也最容易被高估的一环。真正在起作用的，是一批读者永远不会主动注意、但只要做错就会持续制造摩擦的细节。</p>
<p>它们的共同点是：做对了没人夸，做错了没人说得上来哪里不对，只会觉得“这个网站看着有点累”。</p>
<h2 id="中西文之间的那道缝">中西文之间的那道缝<a class="hlink" href="#中西文之间的那道缝" aria-label="本节链接">#</a></h2>
<p>汉字是方块字，字面几乎撑满字身框；拉丁字母是比例字，两侧天然带留白。这两种字挨在一起时，视觉上会挤成一团。</p>
<p>传统排版对此有明确规定：中西文之间加约四分之一个汉字宽的空隙<label class="sn-toggle" for="sn-1" id="fnref1" role="doc-noteref" aria-label="注释 1">1</label><small>（这个空隙的学名很多：中西文混排间距、盘古之白、hanzi-western spacing。宽度在四分之一到八分之一汉字宽之间取值，取决于正文字号和字体的字面率。）</small>。这条规则在纸质书里执行了几十年，到了网页上却大面积失传，因为它需要在文本里插入一点东西，而大多数内容管理系统不提供这个能力。</p>
<p>常见的做法是直接插入空格字符。这解决了视觉问题，却制造了新问题：读者复制这段文字时，会带出一堆本不属于原文的空格。</p>
<div class="callout callout-note"><p class="callout-title">更干净的做法</p>
<p>不插入字符，而是插入一个宽度为零、但左右带外边距的空元素。视觉上有间距，文本内容里却什么都没多出来。复制粘贴出去的是干净的原文。</p>
</div>
<p>这个站上你现在读到的每一处中西文交界，都由构建期的一遍扫描处理过。<code>Claude Code</code> 和 <code>Hugo</code> 这样的词紧贴汉字时，缝隙是自动补上的——包括它们被包在链接或者行内代码里的时候，那种跨标签的接缝最容易漏掉。</p>
<h2 id="标点不该总是占满一个字宽">标点不该总是占满一个字宽<a class="hlink" href="#标点不该总是占满一个字宽" aria-label="本节链接">#</a></h2>
<p>全角标点在字体里的设计宽度是一个完整汉字。这在竖排和格稿纸时代是对的，因为每个格子必须等宽。</p>
<p>但横排正文里，连续出现两个标点时，两个字宽的空白会在版面上撕开一个洞：</p>
<blockquote>
<p>他问：“你确定吗？”——没有人回答。</p>
</blockquote>
<p>这里的 <code>？</code>、<code>&quot;</code>、<code>——</code> 连成一片，如果每个都占满字宽，这一行会明显比其他行松。专业排版的处理叫<strong>标点挤压</strong>：根据前后文压缩标点的实际占位。</p>
<p>现在的浏览器已经支持这件事，只需要一行样式：</p>
<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>
<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>
<span class="t-p">}</span>
</code></pre></figure>
<p>对应地，还有一条<strong>避头尾</strong>规则：句号、逗号、右引号不能出现在行首，左引号不能出现在行尾。CSS 的 <code>line-break: strict</code> 会处理大部分情况。</p>
<h2 id="中文没有斜体">中文没有斜体<a class="hlink" href="#中文没有斜体" aria-label="本节链接">#</a></h2>
<p>这是被误用得最多的一条。</p>
<p><code>&lt;em&gt;</code> 在浏览器里默认渲染成斜体。拉丁字母有专门设计的斜体字形，笔画结构会随之改变，不只是倾斜。汉字没有。浏览器遇到中文的 <code>&lt;em&gt;</code>，只能把正体字形做几何倾斜，结果是笔画粗细失衡、重心歪斜。</p>
<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>
<p>问题在于，CSS 判断不了一个 <code>&lt;em&gt;</code> 里装的是汉字还是拉丁字母。所以这件事必须在生成页面的时候做：扫一遍文档结构，含中文的强调标一个记号，样式再据此分流。</p>
<table>
<thead>
<tr>
<th>内容</th>
<th>呈现方式</th>
<th>原因</th>
</tr>
</thead>
<tbody>
<tr>
<td>中文强调</td>
<td>着重号</td>
<td>汉字没有斜体设计</td>
</tr>
<tr>
<td>西文强调</td>
<td>斜体</td>
<td>有专门设计的斜体字形</td>
</tr>
<tr>
<td>混排强调</td>
<td>着重号</td>
<td>以主体文字为准</td>
</tr>
</tbody>
</table>
<h2 id="行长与行距">行长与行距<a class="hlink" href="#行长与行距" aria-label="本节链接">#</a></h2>
<p>中文的合理行长比西文短。西文常用的经验值是每行 65 到 75 个字符，换算成汉字大约是 32 到 38 个字。超过这个范围，眼睛从行尾跳回下一行行首时容易串行。</p>
<p>行距则相反，中文需要得更多。汉字字面大、笔画密，字与字之间没有西文那样天然的疏密节奏，行距不足会让整段糊成一片。西文正文常用 1.5 到 1.6 倍，中文建议 1.75 到 1.9 倍。</p>
<h2 id="这些细节值多少">这些细节值多少<a class="hlink" href="#这些细节值多少" aria-label="本节链接">#</a></h2>
<p>单独看，每一条都微不足道。四分之一个字宽的空隙、几个标点的挤压、强调用点还是用斜——任何一条都不足以让读者说出“这个页面排得好”。</p>
<p>但它们是叠加的。一页中文里有几百个中西文交界、几十处标点、若干处强调。全部做对和全部不做，读完一篇长文之后的疲劳程度是能感觉出来的——尽管读者说不清原因。</p>
<p>排版做到位的标志，从来不是被夸奖，而是被忘记。</p>


]]></content:encoded>
</item>
</channel>
</rss>