<?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>Static-Web · XB 现场笔记</title><link>https://hk.xbcatm.com/zh/tags/static-web/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://hk.xbcatm.com/zh/tags/static-web/index.xml" rel="self" type="application/rss+xml"/><item><title>静态网站的发布检查清单</title><link>https://hk.xbcatm.com/zh/notes/a-release-checklist-for-a-static-site/</link><pubDate>Thu, 30 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/a-release-checklist-for-a-static-site/</guid><description>每个发布都值得一组小、快、针对性强、且会为真实原因失败的检查。</description><content:encoded>&lt;p&gt;静态网站发布很简单，但简单并不免除发布检查的需要。有用的检查足够小，能在每次发布运行，也足够具体，能为真实的理由而失败。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/release-checklist.detail.zh.svg" alt="四个编号阶段分别构建固定生成器版本的干净制品，要求 index.html、404.html、robots.txt、sitemap.xml 非空并复核链接，在待发布和公共路由测试确切制品，最后记录提交、生成器版本和下载校验和。Hugo 源码预览位于独立分支，会重新构建；HEAD 检查元数据，GET 支持正文比较。" width="720" height="2570"&gt;&lt;figcaption&gt;检查对象应是生成制品。干净输出不等于可复现；占位符搜索只是复核线索，真正的链接门禁应基于解析器。发布前服务确切制品，发布后比较两种语言的公共 GET 正文，并保存提交、生成器版本和下载校验和。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="构建干净的制品"&gt;构建干净的制品&lt;/h2&gt;
&lt;p&gt;使用固定版本的生成器，构建到一次性的输出目录：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hugo version
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hugo --cleanDestinationDir --gc --minify --destination public
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;--gc&lt;/code&gt; 回收未使用的构建缓存资源，不会清理陈旧的已发布页面。&lt;code&gt;--cleanDestinationDir&lt;/code&gt; 会移除目标目录中的陈旧文件，因此只能用于一次性制品目录，不能指向源码或其他数据所在的目录。把打印出的版本与项目固定版本比较；打印版本本身不会强制版本一致。记录构建输入并验证生成的制品。干净的目标目录不能证明逐字节可复现：时间戳与其他非确定性输入需要单独控制和比较。不要在构建之后手改生成的 HTML。&lt;/p&gt;
&lt;h2 id="检查制品而不只是源码"&gt;检查制品，而不只是源码&lt;/h2&gt;
&lt;p&gt;检视生成的树，确认期望的入口点存在。把下面的断言块作为独立 shell 脚本运行；任何必需文件缺失或为空时，都应在打印清单之前停止：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s public/index.html
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s public/404.html
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s public/robots.txt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s public/sitemap.xml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;find public -type f -print &lt;span class="p"&gt;|&lt;/span&gt; sort
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后搜索意外的占位符、本地路径与仅开发用的主机：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rg -l &lt;span class="s1"&gt;&amp;#39;localhost|127\.0\.0\.1|TODO|example\.com|/Users/&amp;#39;&lt;/span&gt; public --glob &lt;span class="s1"&gt;&amp;#39;*.html&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这只是复核线索，不是自动发布门禁：本清单这类示例本来就会命中，翻译页中也一样。这项仅检查 HTML 的扫描不会覆盖 XML 订阅或 JSON 搜索输出。&lt;code&gt;rg&lt;/code&gt; 无匹配时退出 1，发生错误时退出 2。逐个复核列出的文件，区分围栏示例与有效链接或属性，并用基于解析器的链接检查作为真正门禁。公开链接中的私有路径属于发布错误。&lt;/p&gt;
&lt;h2 id="验证公共路由"&gt;验证公共路由&lt;/h2&gt;
&lt;p&gt;让隔离的类生产服务器直接指向这份制品，才能进行更可靠的发布前路由检查。Hugo 开发服务器会重新构建内容；下面是独立的源码预览，不能证明它服务的就是已经检查过的 &lt;code&gt;public/&lt;/code&gt; 字节。第一条命令在一个终端运行，其余请求在另一个终端运行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hugo server --bind 127.0.0.1 --port &lt;span class="m"&gt;1313&lt;/span&gt; --baseURL http://127.0.0.1:1313/ &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --disableFastRender --disableLiveReload --renderToMemory
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/notes/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/resources/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/missing-page/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;HEAD 检查不比较正文。对待发布服务器再运行 GET，发布后将公共正文与制品比较，并检查两种语言。确认缺失路由在生产服务器上返回 404 状态。一个外观精美但状态 200 的错误页，仍然会破坏监控与缓存。&lt;/p&gt;
&lt;h2 id="记录结果"&gt;记录结果&lt;/h2&gt;
&lt;p&gt;在发布记录中保留源码提交、生成器版本与每个可下载资源的校验和。这样无需依赖记忆，就能把线上文件与产生它的源码进行比较。&lt;/p&gt;
&lt;p&gt;这份检查清单的可下载版本在&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;资源&lt;/a&gt;一节中。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/commands/hugo/"&gt;Hugo command reference&lt;/a&gt;，干净生产构建的生成器标志，已核验 2026-07-30。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;，HTTP 验证与响应头检查选项，已核验 2026-07-30。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;静态网站发布检查清单&lt;/a&gt;，可下载的配套资源。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/commands/hugo_server/"&gt;Hugo server command reference&lt;/a&gt;，开发服务器重新构建、内存渲染与实时刷新控制，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>安全响应头应描述实际页面</title><link>https://hk.xbcatm.com/zh/notes/security-headers-should-describe-the-actual-page/</link><pubDate>Thu, 23 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/security-headers-should-describe-the-actual-page/</guid><description>当紧凑的策略与静态页面真正加载的资源匹配时，它更安全。</description><content:encoded>&lt;p&gt;安全响应头不是安全代码的替代品，但它们让浏览器强制实施有用的边界。策略应描述实际被服务的页面，而不是从另一个项目复制来的泛型模板。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/security-layers.detail.zh.svg" alt="HTML、本地样式表与 favicon 决定 CSP 资源许可。Content-Security-Policy 强制执行；Report-Only 揭示违规且报告需另行配置。CSP 应通过响应头发送：frame-ancestors 在 meta 中被忽略且不回退到 default-src，form-action 需要自己的指令，style-src self 本身不允许内联样式。HSTS 从有效 HTTPS 响应学习；www.example.test 的 includeSubDomains 覆盖该主机与后代，不覆盖 example.test 或 api.example.test。应检查正常页、缺失页与资源响应，并在真实浏览器验证执行。本图不声称已经测试。" width="720" height="3460"&gt;&lt;figcaption&gt;把响应头视为部署契约：匹配实际资源，区分强制执行与仅报告策略，再验证准确主机范围和浏览器行为。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="从资源图开始"&gt;从资源图开始&lt;/h2&gt;
&lt;p&gt;如果站点只提供 HTML、本地样式表、一个 favicon、没有任何可执行 JavaScript，那么一条窄策略是可理解的：&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; 只有在站点不需要可执行脚本时才是正确的选择。加入搜索、评论或统计标签会改变资源图，并应触发一次有意识的策略复核。&lt;/p&gt;
&lt;h2 id="增加传输与嵌套保护"&gt;增加传输与嵌套保护&lt;/h2&gt;
&lt;p&gt;对仅 HTTPS 的站点，下列响应头是常见起点：&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;Strict-Transport-Security: max-age=31536000; includeSubDomains
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;X-Content-Type-Options: nosniff
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;X-Frame-Options: DENY
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Referrer-Policy: strict-origin-when-cross-origin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;只有当每个子域名都准备好提供 HTTPS 时才启用带 includeSubDomains 的 HSTS。响应头是与未来部署的契约，而不只是扫描器里的一个分数。&lt;/p&gt;
&lt;h2 id="检查行为而不只是存在"&gt;检查行为，而不只是存在&lt;/h2&gt;
&lt;p&gt;请求一个正常页面、一个缺失页面与一个静态资源。错误响应应携带与成功响应同样重要的安全响应头。还要确认压缩与缓存不会改变内容类型，也不会暴露内部路径。&lt;/p&gt;
&lt;p&gt;本站的&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;Caddy 示例&lt;/a&gt;只是一个起点，不是普适策略。请根据你自己的部署调整主机名、证书处理与应用路由。&lt;/p&gt;
&lt;p&gt;通过响应头发送 CSP。meta 策略中的 &lt;code&gt;frame-ancestors&lt;/code&gt; 会被忽略，而且它不会继承 &lt;code&gt;default-src&lt;/code&gt;；&lt;code&gt;form-action&lt;/code&gt; 也需要自己的指令。&lt;code&gt;style-src 'self'&lt;/code&gt; 本身不允许内联样式，因此应测试生成的主题，包括深色模式。只有页面确实需要 data 图片时，才在 &lt;code&gt;img-src&lt;/code&gt; 中保留 &lt;code&gt;data:&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Content-Security-Policy-Report-Only&lt;/code&gt; 可在上线过程中揭示违规，但不会强制执行策略；报告端点还需单独配置。除了查看响应头，也要在真实浏览器中验证执行效果。HSTS 从有效 HTTPS 响应中学习；带有 &lt;code&gt;includeSubDomains&lt;/code&gt; 时，它还覆盖设置策略的主机的后代：&lt;code&gt;www.example.test&lt;/code&gt; 的策略不覆盖父域 &lt;code&gt;example.test&lt;/code&gt; 或同级 &lt;code&gt;api.example.test&lt;/code&gt;。确认这个准确范围后，再逐步延长有效期。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP"&gt;MDN: Content Security Policy guide&lt;/a&gt;，CSP 指令名称与语法，已核验 2026-07-23。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security"&gt;MDN: &lt;code&gt;Strict-Transport-Security&lt;/code&gt;&lt;/a&gt;，HSTS 指令与部署注意事项，已核验 2026-07-23。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/caddyfile/directives/header"&gt;Caddy &lt;code&gt;header&lt;/code&gt; 指令&lt;/a&gt;，响应头操作指令，已核验 2026-07-23。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/CSP3/"&gt;W3C Content Security Policy Level 3&lt;/a&gt;，响应头交付、指令回退、嵌套与仅报告策略；当前为工作草案，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc6797.html"&gt;RFC 6797 — HTTP Strict Transport Security&lt;/a&gt;，第 8.1–8.3 节的有效 HTTPS 交付与主机及后代匹配，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>发布在公共边缘一致之前不算完成</title><link>https://hk.xbcatm.com/zh/notes/a-release-is-not-complete-until-the-public-edge-agrees/</link><pubDate>Thu, 09 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/a-release-is-not-complete-until-the-public-edge-agrees/</guid><description>把生成制品与访问者实际能收到的响应进行比较。</description><content:encoded>&lt;p&gt;静态发布消除了应用运行时的复杂度，但不会消除生成目录与公共响应之间的边界。发布检查应同时覆盖两边。&lt;/p&gt;
&lt;p&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/diagrams/release-path.svg" alt="一条小的静态发布路径" /&gt;
&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/release-edge.detail.zh.svg" alt="先在使用预期配置的待发布服务器上检查确切制品，切换目录后在公共边缘重复检查。将 index.html、下载资源和 404.html 与解码后的 GET 正文配对。字节比较与预期状态、类型、缓存、安全头分别检查。重复中英文样本，并在新目录通过检查前保留上一个已知良好目录。" width="720" height="2200"&gt;&lt;figcaption&gt;切换前后使用同一份制品。下载行对应文中的 public/downloads/check-http-headers.sh。cmp 比较前会解码受支持的内容编码；服务器转换需有明确比较规则。状态和响应头另行检查，并保留前一目录供主动回滚。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="从干净的制品开始"&gt;从干净的制品开始&lt;/h2&gt;
&lt;p&gt;使用固定生成器构建到干净的目录。记录源码提交、生成器版本、输出文件列表，以及用户会下载的文件的校验和。一次干净的构建能捕获复制式部署可能会意外保留的陈旧文件。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;make verify
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;find public -type f -print &lt;span class="p"&gt;|&lt;/span&gt; sort
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;文件列表是证据，不是装饰。如果某条路由消失，diff 应能说明是该来源被移除，还是生成器未能产出它。&lt;/p&gt;
&lt;h2 id="演练三类响应"&gt;演练三类响应&lt;/h2&gt;
&lt;p&gt;发布前，用预期服务器配置测试待发布制品。切换目录后，在公共边缘重复检查；探测仍在线的旧站不能验证新发布。请求一个 HTML 页面、一个静态资源与一个缺失路径。检查状态码、内容类型、缓存策略与安全头：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl --compressed -fsS -D page.headers -o page.body https://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl --compressed -fsS -D asset.headers -o asset.body &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://www.example.test/downloads/check-http-headers.sh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl --compressed -sS -D missing.headers -o missing.body &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://www.example.test/does-not-exist
&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;cmp public/index.html page.body
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cmp public/downloads/check-http-headers.sh asset.body
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cmp public/404.html missing.body
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;把每个片段作为独立 shell 脚本运行，或放在可丢弃的 shell 会话中执行。&lt;code&gt;set -eu&lt;/code&gt; 让这个顺序脚本在构建、传输或比较失败后停止，避免后面的成功命令掩盖先前失败。缺失路径的请求有意不用 &lt;code&gt;-f&lt;/code&gt;：404 是预期结果，仍应被捕获。检查各响应头文件的最终状态：依次为 &lt;code&gt;200&lt;/code&gt;、&lt;code&gt;200&lt;/code&gt;、&lt;code&gt;404&lt;/code&gt;；字节比较不会自动完成状态码检查。这里不跟随重定向。&lt;code&gt;--compressed&lt;/code&gt; 在比较前解码受支持的内容编码；若服务器有意转换 HTML，需要另行说明比较规则。&lt;code&gt;cmp&lt;/code&gt; 相等时不输出，有差异时返回非零。在 &lt;code&gt;/zh/&lt;/code&gt; 及其错误页上重复检查，不能直接假设两种语言一致。&lt;/p&gt;
&lt;h2 id="让回滚保持简单"&gt;让回滚保持简单&lt;/h2&gt;
&lt;p&gt;在新目录通过检查之前，不要删除上一个目录。回滚应选择一个已知良好目录，而不是在事故中凭记忆重新构造一个。&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;发布检查清单&lt;/a&gt;与&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;HTTP 响应头检查&lt;/a&gt;都刻意做得足够小，能在干净的 shell 里运行。&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/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;，状态与表示语义，已核验 2026-07-09。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;，响应检查选项，已核验 2026-07-09。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;静态网站发布检查清单&lt;/a&gt;，本地配套资源。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Content-Type 应描述你实际发送的字节</title><link>https://hk.xbcatm.com/zh/notes/content-type-should-describe-the-bytes-you-actually-send/</link><pubDate>Thu, 11 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/content-type-should-describe-the-bytes-you-actually-send/</guid><description>在公开边缘核对媒体类型，并用 nosniff 让不匹配以可见的方式失败。</description><content:encoded>&lt;p&gt;静态服务器可能返回正确的字节，却带上错误的 &lt;code&gt;Content-Type&lt;/code&gt;。某个浏览器里页面看起来或许还正常，但下载、样式表、订阅阅读器或安全策略会把同一响应理解成别的东西。公开响应头才是契约；文件名只是服务器配置的输入。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/content-type.detail.zh.svg" alt="CSS 路径之后检查公开响应的状态、实际字节和 Content-Type。正确 CSS 与 HTML 错误页分列。浏览器脚本和样式目标使用 nosniff，失败时应修复响应；CDN 或代理的公开响应头需要单独核对。" width="720" height="2080"&gt;&lt;figcaption&gt;后缀不能证明实际收到的内容。应同时检查公开响应的状态、字节与媒体类型；CSS 路径上的 HTML 错误页不能用来测试 MIME 映射。nosniff 让脚本和样式目标上的类型不匹配以可见方式失败。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="检查有代表性的格式"&gt;检查有代表性的格式&lt;/h2&gt;
&lt;p&gt;检查一份 HTML 文档、样式表、SVG、feed 与可下载脚本。包含响应头但丢弃正文：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; path in / /assets/site.css /favicon.svg /index.xml /downloads/check-http-headers.sh&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; curl -sS -D - -o /dev/null &lt;span class="s2"&gt;&amp;#34;https://www.example.test&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&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; awk -v &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; BEGIN { print &amp;#34;== &amp;#34; path }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; /^HTTP\// || tolower($1) == &amp;#34;content-type:&amp;#34; || tolower($1) == &amp;#34;x-content-type-options:&amp;#34; { print }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &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;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;把示例资源路径换成当前发布中确实存在的文件；虚构 CSS 路径返回 HTML 404，并不能测试 MIME 映射。期望的媒体类型因资源而异，但应该具体且稳定。HTML 通常应包含字符集。CSS 与 SVG 不应被当作泛型二进制数据提供服务。用于下载的 shell 脚本不应被声明为 HTML。&lt;/p&gt;
&lt;p&gt;该循环只是展示响应头，不会自动断言成功、检视被丢弃的字节，也不会执行浏览器的阻止逻辑。应将资源与发布制品核对，并在浏览器中检查它作为样式表或脚本的实际加载结果。把相同 URL 直接当作文档打开，测试的是另一种请求目标。&lt;/p&gt;
&lt;h2 id="保持-nosniff-一致"&gt;保持 nosniff 一致&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;X-Content-Type-Options: nosniff&lt;/code&gt; 请求支持它的浏览器在脚本与样式目标上尊重声明的类型，而不是猜测。在这项 Fetch 检查中，脚本类目标需要 JavaScript MIME 类型，样式目标需要 &lt;code&gt;text/css&lt;/code&gt;；类型缺失或无法使用也会失败。这样，不匹配会变成可见的失败，而不是因浏览器而异的解读。&lt;/p&gt;
&lt;p&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;X-Content-Type-Options: nosniff
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="检查服务器映射"&gt;检查服务器映射&lt;/h2&gt;
&lt;p&gt;静态服务器通常根据扩展名推导类型。如果某个新扩展名被当作 &lt;code&gt;application/octet-stream&lt;/code&gt; 提供服务，先判断这是否有意为之，再决定是否加入全局覆盖。一个窄映射比一条会改变所有响应的规则更容易复核。&lt;/p&gt;
&lt;p&gt;在公开主机名上重复这些检查。CDN 或反向代理可能替换掉正确的源站响应头；而一个友好的错误页可能为后缀像 CSS 或 JavaScript 的路径返回 HTML。&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/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;，表示元数据与 &lt;code&gt;Content-Type&lt;/code&gt;，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Type"&gt;MDN: &lt;code&gt;Content-Type&lt;/code&gt;&lt;/a&gt;，媒体类型与字符集示例，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Content-Type-Options"&gt;MDN: &lt;code&gt;X-Content-Type-Options&lt;/code&gt;&lt;/a&gt;，&lt;code&gt;nosniff&lt;/code&gt; 行为，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff"&gt;WHATWG Fetch 标准&lt;/a&gt;，脚本类与样式目标的阻止规则，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>为什么小型站点可以保持静态</title><link>https://hk.xbcatm.com/zh/posts/why-a-small-site-can-stay-static/</link><pubDate>Mon, 08 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/posts/why-a-small-site-can-stay-static/</guid><description>在构建阶段生成共享文档，发布产物，并验证可编辑源码与对外文件之间的边界。</description><content:encoded>&lt;p&gt;静态站点在请求到来之前准备好文档。判断是否适用，关键是不同访问者是否需要服务器即时生成不同表示，而不是站点到底有多少页面。几百篇文章也可以共享一条发布流程：编辑源文件、构建 HTML 和资源、检查生成结果，再通过服务器或 CDN 对外提供这些文件。页面数量本身不是引入动态运行时的充分理由。&lt;/p&gt;
&lt;h2 id="把文档生成放在合适边界"&gt;把文档生成放在合适边界&lt;/h2&gt;
&lt;p&gt;一篇文章对每位读者含义相同，逐次请求重新拼装通常不会增加多少价值。模板、导航、翻译关系和索引都可以提前生成。对外服务此时依赖的是发布产物，而不是作者使用的内容数据库或生成器。请求仍然需要加密、日志、缓存和必要的访问控制；静态交付只改变文档生成时机，并没有消除托管层的这些职责。&lt;/p&gt;
&lt;p&gt;静态 HTML 完全可以包含 JavaScript 和交互控件。规模适中的公开文集可以使用客户端搜索索引，私有搜索或个性化结果则需要能执行权限检查的服务。表单、评论、支付和需要登录的写入，同样要由合适的处理端负责。应逐个识别这些边界：文章静态化，并不意味着浏览器能够成为权限判断或持久状态的权威来源。&lt;/p&gt;
&lt;p&gt;提前生成是转移工作，而不是消灭工作。大型文集可能重建缓慢，构建依赖可能失败，内容新鲜度也受到发布流水线限制。源文件刚刚保存，并不代表读者已经看到更新。对于有时效的内容，需要观察从编辑到发布的延迟、明确暴露构建失败，并直接验证发布后的页面，而不能以编辑器保存成功代替上线证据。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;常见错误是把整个仓库根目录当成网站根目录。这样即使没有导航链接，源数据、构建脚本和私有草稿也可能被直接下载。应使用专门的输出目录，其中只放准备公开的产物。从文章索引隐藏一项，与从发布文件中排除它，是两件不同的事。检查必须覆盖真实文件清单，包括复制进来的附件以及生成的客户端搜索数据。&lt;/p&gt;
&lt;p&gt;另一个问题是请求到来时逐个覆盖文件。新 HTML 可能先引用尚未上传的脚本，旧页面也可能在清理过程中失去依赖。应先准备完整发布集，保留必要的旧资源，再使用托管平台支持的切换机制。仅仅恢复旧源码提交，不会自动恢复当时的依赖、构建产物、缓存状态或外部服务，因此回退能力必须按真实交付链验证。&lt;/p&gt;
&lt;p&gt;静态输出同样可能存在注入问题。来自不可信输入的文字需要按所在上下文转义，直接插进 HTML 可能把普通字段变成可执行标记。文本转义也不是万能的：它不负责判断任意链接是否安全，不负责清洗有意允许的富文本，更不能直接用于 JavaScript 上下文。应审阅实际模板位置和源数据的信任边界，而不是因为没有请求时渲染就默认安全。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;
&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/posts-why-a-small-site-can-stay-static.detail.zh.svg" alt="夹具只从 public 提供 index.html。删除或修改独立源码后仍提供第一版；重新构建才发布第二版。源码 URL 返回 404，类似 HTML 的普通文字会被转义；只替换一个文件。" width="560" height="1486"&gt;
&lt;figcaption&gt;修改源码不会自动发布。 依据原文与明确限制静态绘制；未为本图重跑原文夹具。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;将下面程序保存为 &lt;code&gt;static-build.py&lt;/code&gt;，用 Python 3 执行 &lt;code&gt;python3 static-build.py&lt;/code&gt;。它先在临时目录创建内容源，并把源码放在 public 目录之外，再构建一个 HTML 文件。回环服务器只提供 public 内的文件。所有关键结果都通过实际 HTTP 请求确认；服务器关闭之后，实验创建的临时文件也会移除，不会修改现有站点。&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;functools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;partial&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;hashlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sha256&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;html&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;escape&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;http.client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTTPConnection&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;http.server&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SimpleHTTPRequestHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ThreadingHTTPServer&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;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;from&lt;/span&gt; &lt;span class="nn"&gt;threading&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Thread&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;json&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;os&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SimpleHTTPRequestHandler&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;def&lt;/span&gt; &lt;span class="nf"&gt;log_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&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;pass&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&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="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="n"&gt;public&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="s1"&gt;&amp;#39;public&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;public&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;source&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="s1"&gt;&amp;#39;source.json&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;source&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="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;title&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Version one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;text&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Published content&amp;#39;&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build&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;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_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="n"&gt;document&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;lt;!doctype html&amp;gt;&amp;lt;html lang=&amp;#34;en&amp;#34;&amp;gt;&amp;lt;head&amp;gt;&amp;lt;meta charset=&amp;#34;utf-8&amp;#34;&amp;gt;&amp;lt;title&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&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 class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h1&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&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 class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;text&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;/p&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&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;pending&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="s1"&gt;&amp;#39;next.html&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;pending&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="n"&gt;document&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;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;public&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;index.html&amp;#39;&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;return&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;build&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;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ThreadingHTTPServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;127.0.0.1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="o"&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;public&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;thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serve_forever&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;thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&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;try&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;def&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/&amp;#39;&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;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HTTPConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;127.0.0.1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;server_port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&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;try&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;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;GET&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getresponse&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;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&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;finally&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;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unlink&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;fetch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first&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;source&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="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;title&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Version two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;text&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;lt;b&amp;gt;literal text&amp;lt;/b&amp;gt;&amp;#39;&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;fetch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first&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;second&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;build&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;fetch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;second&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;first&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="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;amp;lt;b&amp;amp;gt;literal text&amp;amp;lt;/b&amp;amp;gt;&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;second&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/source.json&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;404&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;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;serves_without_source&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;edit_requires_rebuild&amp;#39;&lt;/span&gt;&lt;span class="p"&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="s1"&gt;&amp;#39;published_sha256&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hexdigest&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="s1"&gt;&amp;#39;source_not_published&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;text_escaped&amp;#39;&lt;/span&gt;&lt;span class="p"&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="k"&gt;finally&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;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shutdown&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;thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&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;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;server_close&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;首先，程序删除可编辑的内容源，仍然能够读取已经发布的页面字节。随后重新创建有改动的源文件，但在再次执行 build 之前，HTTP 请求仍返回第一版。第二次构建后，页面摘要发生变化，响应变为第二版。作为普通文字的 &lt;code&gt;&amp;lt;b&amp;gt;literal text&amp;lt;/b&amp;gt;&lt;/code&gt; 被转义，而不是成为真正的加粗标签；访问 &lt;code&gt;/source.json&lt;/code&gt; 则收到 404。预期四个行为标志都为真，同时打印两个不同的发布摘要。&lt;/p&gt;
&lt;p&gt;这些断言已经在 Python 3.14.4 下运行通过。程序通过同一文件系统上的 os.replace 替换一个完整文件，因此没有展示整个多资源站点的原子发布，也没有验证掉电持久性、自动回滚或某家托管平台的部署机制。实验的重点是证明源码与发布产物之间存在明确边界，不能把单文件行为推广成未经验证的整站承诺。&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://hk.xbcatm.com/zh/notes/the-advantage-of-small-systems/"&gt;小系统的优势&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/zh/notes/a-static-server-should-have-one-obvious-root/"&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://docs.github.com/en/pages/getting-started-with-github-pages/about-github-pages"&gt;GitHub Docs: About GitHub Pages&lt;/a&gt;，从已发布的 HTML、CSS 与 JavaScript 文件提供静态托管，已核验 2026-09-13。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Server-side/First_steps/Introduction"&gt;MDN: Introduction to the server side&lt;/a&gt;，静态与动态响应的生成边界，已核验 2026-09-13。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>静态服务器应有一个明确的根目录</title><link>https://hk.xbcatm.com/zh/notes/a-static-server-should-have-one-obvious-root/</link><pubDate>Thu, 04 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/a-static-server-should-have-one-obvious-root/</guid><description>让 Caddy 文件服务、生成制品与回滚路径保持一致。</description><content:encoded>&lt;p&gt;静态部署最有用的属性是：运维人员不用读应用代码，就能回答&amp;quot;这条响应是哪个目录提供的？&amp;quot;。Caddy 的 &lt;code&gt;root&lt;/code&gt; 与 &lt;code&gt;file_server&lt;/code&gt; 指令直接提供了这个边界。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/caddy-root.detail.zh.svg" alt="完整目录先核验，再设为 Caddy 根目录；新目录通过冒烟测试前保留旧的已知良好目录。虚线发布树包含 HTML、文本与资源。指向树外的符号链接可能暴露服务用户可读取的目标。私有文件留在树外；边缘检查独立核验状态码、内容类型、重定向、证书名称与缺失路由行为。" width="720" height="3010"&gt;&lt;figcaption&gt;明确提供响应的目录。检查符号链接及读取和目录穿越权限，再验证边缘的真实响应。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="让根目录明确"&gt;让根目录明确&lt;/h2&gt;
&lt;p&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;www.example.test {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; root * /srv/example-site
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; encode zstd gzip
&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; header {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -Server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; X-Content-Type-Options nosniff
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; X-Frame-Options DENY
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Referrer-Policy strict-origin-when-cross-origin
&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; file_server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;主机名与路径都是占位值；&lt;code&gt;www.example.test&lt;/code&gt; 不是可签发公开可信证书的生产名称。生产环境应使用自己控制且可解析的主机名，测试环境的本地 TLS 信任则另行安排。发布命令应发布一个完整目录、核验它，然后才把这个目录设为配置里的根。在新目录通过冒烟测试之前，保留旧的已知良好目录。&lt;/p&gt;
&lt;h2 id="检查边缘而不只是文件树"&gt;检查边缘，而不只是文件树&lt;/h2&gt;
&lt;p&gt;请求一个 HTML 页面、一个可下载的文本文件和一个缺失路径。把内容类型与状态码与本地制品比较。即使 404 页看起来很精致，若返回 &lt;code&gt;200&lt;/code&gt; 仍是部署缺陷。&lt;/p&gt;
&lt;p&gt;当 Caddy 管理 HTTPS 时，证书签发与 HTTP 到 HTTPS 的重定向都属于边缘行为。请从裸的 HTTP 监听器测试重定向，并从公共主机名读取证书名称。不要只凭进程健康就推断成功。&lt;/p&gt;
&lt;h2 id="根目录是发布边界不是沙箱"&gt;根目录是发布边界，不是沙箱&lt;/h2&gt;
&lt;p&gt;把版本控制元数据、备份、凭据与私有配置留在发布树之外。Caddy 会跟随根目录中的符号链接，因此链接可能暴露根目录外的文件。既要检查制品内容，也要检查服务用户的读取与目录穿越权限。此外，仅把 &lt;code&gt;404.html&lt;/code&gt; 放进目录并不会配置自定义错误处理；应核验缺失路由的状态码与预期错误页行为。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/caddyfile/directives/file_server"&gt;Caddy &lt;code&gt;file_server&lt;/code&gt; 指令&lt;/a&gt;，静态文件服务与根目录选择，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/caddyfile/directives/header"&gt;Caddy &lt;code&gt;header&lt;/code&gt; 指令&lt;/a&gt;，响应头配置，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/automatic-https"&gt;Caddy Automatic HTTPS&lt;/a&gt;，证书签发与 HTTPS 重定向，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options"&gt;MDN: &lt;code&gt;X-Frame-Options&lt;/code&gt;&lt;/a&gt;，防止被嵌套的选项，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc7034.html"&gt;RFC 7034 — X-Frame-Options&lt;/a&gt;，DENY 嵌入策略，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;静态网站发布检查清单&lt;/a&gt;，本地配套资源。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc6761.html"&gt;RFC 6761 — Special-Use Domain Names&lt;/a&gt;，保留的 &lt;code&gt;.test&lt;/code&gt; 名称，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>通过切换完整目录发布静态网站</title><link>https://hk.xbcatm.com/zh/notes/publish-a-static-site-by-switching-complete-directories/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/publish-a-static-site-by-switching-complete-directories/</guid><description>在活动树旁边预置一份完整制品，并只在验证后替换目录名。</description><content:encoded>&lt;p&gt;直接把新文件复制到活动的静态目录上，会造成一段访问者可能从一个版本拿到 HTML、从另一个版本拿到资源的时期。更安全的模式是：在活动树旁预置一棵完整的树，验证它，再改变目录名。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/atomic-switch.detail.zh.svg" alt="由单一发布者完成同文件系统与预置树核验后，live 存放旧版，stage 存放新版。第一次把 live 移到 rollback，live 缺失而 stage 与 rollback 保留。第二次把 stage 移到 live。成功时 live 为新版、rollback 为旧版；失败时 live 仍缺失，需要显式尝试恢复。恢复失败必须检查路径；发布后还须验证公共响应。" width="720" height="1890"&gt;&lt;figcaption&gt;两次移动之间 live 缺失，崩溃可能延长空窗。恢复需要尝试并验证。浏览器各次请求彼此独立，旧资源 URL 必须继续可访问。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="把所有发布目录放在同一文件系统上"&gt;把所有发布目录放在同一文件系统上&lt;/h2&gt;
&lt;p&gt;把活动树、预置树与回滚树放在同一文件系统上。以下片段假设使用同一个 Bash 会话、GNU Coreutils，且只有一个发布进程独占这些目录名。任何命令或复制管道任一侧失败，都应停止：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;df -P /srv/www
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在该父目录下创建唯一命名的中转目录。不要把未解析的变量或宽泛通配符当成删除目标来复用。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp -d /srv/www/site.new.XXXXXX&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="填充并验证预置目录"&gt;填充并验证预置目录&lt;/h2&gt;
&lt;p&gt;把生成制品复制进空目录，然后检查代表性文件。最后一条命令只显示数量；应与已核验制品的期望数量或清单比较，不能把任意数字都当作通过：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tar -C public -cf - . &lt;span class="p"&gt;|&lt;/span&gt; tar -C &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -xf -
&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="nb"&gt;test&lt;/span&gt; -s &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;/index.html&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;/404.html&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;find &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -type f &lt;span class="p"&gt;|&lt;/span&gt; wc -l
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当工具支持目录参数时，对预置路径运行链接与校验和检查。还要确认服务用户能穿过目录并读取文件：&lt;code&gt;mktemp -d&lt;/code&gt; 初始权限较严，解包后的制品必须具备预期权限。未通过验证的预置目录绝不能获得活动目录的名字。&lt;/p&gt;
&lt;h2 id="用显式回滚路径切换名字"&gt;用显式回滚路径切换名字&lt;/h2&gt;
&lt;p&gt;在第一次移动前解析每个路径：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;live&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/srv/www/site
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;stamp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date -u +%Y%m%dT%H%M%SZ&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;rollback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/srv/www/site.old-&lt;span class="nv"&gt;$stamp&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="nb"&gt;test&lt;/span&gt; -d &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt; ! -L &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -d &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt; ! -L &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; ! -e &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$rollback&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt; ! -L &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$rollback&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;df -P &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&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="nv"&gt;live_device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;stat -c %d -- &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;stage_device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;stat -c %d -- &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live_device&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage_device&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&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;mv -T -- &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$rollback&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; mv -T -- &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;test&lt;/span&gt; -s &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;/index.html&amp;#34;&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; mv -T -- &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$rollback&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&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;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Restore failed; inspect %s and %s before retrying.\n&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$rollback&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这是两步切换，不是一次原子替换：两次移动之间活动路径不存在，崩溃后可能一直缺失，直到恢复。GNU &lt;code&gt;mv -T&lt;/code&gt; 可防止已有目录被悄悄当作容器、把发布树移进其中，但不能协调并发发布进程。文件系统检查结果不一致时不要继续。平台特定的原子交换或符号链接替换能消除名字缺失窗口，但必须结合实际服务器测试。&lt;/p&gt;
&lt;h2 id="验证公共边缘"&gt;验证公共边缘&lt;/h2&gt;
&lt;p&gt;切换之后，请求首页、一个深层路由、一个带指纹的资源与一个缺失路径。把公共字节与预置制品比较。在检查完成与观察窗口结束之前，保留回滚目录。&lt;/p&gt;
&lt;p&gt;即使目录名切换是原子的，浏览器请求 HTML 与资源仍不是同一笔事务。让旧指纹资源继续可访问，才能服务缓存中或传输中的 HTML；仅保留一个不对外服务的回滚目录并不能做到这一点。文件系统重命名也不自动保证崩溃后的持久性，这需要平台相应的持久化步骤。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9799919799/functions/rename.html"&gt;POSIX &lt;code&gt;rename()&lt;/code&gt;&lt;/a&gt;，名字替换语义，已核验 2026-06-01。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man2/rename.2.html"&gt;Linux &lt;code&gt;rename(2)&lt;/code&gt;&lt;/a&gt;，文件系统与替换行为，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/tar/manual/tar.html"&gt;GNU tar manual&lt;/a&gt;，归档创建与解包，已核验 2026-06-01。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/coreutils/manual/html_node/Target-directory.html"&gt;GNU Coreutils: 目标目录&lt;/a&gt;，&lt;code&gt;-T&lt;/code&gt; 与目录嵌套风险，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/coreutils/manual/html_node/stat-invocation.html"&gt;GNU Coreutils: stat&lt;/a&gt;，文件系统设备号检查，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>