<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>静态站 · XB 现场笔记</title><link>https://www.xbcatm.com/zh/tags/%E9%9D%99%E6%80%81%E7%AB%99/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/%E9%9D%99%E6%80%81%E7%AB%99/index.xml" rel="self" type="application/rss+xml"/><item><title>Hugo 发布选项决定哪些页面进入产物</title><link>https://www.xbcatm.com/zh/posts/hugo-draft-release-tag/</link><pubDate>Sat, 18 Mar 2028 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/hugo-draft-release-tag/</guid><description>草稿标记、发布时间、构建选项以及后续部署共同决定 Hugo 页面何时公开。</description><content:encoded>&lt;p&gt;Hugo 在构建运行时判断哪些页面应当渲染。文章元数据提供发布时间与草稿状态，项目配置和命令参数提供包含规则，构建时钟提供判断所用的时间。生成目录一旦部署，在下次产物替换之前不会自行变化。因此，日期走到午夜不等于触发发布；没有写 &lt;code&gt;draft: true&lt;/code&gt; 的文件，也不会自动被当成草稿。这两个边界决定了排期内容能否按预期上线。&lt;/p&gt;
&lt;h2 id="元数据和构建选项分别起作用"&gt;元数据和构建选项分别起作用&lt;/h2&gt;
&lt;p&gt;在默认规则下，标记为 &lt;code&gt;draft: true&lt;/code&gt; 的页面不进入产物；发布时间在未来的页面也会被排除，已经过了失效时间的页面同样如此。Hugo 根据 front matter 配置解析各类日期，显式的 &lt;code&gt;publishDate&lt;/code&gt; 可以不同于页面显示的 &lt;code&gt;date&lt;/code&gt;。对必须精确到某个边界的排期，应使用包含时区的完整时间戳，避免本机和构建服务器把同一个日期解释为不同时间。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--buildDrafts&lt;/code&gt; 包含草稿，&lt;code&gt;--buildFuture&lt;/code&gt; 包含未来内容，&lt;code&gt;--buildExpired&lt;/code&gt; 包含已失效内容，短选项分别是 &lt;code&gt;-D&lt;/code&gt;、&lt;code&gt;-F&lt;/code&gt; 和 &lt;code&gt;-E&lt;/code&gt;。这些开关处理不同条件：一篇发布时间尚未到达的草稿，需要同时允许草稿和未来内容才能提前进入产物。项目配置或环境配置也能启用相同行为，所以只看文章里的标记不足以判断实际发布结果，还必须检查本次构建的有效选项。&lt;/p&gt;
&lt;p&gt;本站明确设置了 &lt;code&gt;buildFuture: true&lt;/code&gt;，用于保留包含未来日期文章的既有公开目录。因此，本站这些文章的日期不是保密截止时间，也不能用 Hugo 的默认排期规则推断它们尚未公开。修改发布策略之前，需要把配置和预期 URL 清单一起比较，否则关闭一个看似普通的开关，就可能让大量已经存在的地址消失。介绍默认机制与解释本站事实，应当采用各自对应的证据。&lt;/p&gt;
&lt;h2 id="预览产物仍然可以被公开"&gt;预览产物仍然可以被公开&lt;/h2&gt;
&lt;p&gt;预览开关只改变输出包含什么，不会给产物增加禁止部署的属性。如果流水线上传了用 &lt;code&gt;-D -F&lt;/code&gt; 生成的目录，草稿和未来页面一样可以被访问。因此，预览应使用独立输出位置，正式发布则根据计划中的选项重新构建。Git 发布标签只是标识源码历史，既不会改变 Hugo 的包含规则，也不能证明某个生成目录已经经过审阅。交付记录需要关联源码版本、构建选项和实际产物。&lt;/p&gt;
&lt;p&gt;按时发布还需要在发布时间之后执行新的构建与部署。反过来，把已经公开的页面重新标记为草稿，也不会撤回服务器上的旧 HTML 或缓存副本。需要重建、替换被服务的产物，再检查公共地址。Hugo 通常不会自动删除输出目录里的所有遗留文件，因此独立的新目录或合适的清洁构建，是证明被排除页面确实不存在的必要步骤。导航里不显示链接，远不足以证明内容已经下线。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;把预览目录覆盖到发布目录，可能让一篇后来恢复为草稿的文章仍能通过直接地址打开，而索引里已经看不到它。排查应检查生成文件、订阅源、站点地图以及真正对外的页面，区分索引隐藏和文件移除。另一个容易遗漏的规则是：章节页面标记为草稿，并不会自动把所有子页面也变成草稿。若整个子树需要排除，应设置具体子页元数据，或依照文档使用级联配置。&lt;/p&gt;
&lt;p&gt;时区差异则可能让两台构建机器从同一个提交生成不同页面。边界测试应固定 Hugo 版本和构建时钟，并记录生效参数。不要在尚未弄清日期解析规则、环境时区和参数来源之前，为了让页面出现而随意改动发布日期。那样可能掩盖配置错误，还会改变订阅顺序和页面展示时间。首先使输入和时钟可重现，才能判断差异真正来自哪里。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;安装 Python 3 和 Hugo 0.165.0 后，可运行下面的隔离实验。它在临时项目中创建五篇页面，分别构建到八个独立目录，不修改当前仓库，也不执行部署。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;tempfile&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TemporaryDirectory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;hugo-publication-&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;content/posts&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;layouts/_default&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;hugo.yaml&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;baseURL: https://example.org/&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;layouts/index.html&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;lt;h1&amp;gt;Publication test&amp;lt;/h1&amp;gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;layouts/_default/list.html&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;{{ .Title }}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;layouts/_default/single.html&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;{{ .Title }}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extra&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;visible&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;draft&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;draft: true&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;future&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;publishDate: 2030-01-01T00:00:00Z&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;future-draft&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;draft: true&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;publishDate: 2030-01-01T00:00:00Z&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;expired&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;expiryDate: 2025-12-01T00:00:00Z&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;content/posts/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.md&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;---&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;title: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;date: 2025-01-01T00:00:00Z&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;extra&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;---&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Publication fixture.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;normal&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;2026&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;visible&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;drafts&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;2026&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-D&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;visible&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;draft&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;future&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;2026&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-F&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;visible&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;future&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;preview&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;2026&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-D&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-F&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;visible&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;draft&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;future&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;future-draft&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;expired&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;2026&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-E&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;visible&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;expired&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;all&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;2026&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-D&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-F&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-E&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;visible&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;draft&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;future&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;future-draft&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;expired&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;boundary&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;2030&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;visible&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;future&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;later&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;2031&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;visible&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;future&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;output-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;hugo&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--source&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--destination&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;--clock&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;year&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-01-01T00:00:00Z&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;posts/*/index.html&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;断言依次检查默认排除、单独打开各个开关、组合预览，以及时间恰好到达和越过发布边界后的结果。后两次普通构建仍然不包含两篇草稿。未来草稿只有同时使用 -D 和 -F 才会提前出现；-E 则独立包含已过期页面。这证明的是受控条件下 Hugo 的渲染选择，没有覆盖部署调度、CDN 缓存失效，也没有套用本站主动包含未来页面的配置。正式发布时，还需要从本次产物和公共路由分别取得证据，确认生成结果与线上实际一致。&lt;/p&gt;
&lt;p&gt;修改发布选项时，应保留普通、未来、草稿、未来草稿和过期五类用例，它们覆盖不同的包含条件。检查本次新生成的文件，不能因为导航里没有链接，就断言被排除的页面肯定不在产物里。&lt;/p&gt;
&lt;h2 id="延伸阅读"&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/zh/notes/a-static-server-should-have-one-obvious-root/"&gt;静态服务器应有一个明确的根目录&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/zh/notes/a-release-is-not-complete-until-the-public-edge-agrees/"&gt;发布在公共边缘一致之前不算完成&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/getting-started/usage/"&gt;Hugo：基本用法&lt;/a&gt;，草稿、未来和失效内容，以及输出目录行为，已核验 2026-09-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/content-management/front-matter/"&gt;Hugo：Front matter&lt;/a&gt;，发布元数据和级联设置，已核验 2026-09-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/commands/hugo/"&gt;Hugo 命令参考&lt;/a&gt;，内容包含开关和构建时钟，已核验 2026-09-06。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>可读的证据优先于仪表盘</title><link>https://www.xbcatm.com/zh/essays/readable-evidence-before-dashboards/</link><pubDate>Tue, 18 Aug 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/essays/readable-evidence-before-dashboards/</guid><description>发布论断需要带时间、范围与来源的可检查观察；仪表盘与单次探针回答不同的问题。</description><content:encoded>&lt;p&gt;监控仪表盘很方便，但方便不等于证据。本文论证：对小型静态服务，&amp;ldquo;站点是否健康&amp;quot;最可审计的形式，是一组运维者能阅读、能重跑、能粘进报告里的纯文本事实——而不是一个聚合的面板。&lt;/p&gt;
&lt;p&gt;区别很重要，因为两者回答的问题不同。仪表盘是众多样本之上的透镜，调教成让你看到趋势。探针是对公共边缘的一次单一、可复现的观察。当任务是证明一次发布或诊断一个具体请求时，探针是保真度更高的产物。&lt;/p&gt;
&lt;h2 id="仪表盘的聚合正是风险所在"&gt;仪表盘的聚合正是风险所在&lt;/h2&gt;
&lt;p&gt;仪表盘把成千上万条原始观察折成一系列聚合面板：平均延迟、错误率、可用性百分比。聚合正是审计线索变薄的地方。一个&amp;quot;99.98% 可用&amp;quot;的数字不会告诉你哪个请求失败、哪个请求头错了、今天的发布是否有被服务。除非你在别处保留原始日志，否则它是一个抹掉了产生它的证据的摘要。&lt;/p&gt;
&lt;p&gt;对&amp;quot;边缘服务的是我暂存的那棵树&amp;quot;这种信心属性，仪表盘用错了工具。响应体比较可以回答一个 URL、一种表示、一个网络观测点与一个时刻的这个问题。抓取首页头部不能确定整棵树或所有 CDN 节点的身份。&lt;/p&gt;
&lt;h2 id="一条可以读的探针"&gt;一条可以读的探针&lt;/h2&gt;
&lt;p&gt;仓库附带只读辅助脚本 &lt;code&gt;static/downloads/check-http-headers.sh&lt;/code&gt;，也发布在 &lt;code&gt;/downloads/check-http-headers.sh&lt;/code&gt;。它打印响应头、最终状态与 URL。下面是示意节选，不是新的生产测量：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP/2 200
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-type: text/html; charset=utf-8
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-security-policy: default-src &amp;#39;self&amp;#39;; …; script-src &amp;#39;none&amp;#39;; …
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;人读这一行，立刻同时知道三件事：状态是 200、类型是 HTML、无脚本策略确实在服务。把它与说&amp;quot;边缘 OK&amp;quot;的面板对比——面板要求你信任生成它的东西；响应头这一行是该客户端收到什么的直接证据。两者仍依赖探针实现、TLS 验证、路由以及原始输出的留存。读取证书 &lt;code&gt;notAfter&lt;/code&gt; 增加的是到期时间观察，不能证明主机名匹配或信任链有效。&lt;/p&gt;
&lt;h2 id="用可核验的语句取代信号缺失"&gt;用可核验的语句取代信号缺失&lt;/h2&gt;
&lt;p&gt;本站偏好的不是仪表盘，而是在构建期间强制的契约，和发布窗口内一组确定性的命令。大致是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;离线检查校验产物（每一条内部链接、每一个 SVG、无脚本边界、校验和）。&lt;/li&gt;
&lt;li&gt;分别检查公共边缘的状态与响应头、响应字节、TLS 身份与到期时间。头部脚本不打印证书日期，也不会自动拒绝所有意外 HTTP 状态。&lt;/li&gt;
&lt;li&gt;字节清单描述暂存树；抓取选定 URL 并比较正文，才能建立采样交付的一致性。完整论断需要检查所有发布映射及表示转换，而不是只对服务器磁盘做哈希。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每一处都是一个运维者能粘进 issue 或 changelog、未来的审计者能重跑的句子。它们都不需要面板才能被理解。&lt;/p&gt;
&lt;h2 id="留下足以复查的上下文"&gt;留下足以复查的上下文&lt;/h2&gt;
&lt;p&gt;记录 UTC 时间、请求与最终 URL、方法、所选编码、连接到的远端 IP、客户端版本、退出码、HTTP 状态、相关响应头及正文摘要。保留失败，也保留成功。共享日志前去除凭据、Cookie 与个人信息。一个返回 200 的代理错误页可以通过状态码检查；比较已知正文或发布标记能发现另一类故障。&lt;/p&gt;
&lt;p&gt;有用的发布报告应区分“此响应在此时匹配”与“服务在一个月内满足可用性目标”。保留聚合指标背后的原始观察，可能时让仪表盘链接回它们。显示形式本身不能创造可信测量。&lt;/p&gt;
&lt;h2 id="什么时候你确实需要仪表盘"&gt;什么时候你确实需要仪表盘&lt;/h2&gt;
&lt;p&gt;这一切不是说观测无意义。如果你运维很多实例、聚合容量、或观察长期趋势，仪表盘是看清全局的正确视图。论断更窄：当问题只是&amp;quot;这一棵发布在公共边缘是否健康&amp;quot;时，可复现的终端读取是良好的起点。重复探针、多个观测点、错误预算与告警还能发现单次成功请求看不到的间歇或区域故障。设计良好的仪表盘可以展现这些观察，同时保留其来源。&lt;/p&gt;
&lt;p&gt;决策启发式很简单：如果一条语句可以用一条命令检查、且其输出就是证明，就优先命令；只有在你真正需要聚合处才保留仪表盘。这能让站点自己的操作故事与站点的内容一样可审计。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl 文档&lt;/a&gt;，从单次调用读取响应头，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://12factor.net/logs"&gt;十二要素方法：日志&lt;/a&gt;，把日志当作运维者可读的事件流，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/concepts/observability-primer/"&gt;OpenTelemetry：可观测性基础&lt;/a&gt;，日志、指标与追踪的互补作用，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>两只钟模型中的缓存新鲜度与校验</title><link>https://www.xbcatm.com/zh/essays/caching-as-two-clocks/</link><pubDate>Fri, 14 Aug 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/essays/caching-as-two-clocks/</guid><description>新鲜度决定能否直接复用，校验检查已存表示。用一个小练习分开这两个决定。</description><content:encoded>&lt;p&gt;缓存有两个独立决定：能否不联系服务器就复用已存响应，以及如何检查它是否仍代表当前资源。“两只钟”只是便于记忆的比喻。新鲜度确实是年龄计算；校验是比较，不是另一个计时器。缓存中的响应可能按策略仍然新鲜，却已经不同于刚发布的源站，这并不一定违反 HTTP。&lt;/p&gt;
&lt;h2 id="新鲜度多久可以不询问就复用"&gt;新鲜度：多久可以不询问就复用&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Cache-Control: max-age=60&lt;/code&gt; 给响应 60 秒的新鲜度寿命。缓存将寿命与当前年龄比较，年龄需要考虑 &lt;code&gt;Date&lt;/code&gt;、&lt;code&gt;Age&lt;/code&gt;、传输耗时与存储时间。恰好到 60 秒时已经陈旧，不会再多新鲜一秒。共享缓存有 &lt;code&gt;s-maxage&lt;/code&gt; 时优先采用它；没有显式寿命时，某些响应也可以获得启发式新鲜度。&lt;/p&gt;
&lt;p&gt;新鲜响应通常可以复用，但仍须匹配缓存键与 &lt;code&gt;Vary&lt;/code&gt; 字段，并遵守请求及响应指令。即使按年龄仍新鲜，&lt;code&gt;no-cache&lt;/code&gt; 也要求复用前成功校验。&lt;code&gt;no-store&lt;/code&gt; 禁止存储，不是“每次重新验证”的同义词。&lt;code&gt;public&lt;/code&gt; 允许共享缓存在原本受限制的某些情形下存储响应，它不指定缓存时长。&lt;/p&gt;
&lt;p&gt;对于会变化的 HTML，较短的显式寿命加校验通常有用。这意味着新发布的内容可能要等到寿命结束才通过合规缓存可见，属于有意接受的代价。整树发布不会使浏览器或 CDN 中已有的条目失效。内容寻址的资源则可使用较长寿命，前提是字节变化时 URL 也变化。&lt;/p&gt;
&lt;h2 id="校验询问已存表示是否仍然匹配"&gt;校验：询问已存表示是否仍然匹配&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ETag&lt;/code&gt; 是不透明校验器，不是供客户端解码的校验和。&lt;code&gt;Last-Modified&lt;/code&gt; 是时间戳校验器。两者都不会单独确定新鲜度寿命，服务器也不一定同时提供它们。例如下面是分别表达寿命与校验器的示意头部：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Cache-Control: max-age=60, must-revalidate
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ETag: &amp;#34;release-a&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;过期后，GET 携带 &lt;code&gt;If-None-Match: &amp;quot;release-a&amp;quot;&lt;/code&gt;，选中的表示仍匹配时可得到 &lt;code&gt;304 Not Modified&lt;/code&gt;，变化时则得到带新正文的 &lt;code&gt;200&lt;/code&gt;。缓存根据 304 更新适用的元数据，再复用已存正文。没有相应的已存表示，304 本身没有可用的正文。&lt;code&gt;If-None-Match&lt;/code&gt; 使用弱比较，因此验证成功不一定证明字节完全相同；&lt;code&gt;If-Range&lt;/code&gt; 及部分响应拼接有更严格的要求。&lt;/p&gt;
&lt;p&gt;服务器可以用字节或其他可靠变化的元数据生成 ETag。强标签必须区分数据不同的表示，包括适用时的不同内容编码。给 gzip 与 identity 的不同字节发送相同强 ETag，不能靠缩短寿命修复。&lt;code&gt;Vary: Accept-Encoding&lt;/code&gt; 还告诉缓存哪个请求头参与了表示选择。&lt;/p&gt;
&lt;h2 id="用小练习分开两个决定"&gt;用小练习分开两个决定&lt;/h2&gt;
&lt;p&gt;这个 Python 模型刻意不包含启发式寿命、网络延迟及变体选择。&lt;code&gt;age&lt;/code&gt; 表示已经算好的当前年龄；条件请求面对的服务器校验器暂时没有变化。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;lifetime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;stored_etag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#34;release-a&amp;#34;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;server_etag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#34;release-a&amp;#34;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;61&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fresh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;lifetime&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;reuse without validation&amp;#34;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fresh&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;validate: 304, reuse body&amp;#34;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stored_etag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;server_etag&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;validate: 200, replace body&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第一行直接复用；后两行校验，并在 304 后复用正文。把 &lt;code&gt;server_etag&lt;/code&gt; 改成 &lt;code&gt;&amp;quot;release-b&amp;quot;&lt;/code&gt;：陈旧的两种情况变成 200，59 秒的情况仍然复用已有响应。这就是新鲜度策略用内容可见性延迟换取较低延迟与源站负载的含义。&lt;/p&gt;
&lt;h2 id="值得断言的契约"&gt;值得断言的契约&lt;/h2&gt;
&lt;p&gt;应测试匹配和变化的校验器、新鲜度边界，以及所选编码。还要决定源站故障时怎么办。HTTP 在特定条件或扩展下允许复用陈旧响应；&lt;code&gt;must-revalidate&lt;/code&gt; 则禁止在响应陈旧后未经成功校验就复用，即使客户端已经断网。它用可用性换取更严格的正确性约束。单有校验器没有这样的承诺。&lt;/p&gt;
&lt;p&gt;因此，有用的发布记录应写明缓存策略、选中的表示、实际条件响应状态，以及可接受的更新延迟。强制要求两个校验器头部和一次 304，既不能证明每个缓存都有当前字节，也不能消除所有带宽浪费，更不能代替对真实中间层配置的测试。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9111.html"&gt;RFC 9111：HTTP Caching&lt;/a&gt;，第 4.1–4.3、5.2 节中的变体、年龄、校验与指令，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110：HTTP Semantics&lt;/a&gt;，第 8.8、13.1.2、13.1.5 节中的校验器与比较规则，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>零 JavaScript 静态站是可辩护的选择</title><link>https://www.xbcatm.com/zh/essays/zero-js-as-a-defensible-choice/</link><pubDate>Thu, 13 Aug 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/essays/zero-js-as-a-defensible-choice/</guid><description>为什么在一个内容站点上关闭脚本能力，是一项能用响应头、而不是凭口味证明的安全与可靠性决策。</description><content:encoded>&lt;p&gt;静态站点不需要 JavaScript 才能站得住脚；&lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; 标签的缺席本身就可能是重点。本文论证一个在线内容站点通过策略关闭脚本通道的工程依据，并说明如何在公共边缘验证策略。下方头部来自仓库预期配置，不是刚刚观测到的生产响应。&lt;/p&gt;
&lt;p&gt;论证分三部分：你删掉的攻击面；让删除可被观察的契约；以及确实该放弃这一层的情况。&lt;/p&gt;
&lt;h2 id="关闭脚本通道能缩小哪些威胁面"&gt;关闭脚本通道能缩小哪些威胁面&lt;/h2&gt;
&lt;p&gt;一个不执行自身或第三方脚本的页面，移除了原本需要修补与审查的客户端脚本依赖。有效的无脚本 CSP 会在合规浏览器中阻止注入脚本执行，但 HTML 注入仍可改变文字、链接与页面结构。跟踪还可能依赖图片、导航或服务器日志，无须 JavaScript。本站另有不依赖第三方浏览器资源的要求，这比删掉脚本标签更广。没有浏览器脚本包，就没有其依赖漂移；构建工具与服务器依赖仍然存在。每一类都是有限、可命名的表面——也就是&amp;quot;小系统&amp;quot;那种&amp;quot;选择你能说清的边界&amp;quot;的特性。&lt;/p&gt;
&lt;p&gt;这不是声称站点&amp;quot;安全&amp;quot;，而是声称攻击面更窄，并且关键的是更容易推理，因为它是靠构造枚举的，而不是靠审计枚举的。&lt;/p&gt;
&lt;h2 id="让边界可强制执行的响应头"&gt;让边界可强制执行的响应头&lt;/h2&gt;
&lt;p&gt;口味不是策略。这里的区别是，服务器下发策略，由浏览器强制执行。仓库中的 Caddy 示例配置指定：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-security-policy: default-src &amp;#39;self&amp;#39;; base-uri &amp;#39;self&amp;#39;; form-action &amp;#39;none&amp;#39;; frame-ancestors &amp;#39;none&amp;#39;; object-src &amp;#39;none&amp;#39;; script-src &amp;#39;none&amp;#39;; style-src &amp;#39;self&amp;#39;; img-src &amp;#39;self&amp;#39; data:
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;其中 &lt;code&gt;script-src 'none'&lt;/code&gt; 指令是可强制执行的部分。在这个完整策略且没有更具体的脚本放行规则时，合规浏览器会拒绝页面脚本、内联事件处理器与 JavaScript URL。若使用的是 &lt;code&gt;Content-Security-Policy-Report-Only&lt;/code&gt;，则只报告、不阻止。以后添加 &lt;code&gt;script-src-elem&lt;/code&gt; 或 &lt;code&gt;script-src-attr&lt;/code&gt; 时需要重新审查，因为它们可覆盖各自脚本类别的回退规则。&lt;/p&gt;
&lt;p&gt;构建校验器检查生成产物中的可执行脚本、事件处理器与 URL，不能证明反向代理确实发送了预期 CSP。应分别抓取代表性 HTML、错误页与重定向目的页，检查实际强制策略，并测试浏览器行为。源码检查、响应检查与浏览器执行是三项不同的验证。&lt;/p&gt;
&lt;h2 id="移除之后静态结构带来的好处"&gt;移除之后静态结构带来的好处&lt;/h2&gt;
&lt;p&gt;没有脚本之后，内容模型回归到更接近已发布文档的东西。缓存处理的是 HTTP 表示，不是页面后来发生的 DOM 变化。静态产物让表示更容易检查和版本化；ETag 与新鲜度策略仍然取决于服务器配置。在终端里读线上 HTML、用&amp;quot;查看源代码&amp;quot;、或索引整棵树都很直接，因为产物就是磁盘上的普通文件。这是这个站点接受的交换：没有脚本管理的状态、没有客户端渲染、没有依赖 JavaScript 视图模型的反馈。原生链接、键盘导航、CSS 行为以及 &lt;code&gt;details&lt;/code&gt; 之类的元素仍然可以提供交互。对文档与运维站点而言，这是正确的取舍，值得写下来。&lt;/p&gt;
&lt;h2 id="什么时候零-js的论断是错的"&gt;什么时候&amp;quot;零 JS&amp;quot;的论断是错的&lt;/h2&gt;
&lt;p&gt;这是一层功能，不是一张纯净的标签。一个真正需要客户端交互的站点——实时仪表盘、文档编辑器、支付流程——不应该为了迁就约束就扭曲成提供静态 HTML 再硬塞变通方案。错误的应用是削足适履地把产品切成适应约束的形状。正确的应用是认识到：用来阅读、检索、归档的内容，从&amp;quot;成为文档&amp;quot;中收益；只有在文档本身不足时才需要脚本。&lt;/p&gt;
&lt;p&gt;实际响应头是所采样响应的有用证据，却不能证明所有浏览器都支持、或每条路由都具有相同策略。仍须安全地渲染内容、保护发布权限、维护生成器与服务器。只有说清这些限制，边界才真正可辩护。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.w3.org/TR/CSP3/"&gt;W3C：Content Security Policy Level 3&lt;/a&gt;，策略下发、脚本指令与回退规则（工作草案），已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src"&gt;MDN：内容安全策略（&lt;code&gt;script-src&lt;/code&gt;）&lt;/a&gt;，禁用脚本执行的指令，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://portswigger.net/web-security/cross-site-scripting"&gt;PortSwigger：什么是跨站脚本（XSS）？&lt;/a&gt;，脚本注入及其后果，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://owasp.org/www-community/controls/Content_Security_Policy"&gt;OWASP：内容安全策略&lt;/a&gt;，CSP 作为纵深防御控件，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>