<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>此间札记</title><description>一个把排版当回事、同时写给人和 AI 读的博客。</description><link>https://xiaoxusop.github.io/letterpress/</link><language>zh-CN</language><copyright>© XIAOXUsop</copyright><atom:link href="https://xiaoxusop.github.io/letterpress/rss.xml" rel="self" type="application/rss+xml"/><item><title>中文网页排版，多数主题都做错了</title><link>https://xiaoxusop.github.io/letterpress/cjk-web-typography/</link><guid isPermaLink="true">https://xiaoxusop.github.io/letterpress/cjk-web-typography/</guid><description>行高、字重、行宽、中西文间距——这四个参数直接套西文的值，中文读者看到的是一份「能读但别扭」的页面。2026 年有三个原生 CSS 属性可以修好其中一半。</description><pubDate>Sat, 12 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;中文网页排版有个奇怪的现象：&lt;strong&gt;大家都觉得别扭，但很少有人能指出哪里别扭。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;原因多半是这四个参数被直接沿用了西文的值。它们单独看都不致命，
叠在一起就是那种说不清的「不对劲」。&lt;/p&gt;
&lt;h2 id=&quot;一行高&quot;&gt;一、行高&lt;a class=&quot;heading-anchor&quot; href=&quot;#一行高&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;西文正文的常用行高是 1.4 到 1.5。这个值搬到中文上会显得挤。&lt;/p&gt;
&lt;p&gt;因为汉字是&lt;strong&gt;方块字&lt;/strong&gt;：笔画在字面内铺得很满，字与字之间没有西文那种
由升降部带来的天然空隙。同样行高下，中文的行间留白视觉上更少。&lt;/p&gt;
&lt;p&gt;中文正文的行高应当在 &lt;strong&gt;1.6 到 1.8&lt;/strong&gt; 之间。本站用的是 1.75。&lt;/p&gt;
&lt;p&gt;还有个细节：行高要用&lt;strong&gt;无单位数值&lt;/strong&gt;（&lt;code&gt;line-height: 1.75&lt;/code&gt;），不要用 &lt;code&gt;px&lt;/code&gt; 或 &lt;code&gt;em&lt;/code&gt;。
无单位值是相对于元素自身字号计算的，继承时不会出乱子。&lt;/p&gt;
&lt;h2 id=&quot;二字重&quot;&gt;二、字重&lt;a class=&quot;heading-anchor&quot; href=&quot;#二字重&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;这条最容易被忽略，但效果立竿见影。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;很多中文字体只有两个真实字重：400 和 700。&lt;/strong&gt; 苹方有 Ultralight 到 Semibold，
思源黑体有 300 到 900 的完整字重；微软雅黑主要就是 400 和 700。
（Apple 官方系统字体清单里列有 &lt;code&gt;PingFang SC Semibold&lt;/code&gt;，
见 &lt;a href=&quot;https://developer.apple.com/fonts/system-fonts/&quot;&gt;System Fonts&lt;/a&gt;。）&lt;/p&gt;
&lt;p&gt;这里的通行说法是「写 600 会触发伪合成，所以只能写 500」。&lt;strong&gt;这话只说对了一半。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;伪合成的开关不是字重数值，而是 &lt;strong&gt;&lt;code&gt;font-synthesis-weight&lt;/code&gt;&lt;/strong&gt;。至于字重本身——
写一个字体没有的字重时，规范要求的是&lt;strong&gt;去找最接近的真实字重&lt;/strong&gt;，不是伪造一个：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When a weight is specified for which no face exists, a face with a
nearby weight is used.
—— &lt;a href=&quot;https://www.w3.org/TR/2026/WD-css-fonts-4-20260913/#font-style-matching&quot;&gt;CSS Fonts Level 4 §2.2.2&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;「最接近」具体朝哪边找，规范给了确定规则&lt;/strong&gt;（§5 字重匹配）：
请求的字重&lt;strong&gt;大于 500&lt;/strong&gt; 时，先按升序检查&lt;strong&gt;不小于&lt;/strong&gt;目标的字重
（600 → 700 → 800 → 900），都找不到再向下找。于是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;苹方这类&lt;strong&gt;确有 600&lt;/strong&gt; 的字体 → 请求 600 &lt;strong&gt;直接命中 600&lt;/strong&gt;，不会跳到 700；&lt;/li&gt;
&lt;li&gt;微软雅黑这类&lt;strong&gt;只有 400/700&lt;/strong&gt; 的字体 → 请求 600 先查 700，&lt;strong&gt;落到 700&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两条路径&lt;strong&gt;都不会伪造粗体&lt;/strong&gt;（前提是没开伪合成）：开着合成时它才会把 400
算法加粗——笔画发虚、字形走样，而且每个浏览器合成得还不一样。&lt;/p&gt;
&lt;p&gt;本站的做法是把合成直接关掉：&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-dimmed&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#22272e;color:#24292e;--shiki-dark:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A;--shiki-dark:#8DDB8C&quot;&gt;html&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt;  font-synthesis-weight&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt;none&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是标题可以写 &lt;code&gt;font-weight: 600&lt;/code&gt;：拉丁部分拿到 Archivo 的真实 600，
中文部分落到&lt;strong&gt;自己字体里最接近 600 的真实字重&lt;/strong&gt;（苹方约 600、雅黑 700），
两者视觉重量相当。
（反过来，写 500 的话中文更容易掉到 &lt;strong&gt;400&lt;/strong&gt;——比正文还轻，标题反而立不住。）&lt;/p&gt;
&lt;p&gt;更重要的是：&lt;strong&gt;层级主要靠字号和颜色建立，不要靠字重硬拉。&lt;/strong&gt;
一段 17px/400 的正文和一段 21px/600 的小标题，层级已经足够了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;勘误（2026-09-22）&lt;/strong&gt;：这一节原先写的是「正确做法：标题字重写 500，
让字体自己去挑最接近的真实字重」。&lt;strong&gt;结论和建议都改掉了&lt;/strong&gt;：把伪合成归因到
字重数值本身是错的——开关是 &lt;code&gt;font-synthesis-weight&lt;/code&gt;；而在关掉合成的实现里，
写 500 会让中文落到 400，比 600 更糟。
依据见 &lt;a href=&quot;https://www.w3.org/TR/2026/WD-css-fonts-4-20260913/#font-style-matching&quot;&gt;CSS Fonts Level 4 的字重匹配&lt;/a&gt;
与本站 &lt;code&gt;src/styles/base.css&lt;/code&gt; 的 &lt;code&gt;font-synthesis-weight&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;三行宽&quot;&gt;三、行宽&lt;a class=&quot;heading-anchor&quot; href=&quot;#三行宽&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;这是四个里最麻烦的一个，因为中西文的最优值&lt;strong&gt;在同一个单位下对不齐&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;西文的理想行宽是 45 到 75 个字符，66 是经典值。而 &lt;code&gt;ch&lt;/code&gt; 这个单位
是为西文设计的——按规范，它等于&lt;strong&gt;渲染所用字体&lt;/strong&gt;里「0」字形的宽度：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;ch&lt;/code&gt; — Represents the typical advance measure of &lt;strong&gt;European&lt;/strong&gt; alphanumeric
characters, and measured as the used advance measure of the “0” (ZERO,
U+0030) glyph &lt;strong&gt;in the font used to render it&lt;/strong&gt;.
—— &lt;a href=&quot;https://www.w3.org/TR/2024/WD-css-values-4-20240312/#ch&quot;&gt;CSS Values and Units Level 4&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;两个要点：它是&lt;strong&gt;欧洲字符&lt;/strong&gt;的度量，而且&lt;strong&gt;取决于字体&lt;/strong&gt;。所以 &lt;code&gt;ch&lt;/code&gt; 换算不出一个
跨字体稳定的汉字数——换个字体，「66ch 能排几个汉字」这个数就变了。&lt;/p&gt;
&lt;p&gt;粗估的话，一个汉字约等于两个 &lt;code&gt;ch&lt;/code&gt;，66ch 大约排 &lt;strong&gt;33 个汉字&lt;/strong&gt;。
（顺带一提，这个数其实落在中文舒适的 30–40 字里——但它是粗估，且随字体浮动。）&lt;/p&gt;
&lt;p&gt;本站改用 &lt;code&gt;em&lt;/code&gt;，图的是&lt;strong&gt;可预测&lt;/strong&gt;：&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-dimmed&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#22272e;color:#24292e;--shiki-dark:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#6CB6FF&quot;&gt;.prose&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt;  max-width&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt;34&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;em&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;em&lt;/code&gt; 等于元素的字号，这是规范保证的，且&lt;strong&gt;不依赖字体&lt;/strong&gt;。
但「一个汉字正好 1em 宽」&lt;strong&gt;不是规范保证&lt;/strong&gt;——汉字的 advance 取决于具体字体，
规范为此专门定义了一个 &lt;code&gt;ic&lt;/code&gt; 单位来表示「字体中全角字形的典型 advance」，
并规定在无法确定时&lt;strong&gt;假设为 1em&lt;/strong&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;ic&lt;/code&gt; unit is … a typically exact measure (in the few fonts with proportional
fullwidth glyphs, an approximation) of a single fullwidth glyph’s advance measure.
In the cases where it is impossible or impractical to determine the ideographic
advance measure, it must be assumed to be &lt;code&gt;1em&lt;/code&gt;.
—— &lt;a href=&quot;https://www.w3.org/TR/2024/WD-css-values-4-20240312/#ic&quot;&gt;CSS Values and Units Level 4 §5.1.1&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;这一句本身就说明「容器宽度单位」和「实际字形宽度」不能混为一谈&lt;/strong&gt;——
如果规范能保证两者相等，就不必再定义 &lt;code&gt;ic&lt;/code&gt; 了。&lt;/p&gt;
&lt;p&gt;所以准确的说法是：34em 让内容栏宽度&lt;strong&gt;稳定地跟随字号&lt;/strong&gt;；
对常见全角汉字可&lt;strong&gt;近似&lt;/strong&gt;理解为约 34 字，但&lt;strong&gt;实际每行字数仍受字体、标点、
字距和混排影响&lt;/strong&gt;。于是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;中文：约 34 个字（近似，落在 30–40 的理想区间）✅&lt;/li&gt;
&lt;li&gt;西文：约 68 个字符（近似，落在 45–75 的理想区间）✅&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;一个值同时覆盖两种文字的舒适区，而且容器宽度不随字体变。&lt;/strong&gt; 这是
&lt;code&gt;--measure: 34em&lt;/code&gt; 的由来。&lt;strong&gt;注意被保证的是「宽度稳定」，不是「字数确定」。&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;勘误（2026-09-22）&lt;/strong&gt;：这一节原先写的是「一个汉字大约等于两个 &lt;code&gt;ch&lt;/code&gt;，
所以按 66ch 排中文，每行会排出约 130 个汉字——远超出中文舒适的 30 到 40 字」。
&lt;strong&gt;两处都错&lt;/strong&gt;：按它自己的前提应当除以 2 得 33，而不是乘以 2 得 130；
而 33 本来就落在它自己说的舒适区间内，所以「远超」这个结论也不成立。&lt;/p&gt;
&lt;p&gt;这句话还遮住了一个更关键的事实：&lt;strong&gt;66ch ≈ 33em，与本题采用的 34em 宽度几乎相同&lt;/strong&gt;
（这一步依赖「0 字形约 0.5em」这个假设，是推算而非实测）。
也就是说这个改动换来的是&lt;strong&gt;容器宽度的可预测性&lt;/strong&gt;，不是行宽。
&lt;strong&gt;结论（用 em）不变，论证方式改了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;依据：&lt;a href=&quot;https://www.w3.org/TR/2024/WD-css-values-4-20240312/#ch&quot;&gt;CSS Values and Units Level 4 §ch&lt;/a&gt;、
&lt;a href=&quot;https://www.w3.org/TR/2024/WD-css-values-4-20240312/#ic&quot;&gt;§ic&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;勘误（2026-09-24）&lt;/strong&gt;：上一条勘误之后，本节仍留着
「&lt;code&gt;em&lt;/code&gt; 等于字号、与字体无关。一个汉字约 1em 宽……&lt;strong&gt;而且不随字体变&lt;/strong&gt;」，
并据此把 &lt;code&gt;34em&lt;/code&gt; 说成确定的字数换算。&lt;strong&gt;前半句对，后半句越界了&lt;/strong&gt;：
规范保证的是 &lt;code&gt;em&lt;/code&gt; 等于&lt;strong&gt;计算后的 font-size&lt;/strong&gt;，
不保证&lt;strong&gt;每个汉字的 advance 恒等于 1em&lt;/strong&gt;——所以「34em ≈ 34 字」是近似，
而「&lt;strong&gt;每行字数确定&lt;/strong&gt;」这句话是不成立的。详见上面引的 &lt;code&gt;ic&lt;/code&gt; 那段。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;四中西文间距与标点&quot;&gt;四、中西文间距与标点&lt;a class=&quot;heading-anchor&quot; href=&quot;#四中西文间距与标点&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;中文和英文、数字之间应该有一个小间隙：「共 500 条记录」比「共500条记录」好读。&lt;/p&gt;
&lt;p&gt;过去这件事要靠 &lt;a href=&quot;https://github.com/vinta/pangu.js&quot;&gt;pangu.js&lt;/a&gt; 之类的库在
客户端跑一遍正则，找到边界然后插空格。它有一堆边缘情况（全角标点前后不加、
&lt;code&gt;90°&lt;/code&gt; 不加、&lt;code&gt;15%&lt;/code&gt; 不加），而且要在 DOM 加载后重写文本。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2026 年浏览器原生做了这件事：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-dimmed&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#22272e;color:#24292e;--shiki-dark:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A;--shiki-dark:#8DDB8C&quot;&gt;html&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt;  text-autospace&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt;normal&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt;  text-spacing-trim&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;: trim-start;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;text-autospace: normal&lt;/code&gt; 自动在中西文之间插入约 1/4 个空格。2025 年进入 Baseline。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;text-spacing-trim: trim-start&lt;/code&gt; 做中文标点的字距调整——行首的逗号句号
之类不再占满一格。Chrome / Edge 123+ 已默认开启。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两个前提：&lt;strong&gt;&lt;code&gt;&amp;#x3C;html lang&gt;&lt;/code&gt; 必须设置正确&lt;/strong&gt;，否则浏览器不会应用对应语言的规则；
代码块里要关掉自动间距，因为代码里的空格是有意义的：&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-dimmed&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#22272e;color:#24292e;--shiki-dark:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A;--shiki-dark:#8DDB8C&quot;&gt;pre&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#22863A;--shiki-dark:#8DDB8C&quot;&gt;code&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#22863A;--shiki-dark:#8DDB8C&quot;&gt;kbd&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#22863A;--shiki-dark:#8DDB8C&quot;&gt;samp&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt;text-autospace&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;: no-autospace; }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;只有 Chromium 系支持&lt;/strong&gt;：MDN 的浏览器兼容数据（2026-09-24 查）
记 Chrome 自 123 起支持，&lt;strong&gt;Safari 与 Firefox 均为「不支持」&lt;/strong&gt;——
&lt;code&gt;caniuse&lt;/code&gt; 上还没有这个特性（太新）。
所以它是&lt;strong&gt;渐进增强&lt;/strong&gt;——不支持的浏览器只是少了那一点调整，版式不会坏。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;本项目此前在这里写的是「全球覆盖约 72%」，现已删除。&lt;/strong&gt;
那个数字&lt;strong&gt;没有任何来源&lt;/strong&gt;（caniuse 抓不到、本项目也没实测过），
而它写在一个「实测」味道很重的段落里。
&lt;strong&gt;一个查不到来源的覆盖率，比不写更糟&lt;/strong&gt;——读者会以为它是量出来的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;五一条容易漏的中文没有斜体&quot;&gt;五、一条容易漏的：中文没有斜体&lt;a class=&quot;heading-anchor&quot; href=&quot;#五一条容易漏的中文没有斜体&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;中文字体不提供斜体字形。&lt;code&gt;font-style: italic&lt;/code&gt; 对中文的结果是浏览器
&lt;strong&gt;合成倾斜&lt;/strong&gt;——把笔画整体拉斜，字形变形、笔画糊在一起。&lt;/p&gt;
&lt;p&gt;而 CSS 没法问「这段 &lt;code&gt;&amp;#x3C;em&gt;&lt;/code&gt; 里是中文还是西文」。所以务实的选择是：
&lt;strong&gt;统一把强调处理成加粗&lt;/strong&gt;，这也是中文排版的通行做法。&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-dimmed&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#22272e;color:#24292e;--shiki-dark:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A;--shiki-dark:#8DDB8C&quot;&gt;em&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#22863A;--shiki-dark:#8DDB8C&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt;  font-style&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt;normal&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt;  font-weight&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt;600&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;六字体栈的顺序&quot;&gt;六、字体栈的顺序&lt;a class=&quot;heading-anchor&quot; href=&quot;#六字体栈的顺序&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;最后一条，顺序不能反：&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-dimmed&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#22272e;color:#24292e;--shiki-dark:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A;--shiki-dark:#8DDB8C&quot;&gt;font-family&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;: &apos;Archivo&apos;, -apple-system, &apos;PingFang SC&apos;, &apos;Microsoft YaHei&apos;, &lt;/span&gt;&lt;span style=&quot;color:#22863A;--shiki-dark:#8DDB8C&quot;&gt;sans-serif&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;拉丁字体必须排在前面。&lt;/strong&gt; 如果中文字体排在前面，英文也会用中文字体渲染——
而中文字体里的拉丁字母是等宽的（为了和汉字对齐），排版上很难看。&lt;/p&gt;
&lt;p&gt;顺序对了之后，英文用 Archivo 渲染，汉字自动落到苹方/雅黑，各取所长。&lt;/p&gt;
&lt;h2 id=&quot;那要不要自托管中文字体&quot;&gt;那要不要自托管中文字体&lt;a class=&quot;heading-anchor&quot; href=&quot;#那要不要自托管中文字体&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;默认不要。&lt;/strong&gt; 中文字体单文件 5–20 MB，哪怕做了子集化，一个博客站也不该
为此付出首屏时间。而中文系统字体（苹方、微软雅黑、思源黑体）质量本身就很好——
知乎、Medium 中文版这类大站的内文都用系统字体。&lt;/p&gt;
&lt;p&gt;实在需要自定义时，用 &lt;a href=&quot;https://github.com/KonghaYao/cn-font-split&quot;&gt;cn-font-split&lt;/a&gt;
做子集化，并且务必用 &lt;code&gt;unicode-range&lt;/code&gt; 限定覆盖面。&lt;/p&gt;
&lt;h2 id=&quot;小结&quot;&gt;小结&lt;a class=&quot;heading-anchor&quot; href=&quot;#小结&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;div class=&quot;table-wrap&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;可横向滚动的表格&quot; data-table-wrap=&quot;&quot;&gt;


































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;参数&lt;/th&gt;&lt;th&gt;西文常用&lt;/th&gt;&lt;th&gt;中文应用&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;行高&lt;/td&gt;&lt;td&gt;1.4–1.5&lt;/td&gt;&lt;td&gt;&lt;strong&gt;1.6–1.8&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;标题字重&lt;/td&gt;&lt;td&gt;600–700&lt;/td&gt;&lt;td&gt;&lt;strong&gt;600&lt;/strong&gt;（配 &lt;code&gt;font-synthesis-weight: none&lt;/code&gt;）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;行宽&lt;/td&gt;&lt;td&gt;66ch&lt;/td&gt;&lt;td&gt;&lt;strong&gt;34em&lt;/strong&gt;（≈34 汉字 / ≈68 西文字符，&lt;strong&gt;均为近似&lt;/strong&gt;）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;中西文间距&lt;/td&gt;&lt;td&gt;手动加&lt;/td&gt;&lt;td&gt;&lt;strong&gt;&lt;code&gt;text-autospace&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;强调&lt;/td&gt;&lt;td&gt;斜体&lt;/td&gt;&lt;td&gt;&lt;strong&gt;加粗&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;这些细节单独看都很小。但它们同时出现时，读者的感受就是「这个站读起来舒服」
而说不出为什么——那正是排版该有的样子。&lt;/p&gt;
&lt;p&gt;相关条目：&lt;a href=&quot;/letterpress/wiki/cjk-typography/&quot;&gt;中文排版&lt;/a&gt;、&lt;a href=&quot;/letterpress/wiki/content-negotiation/&quot;&gt;内容协商&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id=&quot;参考&quot;&gt;参考&lt;a class=&quot;heading-anchor&quot; href=&quot;#参考&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/2024/WD-css-values-4-20240312/#ch&quot;&gt;CSS Values and Units Level 4 §ch&lt;/a&gt; —— &lt;code&gt;ch&lt;/code&gt; 的定义
（W3C Working Draft, 12 March 2024）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/2026/WD-css-fonts-4-20260913/#font-style-matching&quot;&gt;CSS Fonts Level 4 §2.2.2 Missing weights&lt;/a&gt; —— 缺少字重时如何匹配&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/2026/WD-css-fonts-4-20260913/#font-synthesis-weight&quot;&gt;CSS Fonts Level 4 §font-synthesis-weight&lt;/a&gt; —— 伪合成的开关
（W3C Working Draft, 13 September 2026）&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么链接带日期而不是 &lt;code&gt;/TR/css-values-4/&lt;/code&gt;&lt;/strong&gt;：后者是”最新版”，
会随规范修订移动——半年后回看，不知道当时引的是哪一版。
W3C 自己推荐的引用方式就是带日期的固定 URL。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;修订记录&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2026-09-12&lt;/strong&gt; 发布。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026-09-22&lt;/strong&gt; 勘误：行宽一节把「一个汉字 ≈ 2 &lt;code&gt;ch&lt;/code&gt;」算反了（应除以 2 得 33，
原写作乘以 2 得 130），连带「远超舒适区」的结论也不成立；字重一节把伪合成
归因到字重数值上，而开关其实是 &lt;code&gt;font-synthesis-weight&lt;/code&gt;，且在关掉合成的实现里
写 500 会让中文落到 400。两处的&lt;strong&gt;结论都保持不变&lt;/strong&gt;（用 &lt;code&gt;em&lt;/code&gt;、标题用 600），
改的是论证与依据。同步修订了 &lt;a href=&quot;/letterpress/wiki/cjk-typography/&quot;&gt;中文排版&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026-09-24&lt;/strong&gt; 勘误：上一轮只改了那句算错的算式，&lt;strong&gt;却留下了更大的越界&lt;/strong&gt;——
「&lt;code&gt;em&lt;/code&gt; 等于字号、与字体无关」被推广成「一个汉字正好 1em 宽、每行字数确定」。
规范保证的只是 &lt;code&gt;em&lt;/code&gt; 等于&lt;strong&gt;计算后的 font-size&lt;/strong&gt;；它为此专门定义了 &lt;code&gt;ic&lt;/code&gt;
来表示全角字形的典型 advance，无法确定时假设 1em。
同时&lt;strong&gt;苹方字重结论与 Apple 官方清单冲突&lt;/strong&gt;（清单里确有 &lt;code&gt;PingFang SC Semibold&lt;/code&gt;），
且「请求 600 必落 700」是条件句：匹配算法对 &gt;500 的请求&lt;strong&gt;先向上找&lt;/strong&gt;，
有真 600 的字体直接命中 600。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>排版</category><category>css</category><category>中文</category><author>XIAOXUsop</author></item><item><title>这个博客是怎么搭的</title><link>https://xiaoxusop.github.io/letterpress/how-this-works/</link><guid isPermaLink="true">https://xiaoxusop.github.io/letterpress/how-this-works/</guid><description>改一个配置文件就能上线，文章放在一个目录里，知识层会自动长出链接图。这一篇解释目录结构、写作流程，以及怎么把它换成你自己的。</description><pubDate>Sat, 12 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这一篇是写给你的——如果你 clone 了这个项目，它会告诉你东西都在哪。&lt;/p&gt;
&lt;h2 id=&quot;你只需要改一个文件&quot;&gt;你只需要改一个文件&lt;a class=&quot;heading-anchor&quot; href=&quot;#你只需要改一个文件&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;src/config.ts&lt;/code&gt;。里面的 &lt;code&gt;site&lt;/code&gt;、&lt;code&gt;author&lt;/code&gt;、&lt;code&gt;nav&lt;/code&gt; 三项改完就能上线：&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-dimmed&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#22272e;color:#24292e;--shiki-dark:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt; site&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#F69D50&quot;&gt; SiteConfig&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;  title: &lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-dark:#96D0FF&quot;&gt;&apos;你的站名&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;  tagline: &lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-dark:#96D0FF&quot;&gt;&apos;一句话说清这个站是什么。&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;  url: &lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-dark:#96D0FF&quot;&gt;&apos;https://example.com&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;,   &lt;/span&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#768390&quot;&gt;// 部署前填，不留斜杠&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;  lang: &lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-dark:#96D0FF&quot;&gt;&apos;zh-CN&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;,                &lt;/span&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#768390&quot;&gt;// 别改错——浏览器的中文标点规则依赖它&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;  author: { name: &lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-dark:#96D0FF&quot;&gt;&apos;你的名字&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;, bio: &lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-dark:#96D0FF&quot;&gt;&apos;一句话介绍自己。&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#768390&quot;&gt;  // …&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;其余全部有可用的默认值，而且默认值本身就是完整站点，不是占位符。&lt;/strong&gt;
你现在看到的这些文章和知识库条目就是示例内容——它们是真的能读的，
不是 &lt;code&gt;lorem ipsum&lt;/code&gt;。删掉它们换成你自己的即可。&lt;/p&gt;
&lt;h2 id=&quot;东西都在哪&quot;&gt;东西都在哪&lt;a class=&quot;heading-anchor&quot; href=&quot;#东西都在哪&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-dimmed&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#22272e;color:#24292e;--shiki-dark:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;src/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── config.ts            你唯一必须改的文件&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── content/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│   ├── posts/           文章。按时间排&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│   └── wiki/            知识层。按主题互链&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── layouts/             页面骨架&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── components/          组件&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── pages/               路由&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── lib/                 纯逻辑（可测试，不依赖 Astro）&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│   ├── negotiate/       Accept 头解析——[[内容协商]] 的实现&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│   └── wiki/            链接图、体检、llms.txt 生成&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;└── styles/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ├── tokens.css       设计令牌&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ├── base.css         基线排版（含 [[中文排版]] 的那些修正）&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    └── site.css         布局与组件&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;两种内容两种组织方式&quot;&gt;两种内容，两种组织方式&lt;a class=&quot;heading-anchor&quot; href=&quot;#两种内容两种组织方式&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;文章&lt;/strong&gt;放在 &lt;code&gt;src/content/posts/&lt;/code&gt;，按时间排列，是「流」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;知识库条目&lt;/strong&gt;放在 &lt;code&gt;src/content/wiki/&lt;/code&gt;，按主题互链，是「网」。&lt;/p&gt;
&lt;p&gt;它们的区别不是格式，是&lt;strong&gt;读者找东西的方式&lt;/strong&gt;：想看你最近写了什么就翻文章，
想搞清某个概念就查知识层。两边的条目地位相同，可以互相引用。&lt;/p&gt;
&lt;h2 id=&quot;用-方括号-连起来&quot;&gt;用 &lt;code&gt;[[方括号]]&lt;/code&gt; 连起来&lt;a class=&quot;heading-anchor&quot; href=&quot;#用-方括号-连起来&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;在任何地方写 &lt;code&gt;[[目标]]&lt;/code&gt;，就会生成一条链接：&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-dimmed&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#22272e;color:#24292e;--shiki-dark:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;markdown&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;见 [[&lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-light-text-decoration:underline;--shiki-dark:#96D0FF;--shiki-dark-text-decoration:inherit&quot;&gt;内容协商&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;]] 一节。&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;支持三种写法：&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;可横向滚动的表格&quot; data-table-wrap=&quot;&quot;&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;写法&lt;/th&gt;&lt;th&gt;效果&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;[[内容协商]]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;链接，显示文本即目标名&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;[[内容协商|那篇]]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;自定义显示文本&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;[[内容协商#为什么]]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;指向页内某小节&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;目标可以写 slug，也可以写标题——你大概不会记得每篇的 slug 是什么。
解析时&lt;strong&gt;大小写和空格都不敏感&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;写错了不会静默失败：构建时会报一条 &lt;code&gt;broken-wikilink&lt;/code&gt; 错误，
并告诉你&lt;strong&gt;哪个页面引用了什么&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=&quot;构建时会做体检&quot;&gt;构建时会做体检&lt;a class=&quot;heading-anchor&quot; href=&quot;#构建时会做体检&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;每次构建会跑一遍知识库体检，输出在控制台。分三级：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;错误&lt;/strong&gt;：断链、两个页面抢同一个 URL、文章占用 &lt;code&gt;/about/&lt;/code&gt; 等系统路由、
引用了没登记的来源、复核状态与正文对不上。
&lt;strong&gt;会让构建失败&lt;/strong&gt;（可在配置里关掉）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;警告&lt;/strong&gt;：孤儿页（没有任何页面指向它）、缺摘要、空正文、
同一关系在正文与 &lt;code&gt;related&lt;/code&gt; 里各写了一次&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提示&lt;/strong&gt;：摘要过长之类的建议&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;这不是可选的装饰。&lt;/strong&gt; 一个由人（或 agent）持续维护的知识库，
最容易的结局是慢慢烂掉——改了标题没改引用、删了页面留下断链。
机械的检查必须能进 CI，否则「知识库」会退化成「一堆文件」。&lt;/p&gt;
&lt;p&gt;完整的规则表在 &lt;code&gt;AGENTS.md&lt;/code&gt;——那份是&lt;strong&gt;写给 agent 看的&lt;/strong&gt;，改规则时两边要一起改。&lt;/p&gt;
&lt;h2 id=&quot;给-ai-读者那一份&quot;&gt;给 AI 读者那一份&lt;a class=&quot;heading-anchor&quot; href=&quot;#给-ai-读者那一份&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;这一站有两个出口，服务两类读者：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Markdown 孪生文件。&lt;/strong&gt; 每篇文章和知识页都有可直接访问的 &lt;code&gt;.md&lt;/code&gt; URL。
在支持内容协商的部署平台上，请求页面 URL 时带上 &lt;code&gt;Accept: text/markdown&lt;/code&gt;
也会得到 markdown；本站使用 GitHub Pages，因此应直接访问 &lt;code&gt;.md&lt;/code&gt; 链接。
原理和托管限制见 &lt;a href=&quot;/letterpress/wiki/content-negotiation/&quot;&gt;内容协商&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;目录与全文。&lt;/strong&gt; &lt;code&gt;/llms.txt&lt;/code&gt; 是给 agent 看的目录（标题 + 一句话说明），
&lt;code&gt;/llms-full.txt&lt;/code&gt; 是全文内联版。每个页面还有 &lt;code&gt;.md&lt;/code&gt; 孪生文件，
比如 &lt;code&gt;/how-this-works.md&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;需要说明的是：&lt;strong&gt;&lt;code&gt;llms.txt&lt;/code&gt; 的实际效果被高估了&lt;/strong&gt;——
Ahrefs 实测 13.7 万个域名里 97% 的 llms.txt 从未被请求过。
本站生成它是因为零成本，但主要依靠显式 &lt;code&gt;.md&lt;/code&gt; 链接、内容清单和干净的 HTML。&lt;/p&gt;
&lt;h2 id=&quot;部署&quot;&gt;部署&lt;a class=&quot;heading-anchor&quot; href=&quot;#部署&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;静态产物，任何静态托管都能跑。但&lt;strong&gt;内容协商需要一层边缘函数&lt;/strong&gt;——
因为静态托管不解析请求头，它只会把文件原样吐出来。&lt;/p&gt;
&lt;p&gt;项目里带了三个平台的现成实现，&lt;strong&gt;选你用的那个&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;可横向滚动的表格&quot; data-table-wrap=&quot;&quot;&gt;
























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;平台&lt;/th&gt;&lt;th&gt;边缘函数&lt;/th&gt;&lt;th&gt;平台配置&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Cloudflare Pages&lt;/td&gt;&lt;td&gt;&lt;code&gt;functions/[[path]].ts&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;public/_headers&lt;/code&gt;（会被拷进产物）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Netlify&lt;/td&gt;&lt;td&gt;&lt;code&gt;netlify/edge-functions/negotiate.ts&lt;/code&gt;&lt;/td&gt;&lt;td&gt;无额外配置&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Vercel&lt;/td&gt;&lt;td&gt;&lt;code&gt;middleware.ts&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;vercel.json&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;不想要这一层也没关系，站点照常工作，只是 agent 拿到的是 HTML 而不是 markdown。&lt;/p&gt;
&lt;h2 id=&quot;详细说明&quot;&gt;详细说明&lt;a class=&quot;heading-anchor&quot; href=&quot;#详细说明&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;排版与设计决策：&lt;a href=&quot;/letterpress/wiki/cjk-typography/&quot;&gt;中文排版&lt;/a&gt;、&lt;a href=&quot;/letterpress/wiki/design-tokens/&quot;&gt;设计令牌&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;知识层的设计取舍：&lt;a href=&quot;/letterpress/wiki/llm-wiki/&quot;&gt;LLM 知识库&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>使用说明</category><author>XIAOXUsop</author></item><item><title>你的博客该不该给 AI 一份 markdown</title><link>https://xiaoxusop.github.io/letterpress/markdown-for-agents/</link><guid isPermaLink="true">https://xiaoxusop.github.io/letterpress/markdown-for-agents/</guid><description>内容协商是 HTTP 自带的能力，但博客圈几乎没人用。三个 agent 会主动要 markdown，四个不会——而现有工具都跳过了最难的那一步。</description><pubDate>Sat, 12 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;一个 AI agent 抓取你的网页时，看到的东西和你看到的不一样。它看到的是两万个
token 的导航栏、页脚和 &lt;code&gt;&amp;#x3C;div&gt;&lt;/code&gt; 嵌套，而正文可能只占其中百分之几。&lt;/p&gt;
&lt;p&gt;这不是比喻。Cloudflare 实测自己的文档：同一页 HTML 要 16,180 token，
markdown 版只要 3,150 token——&lt;strong&gt;省 80%，内容一模一样&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;HTTP 从 1.1 起就有解决这个问题的机制：内容协商。客户端在 &lt;code&gt;Accept&lt;/code&gt; 头里说
「我更想要 markdown，不行的话 HTML 也行」，服务端照办。&lt;/p&gt;
&lt;p&gt;问题是：&lt;strong&gt;博客圈几乎没人用。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;谁在要-markdown&quot;&gt;谁在要 markdown&lt;a class=&quot;heading-anchor&quot; href=&quot;#谁在要-markdown&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;拿七个常见 agent 去请求 &lt;code&gt;httpbin.org/headers&lt;/code&gt;，
看它们各自发出的 &lt;code&gt;Accept&lt;/code&gt; 头。结果是这样的：&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;可横向滚动的表格&quot; data-table-wrap=&quot;&quot;&gt;












































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Agent&lt;/th&gt;&lt;th&gt;要 markdown 吗&lt;/th&gt;&lt;th&gt;Accept 头&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Claude Code&lt;/td&gt;&lt;td&gt;是&lt;/td&gt;&lt;td&gt;&lt;code&gt;text/markdown, text/html, */*&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cursor&lt;/td&gt;&lt;td&gt;是&lt;/td&gt;&lt;td&gt;&lt;code&gt;text/markdown&lt;/code&gt;, &lt;code&gt;text/html;q=0.9&lt;/code&gt;, …&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;OpenCode&lt;/td&gt;&lt;td&gt;是&lt;/td&gt;&lt;td&gt;&lt;code&gt;text/markdown;q=1.0&lt;/code&gt;, &lt;code&gt;text/x-markdown;q=0.9&lt;/code&gt;, …&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;OpenAI Codex&lt;/td&gt;&lt;td&gt;否&lt;/td&gt;&lt;td&gt;&lt;code&gt;text/html&lt;/code&gt;, &lt;code&gt;application/xhtml+xml&lt;/code&gt;, …&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;GitHub Copilot&lt;/td&gt;&lt;td&gt;否&lt;/td&gt;&lt;td&gt;&lt;code&gt;text/html&lt;/code&gt;, &lt;code&gt;application/xhtml+xml&lt;/code&gt;, …&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Gemini CLI&lt;/td&gt;&lt;td&gt;否&lt;/td&gt;&lt;td&gt;通配符&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Windsurf&lt;/td&gt;&lt;td&gt;否&lt;/td&gt;&lt;td&gt;通配符&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;七个里三个主动要。&lt;strong&gt;而这七行数据本身就是一份测试用例&lt;/strong&gt;——见
&lt;a href=&quot;/letterpress/wiki/content-negotiation/&quot;&gt;内容协商&lt;/a&gt; 里关于怎么把它钉成回归测试的部分。&lt;/p&gt;
&lt;h2 id=&quot;一个安静失败的实现细节&quot;&gt;一个安静失败的实现细节&lt;a class=&quot;heading-anchor&quot; href=&quot;#一个安静失败的实现细节&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Claude Code 那一行值得单独说。&lt;/p&gt;
&lt;p&gt;它发的是 &lt;code&gt;text/markdown, text/html, */*&lt;/code&gt;——&lt;strong&gt;没有 q 值&lt;/strong&gt;。在 HTTP 里，
缺省 q 值等于 1.0，于是 markdown 和 html 两个媒体范围并列。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这里要说清楚：RFC 并没有规定并列时怎么打破平局。&lt;/strong&gt;
&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.1&quot;&gt;RFC 9110 §12.5.1&lt;/a&gt;
规定的是「媒体范围可以被更具体的范围覆盖，&lt;strong&gt;更具体的优先&lt;/strong&gt;」——
它按&lt;em&gt;具体程度&lt;/em&gt;排序（&lt;code&gt;text/plain;format=flowed&lt;/code&gt; → &lt;code&gt;text/plain&lt;/code&gt; → &lt;code&gt;text/*&lt;/code&gt; → &lt;code&gt;*/*&lt;/code&gt;），
而对 q 值相同的两个类型，规范只说它们是 &lt;em&gt;equally preferred&lt;/em&gt;，没有给顺序。&lt;/p&gt;
&lt;p&gt;所以「靠前的优先」是&lt;strong&gt;本项目的实现选择&lt;/strong&gt;，不是规范要求。&lt;/p&gt;
&lt;p&gt;但如果实现里写的是严格的 &lt;code&gt;&gt;&lt;/code&gt; 比较：&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-dimmed&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#22272e;color:#24292e;--shiki-dark:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; (markdown.q &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; html.q) &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-dark:#96D0FF&quot;&gt; &apos;markdown&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-dark:#96D0FF&quot;&gt; &apos;html&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么 &lt;code&gt;1.0 &gt; 1.0&lt;/code&gt; 为假，判定落到 HTML 上。&lt;strong&gt;Claude Code 永远拿不到 markdown，
而服务端不报错、不警告、日志里什么都没有。&lt;/strong&gt; 你只会觉得「这功能好像没用」。&lt;/p&gt;
&lt;p&gt;本项目的做法是&lt;strong&gt;并列时比顺序&lt;/strong&gt;（如上所述，这是选择而非规范要求）：&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-dimmed&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#22272e;color:#24292e;--shiki-dark:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; (markdown.q &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;!==&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; html.q) &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; markdown.q &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; html.q;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; markdown.index &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; html.index;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这类 bug 的可怕之处在于它&lt;strong&gt;不会以错误的形式出现，只会以「没效果」的形式出现&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=&quot;为什么现成的工具都不太行&quot;&gt;为什么现成的工具都不太行&lt;a class=&quot;heading-anchor&quot; href=&quot;#为什么现成的工具都不太行&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Astro 生态里已经有两个包在做这件事，但都停在半路：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;astro-markdown-for-agents&lt;/strong&gt;：在构建期生成 &lt;code&gt;.md&lt;/code&gt; 文件，但协商&lt;strong&gt;只在 dev server 里生效&lt;/strong&gt;。
它的 README 直说：「不包含托管平台运行时垫片；静态托管环境下的服务时协商
应由使用方自行实现。」也就是说&lt;strong&gt;上生产就没了&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;astro-markdown-export&lt;/strong&gt;：把源 &lt;code&gt;.md&lt;/code&gt; 复制进产物目录，&lt;strong&gt;完全不做协商&lt;/strong&gt;。
要访问 &lt;code&gt;/slug/index.md&lt;/code&gt; 才拿得到，而且导出的是未渲染的源文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两者的周下载量分别是 106 和 17。而主流 Astro 集成是三十万到两百万。
&lt;strong&gt;这不是「已经解决了只是没人知道」，是「有人碰过就放弃了」。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;难在哪&quot;&gt;难在哪&lt;a class=&quot;heading-anchor&quot; href=&quot;#难在哪&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;难的不是算法——算法上面已经写完了，四十行。&lt;/p&gt;
&lt;p&gt;难的是&lt;strong&gt;静态托管不解析请求头&lt;/strong&gt;。静态站就是一堆文件，CDN 把文件原样吐出来，
它不会因为你 &lt;code&gt;Accept&lt;/code&gt; 写了什么就换一个文件给你。所以你需要一层边缘函数。&lt;/p&gt;
&lt;p&gt;这也是为什么 Cloudflare 的 Markdown for Agents 要 Pro 及以上套餐——
它是在边缘替你做了转换。而 Vercel 的实现只在自己的平台内生效。
&lt;strong&gt;跑在免费静态托管上的博客，两头都够不着。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;出路是：构建期生成 &lt;code&gt;.md&lt;/code&gt; 孪生文件，再用一层几十行的边缘函数做选择。
转换在构建期完成（零运行时成本），边缘只负责「给哪个」。
Cloudflare Pages Functions、Netlify Edge Functions、Vercel Middleware
的免费额度都够用。&lt;/p&gt;
&lt;h2 id=&quot;两件必须做对的事&quot;&gt;两件必须做对的事&lt;a class=&quot;heading-anchor&quot; href=&quot;#两件必须做对的事&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;一、&lt;code&gt;Vary: Accept&lt;/code&gt; 不能省。&lt;/strong&gt; 少了它，CDN 会把 markdown 版本缓存下来
发给浏览器——用户打开博客看到一坨纯文本。这是个会让人当场关掉标签页的 bug，
而它只在缓存命中时出现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;二、&lt;code&gt;*/*&lt;/code&gt; 不算「想要 markdown」。&lt;/strong&gt; Gemini CLI 和 Windsurf 只发通配符，
意思是「给什么都行」。如果把通配符也算作 markdown 偏好，这两家会被塞
markdown——它们本来接受 HTML，你硬给别的格式，属于自找麻烦。&lt;/p&gt;
&lt;h2 id=&quot;顺带说一句-llmstxt&quot;&gt;顺带说一句 llms.txt&lt;a class=&quot;heading-anchor&quot; href=&quot;#顺带说一句-llmstxt&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;你可能想问为什么不提 &lt;code&gt;llms.txt&lt;/code&gt;。因为它的实际效果被严重高估了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Google 明说不需要。&lt;/strong&gt; Search Central 的 AI 功能文档里写着：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You don’t need to create new machine readable files, AI text files,
or markup to appear in these features.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;注意原文说的是「&lt;strong&gt;不需要&lt;/strong&gt;」而不是「不支持」——不做也不会因此缺席，
但 Google 也没说它有害。
（&lt;a href=&quot;https://developers.google.com/search/docs/appearance/ai-features&quot;&gt;原文&lt;/a&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多个独立实验都没测到可归因的效果。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;这组数字曾经被删掉过，2026-09-24 找回来了。&lt;/strong&gt; 这里原先写
「Ahrefs 2026 年 5 月实测 137,210 个域名：97% 的 llms.txt 从未被请求过」。
上一轮我按几种常见路径试 &lt;code&gt;ahrefs.com/blog/*&lt;/code&gt; 全部 404、Ahrefs 自己的
sitemap 里没有收录博客，于是&lt;strong&gt;把整组数字删了&lt;/strong&gt;，并写明「查不到就不该留着」。&lt;/p&gt;
&lt;p&gt;那个删除是&lt;strong&gt;对的推理、错的结论&lt;/strong&gt;：原文确实存在，地址是
&lt;a href=&quot;https://ahrefs.com/blog/llmstxt-study/&quot;&gt;https://ahrefs.com/blog/llmstxt-study/&lt;/a&gt;，此前只是没试中路径。
2026-09-24 重新抓取原文逐字核对，数据全部对得上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;样本：Ahrefs Web Analytics 中&lt;strong&gt;2026 年 5 月&lt;/strong&gt;获得流量的全部 &lt;strong&gt;137,210&lt;/strong&gt; 个域名
（方法学章节原文：&lt;code&gt;all 137,210 domains ... that received traffic in May 2026&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;其中 28% 发布了 &lt;code&gt;llms.txt&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;这些文件的 &lt;strong&gt;97% 在 2026 年 5 月零请求&lt;/strong&gt;——&lt;code&gt;97% of those files received zero traffic in May 2026&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;有请求的那 3% 里，&lt;strong&gt;96% 的请求来自 bots&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;发布日期与数据窗口是两件事&lt;/strong&gt;：原文发布于 &lt;strong&gt;2026-06-15&lt;/strong&gt;，
统计的是 &lt;strong&gt;2026 年 5 月&lt;/strong&gt;。先前的写法把两者混成了「2026 年 5 月实测」。&lt;/p&gt;
&lt;p&gt;留着一个可点开的来源，比留着一句「查不到」更有用；
但&lt;strong&gt;边界也要跟着一起写&lt;/strong&gt;：这是对 &lt;code&gt;llms.txt&lt;/code&gt; 根文件的 HTTP 200 探测，
不是对「AI 有没有读到内容」的观测——&lt;strong&gt;被请求的 3% 里，77% 的 bots 并不是 AI 工具&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;它&lt;strong&gt;零成本、无下行风险&lt;/strong&gt;，而且确实有 agent 会抓（Claude Code 抓取 llms.txt
的频率高于任何 AI 搜索机器人），所以本站在生成它。但把它当卖点是误导。&lt;/p&gt;
&lt;p&gt;真正的杠杆是&lt;strong&gt;干净的内容协商&lt;/strong&gt;和&lt;strong&gt;语义化的 HTML&lt;/strong&gt;——后者对静态站来说是白送的。&lt;/p&gt;
&lt;h2 id=&quot;参考文献&quot;&gt;参考文献&lt;a class=&quot;heading-anchor&quot; href=&quot;#参考文献&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.1&quot;&gt;RFC 9110 §12.5.1（Accept 头）&lt;/a&gt;、
&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc7763.html&quot;&gt;RFC 7763（&lt;code&gt;text/markdown&lt;/code&gt; 媒体类型）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.cloudflare.com/markdown-for-agents/&quot;&gt;Cloudflare, &lt;em&gt;Markdown for Agents&lt;/em&gt;&lt;/a&gt;
—— 正文里「16,180 → 3,150 token、省 80%」那组数字出自这里。
&lt;strong&gt;测量条件是 Cloudflare 拿自己的文档做的，本项目没有复现它&lt;/strong&gt;；
它说明的是「干净 HTML 转 markdown 能省多少」，不是本站的节省率
（本站自己的数字在 &lt;a href=&quot;/letterpress/wiki/content-negotiation/&quot;&gt;内容协商&lt;/a&gt; 末尾，且是&lt;strong&gt;启发式估算&lt;/strong&gt;，那里附了真实词表的对照）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.google.com/search/docs/appearance/ai-features&quot;&gt;Google Search Central, &lt;em&gt;AI features and your website&lt;/em&gt;&lt;/a&gt;
—— 正文里「Google 明说不需要」那段引文出自这里&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ahrefs.com/blog/llmstxt-study/&quot;&gt;Ahrefs, &lt;em&gt;We Analyzed 137K Sites: 97% of llms.txt Files Never Get Read&lt;/em&gt;&lt;/a&gt;，
2026-06-15 发布、数据窗口为 2026 年 5 月
—— 正文里 137,210 / 97% / 96% 那组数字出自这里。
&lt;strong&gt;测量条件是 Ahrefs 拿自己的流量数据做的，本项目没有复现它&lt;/strong&gt;；
它观测的是「根文件有没有被请求」，&lt;strong&gt;不是&lt;/strong&gt;「AI 有没有读到内容」——
被请求的那 3% 里 77% 的 bots 并不是 AI 工具&lt;/li&gt;
&lt;li&gt;&lt;del&gt;Ahrefs 那组数字原先作为一条独立引用，2026-09-23 被移除。&lt;/del&gt;
&lt;strong&gt;2026-09-24 找到了原始地址并已恢复&lt;/strong&gt;（见上一条）——
那一轮的删除是&lt;strong&gt;对的推理、错的结论&lt;/strong&gt;：&lt;code&gt;ahrefs.com/blog/*&lt;/code&gt; 几个常见路径全 404
只证明了「我按这几个路径没找到」，不等于「原文不存在」。
&lt;strong&gt;一个找不到的路径不是不存在的证据。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;勘误（2026-09-22 / 09-23 / 09-24）&lt;/strong&gt;：这几条原先都是纯文本、点不开，已核实的补了链接；
&lt;strong&gt;09-23 那一轮把 Ahrefs 那条当作「核实不到」移除了，09-24 又找回来了&lt;/strong&gt;——
因为原文一直存在，只是当时试的路径没命中。
三轮下来值得记住的是：&lt;strong&gt;「我没找到」和「它不存在」之间隔着一整条推理链&lt;/strong&gt;，
跳过那条链就会把一次失败的检索固化成一条结论。&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><category>web</category><category>agent</category><category>http</category><author>XIAOXUsop</author></item><item><title>静态站搜索的三种做法，以及它们各自真实的上限</title><link>https://xiaoxusop.github.io/letterpress/static-site-search/</link><guid isPermaLink="true">https://xiaoxusop.github.io/letterpress/static-site-search/</guid><description>前端过滤、构建期索引、外挂服务——三种方案的取舍不在功能多少，在「索引存在哪」这一件事上。</description><pubDate>Fri, 14 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;静态站没有后端，所以搜索必须换个地方放。只有三个位置：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;放进浏览器&lt;/strong&gt;、&lt;strong&gt;放进构建产物&lt;/strong&gt;、&lt;strong&gt;放到另一台服务器上&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;选哪种，取决于你愿意接受哪一种上限。&lt;/p&gt;
&lt;h2 id=&quot;一放进浏览器把内容全量打到前端&quot;&gt;一、放进浏览器：把内容全量打到前端&lt;a class=&quot;heading-anchor&quot; href=&quot;#一放进浏览器把内容全量打到前端&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;最简单：构建时把全部文章导成一个 JSON，页面加载后在前端过滤。&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-dimmed&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#22272e;color:#24292e;--shiki-dark:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt; results&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; all.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#DCBDFB&quot;&gt;filter&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E36209;--shiki-dark:#F69D50&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; p.title.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#DCBDFB&quot;&gt;includes&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;(q) &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;||&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt; p.body.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#DCBDFB&quot;&gt;includes&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;(q));&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;上限在「全量」两个字上。&lt;/strong&gt; 索引就是全部正文，文章一多，用户为了搜一个词要下载整站内容。&lt;/p&gt;
&lt;p&gt;50 篇中等长度的文章大约 1–2 MB。1000 篇就是几十 MB——搜索框还没用上，
流量已经烧完了。&lt;/p&gt;
&lt;p&gt;它适合&lt;strong&gt;几十篇以内&lt;/strong&gt;的站点。超过这个量级，你会开始想「能不能只加载摘要」，
而那正是第二种方案。&lt;/p&gt;
&lt;h2 id=&quot;二放进构建产物只下载倒排索引&quot;&gt;二、放进构建产物：只下载倒排索引&lt;a class=&quot;heading-anchor&quot; href=&quot;#二放进构建产物只下载倒排索引&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;构建时切词、建倒排索引，把索引作为静态资源发布。用户只下载索引，不下载正文；
命中之后再按需取片段。&lt;/p&gt;
&lt;p&gt;这一类的代表是 &lt;a href=&quot;https://pagefind.app/&quot;&gt;Pagefind&lt;/a&gt; 和 &lt;a href=&quot;https://www.fusejs.io/&quot;&gt;Fuse.js&lt;/a&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fuse.js&lt;/strong&gt; 是纯前端模糊匹配，仍然需要你把待搜内容喂给它——所以它解决的是
「怎么匹配」，没解决「内容放哪」&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pagefind&lt;/strong&gt; 是构建后跑的索引器，直接读你的 HTML 产物。它把索引切成小块，
搜索时只加载相关分片——&lt;strong&gt;这才是真正的「索引比正文小」&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pages/Netlify 这类托管上，Pagefind 是当前最省事的选择：一条命令，
零后端，还能按语言分别处理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;上限在分词上。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Pagefind 对中文不做词干提取——中文本来也没有词形变化，所以这不影响正确性。
真正的影响是&lt;strong&gt;召回&lt;/strong&gt;：它按字/词切分建索引，你搜「排版」能命中包含「排版」的段落，
但搜「网页排版」不一定能命中只写了「排版」的地方。&lt;/p&gt;
&lt;p&gt;英文靠词干回退（running ↔ run）缓解这类问题，中文没有对应机制。
&lt;strong&gt;这是静态站搜索的固有限制，不是某个库的缺陷。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;三外挂服务algoliameilisearch-这类&quot;&gt;三、外挂服务：Algolia、Meilisearch 这类&lt;a class=&quot;heading-anchor&quot; href=&quot;#三外挂服务algoliameilisearch-这类&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;把索引放到一台真的服务器上，查询走网络。&lt;/p&gt;
&lt;p&gt;功能上限最高：分词器可换、同义词、拼写纠错、按字段加权、结果高亮、
搜索分析——都能做。&lt;/p&gt;
&lt;p&gt;代价也很清楚：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多了一个要运维的东西。&lt;/strong&gt; 静态站的全部好处就是「没有服务器」，
为了搜索把它加回来，这笔账要自己算&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多了一个会挂的东西。&lt;/strong&gt; 搜索服务挂了，站还在，但搜索框废了&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多了一个花钱的地方。&lt;/strong&gt; 免费额度通常按「记录数 × 查询次数」计&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;它适合「搜索是核心功能」的站点&lt;/strong&gt;——文档站、电商、知识库。
对个人博客，通常是过度配置。&lt;/p&gt;
&lt;h2 id=&quot;怎么选&quot;&gt;怎么选&lt;a class=&quot;heading-anchor&quot; href=&quot;#怎么选&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;div class=&quot;table-wrap&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;可横向滚动的表格&quot; data-table-wrap=&quot;&quot;&gt;
























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;文章数&lt;/th&gt;&lt;th&gt;建议&lt;/th&gt;&lt;th&gt;理由&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&amp;#x3C; 50&lt;/td&gt;&lt;td&gt;第一种就够&lt;/td&gt;&lt;td&gt;全量 JSON 也就几百 KB，省一套索引流程&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;50 – 几千&lt;/td&gt;&lt;td&gt;第二种&lt;/td&gt;&lt;td&gt;索引与正文分离，零运维&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;搜索本身是产品&lt;/td&gt;&lt;td&gt;第三种&lt;/td&gt;&lt;td&gt;只有它能在&lt;strong&gt;分词&lt;/strong&gt;这一层做文章&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;判断的关键不是功能表，是「索引存在哪」&lt;/strong&gt;：在用户浏览器里、在构建产物里、
还是在别人的服务器上。三种位置对应三种上限，没有例外。&lt;/p&gt;
&lt;h2 id=&quot;顺带一句搜索需要-javascript这是要认的&quot;&gt;顺带一句：搜索需要 JavaScript，这是要认的&lt;a class=&quot;heading-anchor&quot; href=&quot;#顺带一句搜索需要-javascript这是要认的&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;前两种方案都跑在浏览器里，所以&lt;strong&gt;没有 JS 就没有搜索&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这不是可以糊弄过去的——正确的做法是给一个 &lt;code&gt;&amp;#x3C;noscript&gt;&lt;/code&gt; 出口，
让关掉 JS 的读者至少能走到归档页，而不是对着一个永远转圈的输入框。&lt;/p&gt;
&lt;p&gt;相关条目：&lt;a href=&quot;/letterpress/wiki/content-negotiation/&quot;&gt;内容协商&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id=&quot;参考&quot;&gt;参考&lt;a class=&quot;heading-anchor&quot; href=&quot;#参考&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Pagefind 官方文档（索引分片与多语言处理）&lt;/li&gt;
&lt;li&gt;Fuse.js 官方文档（模糊匹配算法与阈值）&lt;/li&gt;
&lt;li&gt;本文提到的中文召回限制来自本站在 2026-09 的实测：搜「排版」7 条命中，
搜「行高」6 条命中，而搜「网页排版」只有 4 条——少掉的那几条正是
正文里只写了「排版」、没写完整四个字的地方&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;复现方式：&lt;code&gt;npm run build&lt;/code&gt; 之后到 &lt;code&gt;/search/&lt;/code&gt; 搜这三个词。
&lt;strong&gt;&lt;code&gt;npm run dev&lt;/code&gt; 里搜不到任何东西&lt;/strong&gt;——索引是构建期（Pagefind）生成的，
开发服务器没有它。这个数字也会随内容增删而变化，上面记的是当时的快照。&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><category>搜索</category><category>静态站</category><category>索引</category><author>XIAOXUsop</author></item><item><title>构建可复现比你想的难，而且它值得</title><link>https://xiaoxusop.github.io/letterpress/reproducible-builds/</link><guid isPermaLink="true">https://xiaoxusop.github.io/letterpress/reproducible-builds/</guid><description>同一份源码在两台机器上构建出不同的产物——这件事的代价不是洁癖，是你没法用 diff 判断&quot;这次改动干了什么&quot;。</description><pubDate>Thu, 20 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;「同一份源码应该构建出同一个产物」听起来像句废话。&lt;/p&gt;
&lt;p&gt;但它在大多数项目里&lt;strong&gt;并不成立&lt;/strong&gt;，而且失败方式全都极其安静：
构建照常成功，产物照常能用，只是&lt;strong&gt;每次都不一样&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;代价在后面才显现：你没法用 diff 判断「这次改动到底改了什么」，
因为产物里全是无关噪声。&lt;/p&gt;
&lt;h2 id=&quot;那些让构建不可复现的东西&quot;&gt;那些让构建不可复现的东西&lt;a class=&quot;heading-anchor&quot; href=&quot;#那些让构建不可复现的东西&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;按我实际踩到的顺序：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一、时间戳。&lt;/strong&gt; 最容易想到，也最容易修。但它的变体很多：产物里嵌构建时间、
日志文件带时间、&lt;code&gt;Date.now()&lt;/code&gt; 漏进某个 ID 的计算。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;二、文件系统的枚举顺序。&lt;/strong&gt; 目录里读文件不会保证顺序。你在本地按 A、B、C 遍历，
CI 上可能是 C、A、B——于是拼接出的产物顺序不同。&lt;/p&gt;
&lt;p&gt;修法是&lt;strong&gt;显式排序&lt;/strong&gt;：&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-dimmed&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#22272e;color:#24292e;--shiki-dark:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#6CB6FF&quot;&gt; files&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F47067&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#DCBDFB&quot;&gt; readdir&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;(dir);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;files.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#DCBDFB&quot;&gt;sort&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#ADBAC7&quot;&gt;();   &lt;/span&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#768390&quot;&gt;// 这一行不是可选的&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;三、哈希的输入里混进了路径。&lt;/strong&gt; &lt;code&gt;哈希(绝对路径 + 内容)&lt;/code&gt; 会让产物随 checkout 目录变化。
看起来低级，但在「把目录结构也算进去」时很容易顺手写出来。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;四、依赖的解析顺序。&lt;/strong&gt; 锁文件解决了一部分，但 &lt;code&gt;^1.2.3&lt;/code&gt; 这类范围在锁文件被删后
仍会解析到不同版本。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;五、当前时区。&lt;/strong&gt; 这条最阴——见下文。&lt;/p&gt;
&lt;h2 id=&quot;时区那条一次真实的-ci-失败&quot;&gt;时区那条：一次真实的 CI 失败&lt;a class=&quot;heading-anchor&quot; href=&quot;#时区那条一次真实的-ci-失败&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;我给站点加了「定时发布」：frontmatter 里写 &lt;code&gt;date&lt;/code&gt;，到点之前不发布。&lt;/p&gt;
&lt;p&gt;实现很简单：比较文章日期与当前时刻。&lt;/p&gt;
&lt;p&gt;本地全绿。推上去，&lt;strong&gt;CI 红了&lt;/strong&gt;。&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-dimmed&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#22272e;color:#24292e;--shiki-dark:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;UTC+8 的机器：此刻是 9 月 12 日 01:57&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;CI（UTC）：  此刻是 9 月 11 日 17:57&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;new Date(&apos;2026-09-12&apos;)&lt;/code&gt; 在两种情况下都解析成 &lt;strong&gt;UTC 午夜&lt;/strong&gt;，
但「今天」取决于机器时区。于是&lt;strong&gt;同一篇文章在两地判定相反&lt;/strong&gt;：
本地认为该发布，CI 认为还没到点——而失败形态是「文章静默消失 →
引用它的 &lt;code&gt;[[链接]]&lt;/code&gt; 变成断链」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;根子不在比较逻辑写错了，在于「是否发布」依赖了构建时刻。&lt;/strong&gt;
只要依赖构建时刻，就必然破坏两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;构建可复现&lt;/strong&gt;：9 月 11 日与 9 月 12 日构建出的产物不同&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨机器一致&lt;/strong&gt;：作者本地能看到、CI 看不到，或反过来&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我最后&lt;strong&gt;把这个功能删掉了&lt;/strong&gt;。不是修不好，是代价算不过来：
&lt;code&gt;draft: true&lt;/code&gt; 已经覆盖了「还没写完」这个主要场景，
而定时发布多出来的那点便利，换的是两个更重要的契约。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;做不到的事不该留着。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;怎么验&quot;&gt;怎么验&lt;a class=&quot;heading-anchor&quot; href=&quot;#怎么验&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;「可复现」是个可以被证明的性质，不用靠信念。&lt;/p&gt;
&lt;p&gt;最直接的做法：&lt;strong&gt;改变一个本不该影响内容的环境输入，再比哈希&lt;/strong&gt;。仓库已经把这条
做成命令：&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-dimmed&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#22272e;color:#24292e;--shiki-dark:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#F69D50&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-dark:#96D0FF&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-dark:#96D0FF&quot;&gt; verify:reproducible&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它会先扫描生产源码，禁止无参数 &lt;code&gt;new Date()&lt;/code&gt; 与 &lt;code&gt;Date.now()&lt;/code&gt; 直接读取构建时钟；
再清空 &lt;code&gt;.astro&lt;/code&gt; 与 &lt;code&gt;dist&lt;/code&gt;，分别在 UTC 与 America/Los_Angeles 跑完整的
Astro + Pagefind，逐文件比较 SHA-256。当前 93 个产物跨时区逐字节一致；
任一文件不同都会列出路径并以非零状态退出。&lt;/p&gt;
&lt;p&gt;这里两层检查缺一不可：跨时区构建能抓出日期显示偏移，但抓不出一年才变化一次的
页脚年份；源码时钟检查能在跨年之前就拦住后者。内容里的日期则统一按 UTC 日历显示，
不会因为构建机位于负时区而退到前一天。&lt;/p&gt;
&lt;p&gt;更有价值的一种是&lt;strong&gt;换一种无关输入再比&lt;/strong&gt;。比如换行符：&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark-dimmed&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#22272e;color:#24292e;--shiki-dark:#adbac7; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#768390&quot;&gt;# CRLF 工作区构建一次，LF 工作区构建一次，比对产物&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我在这个站上做过这个实验：&lt;strong&gt;CRLF 与 LF 两种工作区，产物 sha256 完全一致&lt;/strong&gt;
（87 个文件）。结论是换行符不影响产物——但这个结论&lt;strong&gt;是测出来的，不是想出来的&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=&quot;为什么值得&quot;&gt;为什么值得&lt;a class=&quot;heading-anchor&quot; href=&quot;#为什么值得&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;有人会说这是洁癖。不是。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可复现是「用 diff 做判断」的前提。&lt;/strong&gt; 如果每次构建产物都在抖，
那「这次提交改了什么」就只能靠读代码——而读代码是会漏的。&lt;/p&gt;
&lt;p&gt;它也是&lt;strong&gt;缓存正确性&lt;/strong&gt;的前提。构建产物不稳定，内容寻址的缓存就永远不命中，
或者更糟：命中了不该命中的。&lt;/p&gt;
&lt;p&gt;而代价通常很小。上面那些来源，每一个的修法都是几行：
排序一下、固定一下、把时间从哈希输入里拿掉。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;花的这几行，换来的是「diff 可信」。&lt;/strong&gt; 这笔账很好算。&lt;/p&gt;
&lt;p&gt;相关条目：&lt;a href=&quot;/letterpress/wiki/build-probe/&quot;&gt;构建探针&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id=&quot;参考&quot;&gt;参考&lt;a class=&quot;heading-anchor&quot; href=&quot;#参考&quot; aria-label=&quot;此小节的永久链接&quot; data-pagefind-ignore=&quot;&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;reproducible-builds.org（这个概念的系统性整理）&lt;/li&gt;
&lt;li&gt;本站的实测：&lt;code&gt;npm run verify:reproducible&lt;/code&gt; 会在两个时区完整构建并逐文件比对，
同时禁止构建时钟泄漏；&lt;code&gt;.gitattributes&lt;/code&gt; 统一换行符，内容层的排序全部显式化&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>构建</category><category>可复现</category><category>ci</category><author>XIAOXUsop</author></item></channel></rss>