<?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 Field Notes</title><link>https://hk.xbcatm.com/tags/static-web/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://hk.xbcatm.com/tags/static-web/index.xml" rel="self" type="application/rss+xml"/><item><title>Readable evidence before dashboards</title><link>https://hk.xbcatm.com/essays/readable-evidence-before-dashboards/</link><pubDate>Tue, 18 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/essays/readable-evidence-before-dashboards/</guid><description>A release claim needs inspectable observations with time, scope, and provenance; dashboards and individual probes answer different questions.</description><content:encoded>&lt;p&gt;Monitoring dashboards are convenient, but convenience is not the same as evidence. This essay argues that for a small static service, the most auditable form of &amp;ldquo;is the site healthy?&amp;rdquo; is a set of plain-text facts an operator can read, re-run, and paste into a report — not an aggregated panel.&lt;/p&gt;
&lt;p&gt;The distinction matters because the two answer different questions. A dashboard is a lens over many samples, tuned to alert you to trends. A probe is a single, reproducible observation of the public edge. When the job is to prove a release or diagnose a specific request, the probe is the higher-fidelity artifact.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/essay-evidence.detail.en.svg" alt="The scope box limits one matched response to a URL, representation, vantage point and time. Four rows distinguish headers, response-body comparison, TLS checks and a byte manifest. A separate aggregation path preserves raw observations behind trends. Failures and context are retained, secrets redacted, and collection integrity checked." width="720" height="2580"&gt;&lt;figcaption&gt;A response supports a scoped claim. Repeated observations support trends when the underlying records and their provenance remain inspectable.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="the-dashboards-aggregation-is-the-risk"&gt;The dashboard&amp;rsquo;s aggregation is the risk&lt;/h2&gt;
&lt;p&gt;A dashboard turns thousands of raw observations into a series of aggregated panels: average latency, error rate, uptime percentage. Aggregation is where the audit trail thins. An &amp;ldquo;uptime 99.98%&amp;rdquo; figure does not tell you which request failed, which header was wrong, or whether today&amp;rsquo;s release is the one being served. It is a summary that erases the evidence that produced it unless you keep the raw log elsewhere.&lt;/p&gt;
&lt;p&gt;For a confidence property like &amp;ldquo;the edge serves the tree I staged&amp;rdquo;, a dashboard is the wrong tool. A response-body comparison can answer that question for one URL, representation, network vantage point, and instant. A homepage header fetch cannot establish the identity of a whole tree or every CDN node.&lt;/p&gt;
&lt;h2 id="a-probe-you-can-read"&gt;A probe you can read&lt;/h2&gt;
&lt;p&gt;The repository ships a small read-only helper, &lt;code&gt;static/downloads/check-http-headers.sh&lt;/code&gt;, also published at &lt;code&gt;/downloads/check-http-headers.sh&lt;/code&gt;. It prints response headers and the final status/URL. The following is an illustrative excerpt, not a new production measurement:&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;A real capture can directly show the response status, declared media type, and delivered CSP. The abbreviated example above cannot establish a complete no-script policy: inspect the full enforcing header, including any &lt;code&gt;script-src-elem&lt;/code&gt; or &lt;code&gt;script-src-attr&lt;/code&gt; directives that take precedence for their categories. A Report-Only policy does not block execution. A panel that says &amp;ldquo;edge OK&amp;rdquo; should expose the same underlying capture and acceptance criteria. Both still depend on the probe implementation, TLS verification, routing, and retention of the original output. Reading certificate &lt;code&gt;notAfter&lt;/code&gt; adds an expiry observation; it does not establish hostname matching or trust-chain validity.&lt;/p&gt;
&lt;h2 id="replace-signal-loss-with-a-checkable-statement"&gt;Replace signal loss with a checkable statement&lt;/h2&gt;
&lt;p&gt;The mechanism the site favors instead of a dashboard is a contract enforced during build and a set of repeatable collection steps for the release window. Roughly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Offline checks validate the artifact (every internal link, every SVG, the no-script boundary, the checksums).&lt;/li&gt;
&lt;li&gt;Separate public-edge checks record status/headers, response bytes, and TLS identity/expiry. The header helper does not print certificate dates or automatically reject every unexpected HTTP status.&lt;/li&gt;
&lt;li&gt;A byte manifest describes the staged tree; retrieving selected URLs and comparing their bodies establishes sampled delivery parity. A complete claim requires checking all published mappings and representation transformations, not merely hashing the server disk.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each of these is a sentence an operator can paste into an issue or a changelog and a future auditor can re-run. None of them requires a panel to be understood.&lt;/p&gt;
&lt;h2 id="keep-enough-context-to-repeat-the-observation"&gt;Keep enough context to repeat the observation&lt;/h2&gt;
&lt;p&gt;Record UTC time, requested and final URL, method, selected encoding, resolved remote IP, client version, exit status, HTTP status, relevant headers, and body digest. Preserve failures as well as successes. Redact credentials, cookies, and personal data before sharing logs. A proxy error page returning 200 can pass a status-only check; comparing a known body or release marker catches a different failure mode.&lt;/p&gt;
&lt;p&gt;A useful release report distinguishes “this response matched at this time” from “the service met its availability objective over a month.” Retain the raw observations behind aggregate metrics, and make a dashboard link back to them where possible. Neither display format creates trustworthy measurements by itself.&lt;/p&gt;
&lt;h2 id="when-you-genuinely-need-a-dashboard"&gt;When you genuinely need a dashboard&lt;/h2&gt;
&lt;p&gt;None of this says observability is pointless. If you operate many instances, aggregate capacity, or long trends, a dashboard is the right view to see the forest. The claim is narrower: when the question is &amp;ldquo;is this one release healthy at the public edge&amp;rdquo;, a reproducible terminal read is a good starting point. Repeated probes, several vantage points, error budgets, and alerting add evidence about intermittent or regional failures that a single successful request cannot supply. A well-designed dashboard can expose those observations without hiding their provenance.&lt;/p&gt;
&lt;p&gt;The decision heuristic is simple. If a claim can be checked with one command, preserve that command’s output together with its scope and acceptance criteria. Use dashboards where aggregation helps, while retaining access to the underlying evidence. That keeps the site&amp;rsquo;s own operational story as auditable as the site&amp;rsquo;s content.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;Curl documentation&lt;/a&gt;, reading response headers from a single invocation, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://12factor.net/logs"&gt;Twelve-Factor: Logs&lt;/a&gt;, treating logs as event streams an operator can read, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/concepts/observability-primer/"&gt;OpenTelemetry: Observability primer&lt;/a&gt;, the complementary roles of logs, metrics, and traces, checked 2026-10-15.&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;, enforcing versus Report-Only delivery and script directive precedence (working draft), checked 2026-10-18.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Freshness and validation in the two-clock cache model</title><link>https://hk.xbcatm.com/essays/caching-as-two-clocks/</link><pubDate>Fri, 14 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/essays/caching-as-two-clocks/</guid><description>Freshness controls reuse without asking; validation checks a stored representation. A small exercise separates the two decisions.</description><content:encoded>&lt;p&gt;Caching has two separate decisions: may a stored response be reused without contacting the server, and how can the client check whether that response still represents the resource? Calling them “two clocks” is a mnemonic. Freshness really is an age calculation; validation is a comparison, not another timer. A response can be fresh by policy while already differing from a newly deployed origin, without the cache violating HTTP.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/essay-two-clocks.detail.en.svg" alt="The formula age less than lifetime makes age 59 fresh and ages 60 and 61 stale. Stale entries send If-None-Match; a match returns 304, updates metadata and reuses the stored body, while a changed GET returns 200 with a new body. A fresh cached A can still be used after origin B is deployed." width="720" height="1540"&gt;&lt;figcaption&gt;Simplified age example: matching variant, no overriding request directive. no-cache still requires validation; must-revalidate forbids unvalidated stale reuse. Validator matching does not prove byte identity.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="freshness-how-long-to-reuse-without-asking"&gt;Freshness: how long to reuse without asking&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Cache-Control: max-age=60&lt;/code&gt; gives a response a freshness lifetime of 60 seconds. A cache compares that lifetime with the response&amp;rsquo;s current age, accounting for &lt;code&gt;Date&lt;/code&gt;, &lt;code&gt;Age&lt;/code&gt;, transit time, and time stored. At exactly 60 seconds it is stale, not fresh for one more second. Shared caches prefer &lt;code&gt;s-maxage&lt;/code&gt; when present; absent explicit freshness, some responses can receive heuristic freshness.&lt;/p&gt;
&lt;p&gt;A fresh response is usually reusable, but matching the cache key and &lt;code&gt;Vary&lt;/code&gt; fields and obeying request/response directives still matters. &lt;code&gt;no-cache&lt;/code&gt; requires successful validation before reuse even when the response could otherwise be fresh. &lt;code&gt;no-store&lt;/code&gt; prohibits storage; it is not a synonym for revalidate. &lt;code&gt;public&lt;/code&gt; permits shared-cache storage in cases otherwise restricted; it does not specify a duration.&lt;/p&gt;
&lt;p&gt;For mutable HTML, a short explicit lifetime plus validation is often useful. It intentionally permits updates to take up to that lifetime to become visible through a compliant cache. A whole-tree deployment does not invalidate existing browser or CDN entries. Content-addressed assets can instead use a long lifetime when the URL changes with the bytes.&lt;/p&gt;
&lt;h2 id="validation-ask-whether-the-stored-representation-still-matches"&gt;Validation: ask whether the stored representation still matches&lt;/h2&gt;
&lt;p&gt;An &lt;code&gt;ETag&lt;/code&gt; is an opaque validator, not a checksum the client should decode. &lt;code&gt;Last-Modified&lt;/code&gt; is a timestamp validator. Neither header specifies an explicit freshness lifetime, and a server need not supply both. When heuristic freshness is permitted, however, a cache can use &lt;code&gt;Last-Modified&lt;/code&gt; to estimate a lifetime. For example, these illustrative headers express a lifetime and a validator separately:&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;After expiry, &lt;code&gt;If-None-Match: &amp;quot;release-a&amp;quot;&lt;/code&gt; lets a GET receive &lt;code&gt;304 Not Modified&lt;/code&gt; if the selected representation still matches, or a new &lt;code&gt;200&lt;/code&gt; with a body if it changed. The cache updates applicable metadata from the 304 and reuses its stored body. A 304 is not useful without a corresponding stored representation. &lt;code&gt;If-None-Match&lt;/code&gt; uses weak comparison, so successful validation does not necessarily prove byte-for-byte identity; &lt;code&gt;If-Range&lt;/code&gt; and combining partial responses have stronger requirements.&lt;/p&gt;
&lt;p&gt;The server can compute an ETag from bytes or other reliably changing metadata. A strong tag must distinguish representations whose data differs, including content codings where applicable. Sending the same strong ETag for different gzip and identity bytes is not fixed by adding a short lifetime. &lt;code&gt;Vary: Accept-Encoding&lt;/code&gt; also tells a cache which request header affected selection.&lt;/p&gt;
&lt;h2 id="a-small-exercise-that-separates-the-decisions"&gt;A small exercise that separates the decisions&lt;/h2&gt;
&lt;p&gt;This Python model deliberately excludes heuristic freshness, network delay, and variant selection. &lt;code&gt;age&lt;/code&gt; stands for the already calculated current age; the conditional request uses a server validator that has not changed. The model compares two fixed tag strings; it does not implement HTTP entity-tag parsing, tag lists, or the full weak-comparison rules.&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;The first line reuses without validation; the other two validate and reuse after a 304. Set &lt;code&gt;server_etag&lt;/code&gt; to &lt;code&gt;&amp;quot;release-b&amp;quot;&lt;/code&gt;: the stale cases become 200, while the 59-second case still reuses the stored response. That is the permitted visibility delay the freshness policy buys in exchange for lower latency and origin load.&lt;/p&gt;
&lt;h2 id="the-contract-worth-asserting"&gt;The contract worth asserting&lt;/h2&gt;
&lt;p&gt;Test matching and changed validators, freshness at the boundary, and the selected encoding. Also decide what should happen during an origin outage. HTTP can permit stale reuse under defined conditions or extensions; &lt;code&gt;must-revalidate&lt;/code&gt; prohibits reusing the response once stale without successful validation, even when disconnected. This increases correctness pressure at the cost of availability. A validator alone makes no such promise.&lt;/p&gt;
&lt;p&gt;A useful release record therefore states the cache policy, selected representation, observed conditional status, and tolerated update delay. Requiring two validator headers and one 304 cannot prove that every cache has current bytes, prevent all bandwidth waste, or replace testing the actual intermediary configuration.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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;, sections 4.1–4.3 and 5.2 on variants, age, validation, and directives, checked 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;, sections 8.8, 13.1.2, and 13.1.5 on validators and comparison, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>The zero-JavaScript static site is a defensible choice</title><link>https://hk.xbcatm.com/essays/zero-js-as-a-defensible-choice/</link><pubDate>Thu, 13 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/essays/zero-js-as-a-defensible-choice/</guid><description>Why turning off scripting on a content site is a security and reliability decision you can justify with headers, not taste.</description><content:encoded>&lt;p&gt;A static site does not need JavaScript to be defensible; the absence of a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag can itself be the point. This essay argues the engineering case for shipping a site whose script lanes are closed by policy, then explains how to verify the policy at the public edge. The header below is the repository’s intended configuration, not a fresh observation of production.&lt;/p&gt;
&lt;p&gt;The argument has three parts: the threat surface you delete, the contract that makes deletion observable, and the cases where deleting the layer is the wrong call.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/essay-nojs.detail.en.svg" alt="An enforcing no-script browser boundary allows document assets and native interaction but blocks page scripts, inline handlers and JavaScript URLs. Content, publishing, generator and server responsibilities remain. Three separate required checks inspect generated artifacts, delivered CSP and browser behavior." width="720" height="1690"&gt;&lt;figcaption&gt;Assumes the article’s complete enforcing CSP with no more-specific script allowance. Report-Only does not block. The three checks shown are required evidence, not completed tests.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="the-threat-surface-you-delete-by-not-having-a-script-lane"&gt;The threat surface you delete by not having a script lane&lt;/h2&gt;
&lt;p&gt;A page that executes no own or third-party script removes the client-side script dependencies that would otherwise need patching and review. An effective no-script CSP blocks injected script execution in conforming browsers, but HTML injection can still change text, links, and page structure. Trackers can also use images, navigation, or server logs without JavaScript. This site separately avoids third-party browser dependencies; that is a broader requirement than removing script tags. Browser-bundle dependency drift disappears when there is no bundle, while build-tool and server dependencies remain. Each of these is a bounded, nameable surface — the small-systems property of choosing boundaries you can articulate.&lt;/p&gt;
&lt;p&gt;None of this claims the site is &amp;ldquo;secure&amp;rdquo;. It claims the attack surface is narrower and, crucially, easier to reason about because it is enumerated by construction rather than by audit.&lt;/p&gt;
&lt;h2 id="the-header-that-makes-the-boundary-enforceable"&gt;The header that makes the boundary enforceable&lt;/h2&gt;
&lt;p&gt;A taste is not a policy. The difference here is that the server delivers a policy that the browser enforces. The example Caddy configuration in this repository specifies:&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;The &lt;code&gt;script-src 'none'&lt;/code&gt; directive is the enforceable part. With this complete policy and no more specific script allowance, a conforming browser refuses page scripts, inline handlers, and JavaScript URLs. A &lt;code&gt;Content-Security-Policy-Report-Only&lt;/code&gt; header would only report violations, not block them. Later additions of &lt;code&gt;script-src-elem&lt;/code&gt; or &lt;code&gt;script-src-attr&lt;/code&gt; need review because they can override the fallback for their respective script categories.&lt;/p&gt;
&lt;p&gt;The build verifier checks generated artifacts for executable scripts, handlers, and URLs. It cannot prove that a reverse proxy serves the intended CSP. Fetch representative HTML, error pages, and redirect destinations separately; inspect the actual enforcing header and test browser behavior. Source inspection, response inspection, and browser execution are three distinct checks.&lt;/p&gt;
&lt;h2 id="what-the-static-structure-buys-after-the-removal"&gt;What the static structure buys after the removal&lt;/h2&gt;
&lt;p&gt;With no scripting, the content model reverts to something closer to published documents. Caching operates on HTTP representations, not on a page’s later DOM mutations. Static output makes those representations easy to inspect and version; an ETag and freshness policy still depend on server configuration. Reading the served HTML in a terminal, using &lt;code&gt;view-source&lt;/code&gt;, or indexing the tree is straightforward because the artifact is ordinary files on disk. The trade the site accepts is deliberate: no script-managed state, no client-side rendering, and no feedback that needs a JavaScript view model. Native links, keyboard navigation, CSS behavior, and elements such as &lt;code&gt;details&lt;/code&gt; can still provide interaction. For a documentation and operations site that is the correct exchange, and it is worth writing down.&lt;/p&gt;
&lt;h2 id="when-the-zero-js-claim-is-the-wrong-call"&gt;When the zero-JS claim is the wrong call&lt;/h2&gt;
&lt;p&gt;The boundary is a feature, not a badge of purity. A site that genuinely needs client-side interactivity — a realtime dashboard, a document editor, a payment flow — should not contort itself into serving static HTML and then bolting on a workaround. The wrong application of this decision is to shave a product to fit the constraint. The right application is to recognize that content that is read, searched, and archived profits from being a document, and to reserve scripting for where a document is insufficient.&lt;/p&gt;
&lt;p&gt;For this property, the served header is useful evidence for the response sampled. It does not prove universal browser support or that every route has the same policy. Keep the underlying content safe to render, protect publishing access, and maintain the generator and server. The boundary is defensible precisely when its limits are explicit.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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;, policy delivery, script directives, and directive fallback (working draft), checked 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: Content Security Policy (&lt;code&gt;script-src&lt;/code&gt;)&lt;/a&gt;, directives that disable script execution, checked 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: What is cross-site scripting (XSS)?&lt;/a&gt;, script injection and its consequences, checked 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: Content Security Policy&lt;/a&gt;, CSP as a defense-in-depth control, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>A release checklist for a static site</title><link>https://hk.xbcatm.com/notes/a-release-checklist-for-a-static-site/</link><pubDate>Thu, 30 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/notes/a-release-checklist-for-a-static-site/</guid><description>A compact pre-publish check for generated HTML, links, assets, and response headers.</description><content:encoded>&lt;p&gt;A static site is simple to publish, but simplicity does not remove the need for a release check. The useful checks are small enough to run on every release and specific enough to fail for a real reason.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/release-checklist.detail.en.svg" alt="Four numbered release phases build a clean artifact with a pinned generator, require nonempty index.html, 404.html, robots.txt, and sitemap.xml plus reviewed links, test the exact artifact at staged and then public routes, and record commit, generator version, and download checksums. A separate Hugo preview rebuilds source; HEAD checks metadata while GET enables body comparison." width="720" height="2570"&gt;&lt;figcaption&gt;The checks belong to the generated artifact. Clean output does not establish reproducibility; placeholder searches are review aids, with parser-based link checks as the gate. Serve the exact artifact before publishing, compare public GET bodies afterward in both languages, and keep the commit, generator version, and download checksums.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="build-a-clean-artifact"&gt;Build a clean artifact&lt;/h2&gt;
&lt;p&gt;Use a pinned generator version and build into a disposable output directory:&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; collects unused build-cache resources; it does not clean stale published pages. &lt;code&gt;--cleanDestinationDir&lt;/code&gt; removes stale destination files, so use it only on the disposable artifact directory, never a directory containing source or unrelated data. Compare the printed version with the project&amp;rsquo;s pin; printing a version does not enforce it. Record the build inputs and verify the resulting artifact. A clean destination does not establish byte-for-byte reproducibility: timestamps and other nondeterministic inputs need separate control and comparison. Do not edit generated HTML by hand after the build.&lt;/p&gt;
&lt;h2 id="check-the-artifact-not-only-the-source"&gt;Check the artifact, not only the source&lt;/h2&gt;
&lt;p&gt;Inspect the generated tree and verify that expected entry points exist. Run this assertion block as a standalone shell script; it must stop before printing the inventory if any required file is missing or empty:&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;Then search for accidental placeholders, local paths, and development-only hosts:&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;This is a review aid, not an automatic release gate: examples like this checklist deliberately match, including in translated pages. This HTML-only scan does not cover XML feeds or JSON search output. &lt;code&gt;rg&lt;/code&gt; exits 1 when there are no matches and 2 on an error. Review each listed file, distinguish fenced examples from active links or attributes, and use a parser-based link check for the actual gate. A private path in a published link is a release mistake.&lt;/p&gt;
&lt;h2 id="exercise-the-public-routes"&gt;Exercise the public routes&lt;/h2&gt;
&lt;p&gt;An isolated production-like server pointed at this exact artifact gives the strongest pre-release routing check. Hugo’s development server rebuilds content; the following is a separate source preview, not evidence that the already checked &lt;code&gt;public/&lt;/code&gt; bytes were served. Run the first command in one terminal and the requests in another:&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 checks do not compare bodies. Repeat with GET against the staged server, then after publishing compare public bodies with the artifact and check both languages. Confirm that the missing route returns a 404 status in the production server. A pretty error page with a 200 status still breaks monitoring and caches.&lt;/p&gt;
&lt;h2 id="record-the-result"&gt;Record the result&lt;/h2&gt;
&lt;p&gt;Keep the source commit, generator version, and checksum of each downloadable resource in the release record. This makes it possible to compare a live file with the source that produced it without relying on memory.&lt;/p&gt;
&lt;p&gt;The downloadable version of this checklist is in the &lt;a href="https://hk.xbcatm.com/resources/"&gt;Resources&lt;/a&gt; section.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/commands/hugo/"&gt;Hugo command reference&lt;/a&gt;, generator flags for a clean production build, checked 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 verification and header inspection options, checked 2026-07-30.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/resources/"&gt;Static-site release checklist&lt;/a&gt;, downloadable companion resource.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/commands/hugo_server/"&gt;Hugo server command reference&lt;/a&gt;, development rebuilds, memory rendering, and live-reload controls, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Security headers should describe the actual page</title><link>https://hk.xbcatm.com/notes/security-headers-should-describe-the-actual-page/</link><pubDate>Thu, 23 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/notes/security-headers-should-describe-the-actual-page/</guid><description>A compact policy is safer when it matches the resources a static page really loads.</description><content:encoded>&lt;p&gt;Security headers are not a substitute for secure code, but they make the browser enforce useful boundaries. The policy should describe the page that is actually served, not a generic template copied from another project.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/security-layers.detail.en.svg" alt="The actual HTML, local stylesheet and favicon determine CSP resource allowances. Content-Security-Policy enforces; Report-Only reveals violations and needs separate reporting setup. Deliver CSP as a header: frame-ancestors is ignored in meta and has no default-src fallback, form-action needs its own directive, and style-src self alone does not allow inline styles. HSTS is learned from valid HTTPS responses; includeSubDomains at www.example.test covers that host and descendants, not example.test or api.example.test. Check normal, missing and asset responses and verify enforcement in a real browser. No browser test is claimed." width="720" height="3460"&gt;&lt;figcaption&gt;Treat headers as a deployment contract: match actual resources, distinguish enforced and report-only policy, and verify exact host scope and browser behavior.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="start-from-the-resource-graph"&gt;Start from the resource graph&lt;/h2&gt;
&lt;p&gt;If a site serves HTML, a local stylesheet, a favicon, and no executable JavaScript, a narrow policy can be understandable:&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;The &lt;code&gt;script-src 'none'&lt;/code&gt; choice is only correct while the site does not require executable scripts. Adding search, comments, or an analytics tag changes the resource graph and should trigger a deliberate policy review.&lt;/p&gt;
&lt;h2 id="add-transport-and-framing-protections"&gt;Add transport and framing protections&lt;/h2&gt;
&lt;p&gt;For an HTTPS-only site, the following headers are common starting points:&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;Only enable HSTS with includeSubDomains when every subdomain is prepared to serve HTTPS. A header is a contract with future deployments, not merely a score in a scanner.&lt;/p&gt;
&lt;h2 id="check-behavior-not-just-presence"&gt;Check behavior, not just presence&lt;/h2&gt;
&lt;p&gt;Request a normal page, a missing page, and a static asset. Error responses should carry the same important security headers as successful responses. Also confirm that compression and caching do not change the content type or expose an internal path.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://hk.xbcatm.com/resources/"&gt;Caddy example&lt;/a&gt; in this site is a starting point, not a universal policy. Adapt the host names, certificate handling, and application routes to your own deployment.&lt;/p&gt;
&lt;p&gt;Deliver the CSP as a response header. &lt;code&gt;frame-ancestors&lt;/code&gt; is ignored in a meta policy and does not inherit &lt;code&gt;default-src&lt;/code&gt;; &lt;code&gt;form-action&lt;/code&gt; also needs its own directive. &lt;code&gt;style-src 'self'&lt;/code&gt; does not allow inline styles by itself, so test the generated theme, including dark mode. Keep &lt;code&gt;data:&lt;/code&gt; in &lt;code&gt;img-src&lt;/code&gt; only when the page actually needs data images.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Content-Security-Policy-Report-Only&lt;/code&gt; can reveal violations during a rollout, but does not enforce the policy; a reporting endpoint needs separate configuration. Verify enforcement in a real browser as well as checking headers. HSTS is learned from valid HTTPS responses; with &lt;code&gt;includeSubDomains&lt;/code&gt; it also applies to descendants of the host that sets it: a policy on &lt;code&gt;www.example.test&lt;/code&gt; does not cover the parent &lt;code&gt;example.test&lt;/code&gt; or sibling &lt;code&gt;api.example.test&lt;/code&gt;. Gradually increase the lifetime after checking that exact scope.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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 directive names and syntax, checked 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 directives and deployment cautions, checked 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; directive&lt;/a&gt;, response header manipulation directives, checked 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;, response-header delivery, directive fallbacks, framing, and report-only policies; current working draft, checked 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;, valid HTTPS delivery and host/descendant matching in sections 8.1–8.3, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>A release is not complete until the public edge agrees</title><link>https://hk.xbcatm.com/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/notes/a-release-is-not-complete-until-the-public-edge-agrees/</guid><description>Compare the generated artifact with the responses a visitor can actually receive.</description><content:encoded>&lt;p&gt;Static publishing removes application runtime complexity, but it does not remove the boundary between a generated directory and a public response. A release check should cover both.&lt;/p&gt;
&lt;p&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/diagrams/release-path.svg" alt="A small static release path" /&gt;
&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/release-edge.detail.en.svg" alt="Check the exact artifact at a staged server with intended configuration, then repeat at the public edge after switching directories. Pair index.html, the downloadable resource, and 404.html with decoded GET bodies. Compare their bytes separately from expected status, type, cache, and security headers. Repeat English and Chinese samples and retain the previous known-good directory until the new one passes." width="720" height="2200"&gt;&lt;figcaption&gt;Use the same artifact before and after selection. The download row refers to public/downloads/check-http-headers.sh in the article. Supported content codings are decoded before cmp; server transformations need a documented rule. Status and header checks are separate. Retain the previous directory for a deliberate rollback.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="start-from-a-clean-artifact"&gt;Start from a clean artifact&lt;/h2&gt;
&lt;p&gt;Build with the pinned generator into a clean destination. Record the source commit, generator version, output file list, and checksums of files that users download. A clean build catches stale files that a copy-over deployment can accidentally preserve.&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;The file list is evidence, not decoration. If a route disappears, the diff should explain whether the source was removed or the generator failed to emit it.&lt;/p&gt;
&lt;h2 id="exercise-three-response-classes"&gt;Exercise three response classes&lt;/h2&gt;
&lt;p&gt;Before publishing, test the staged artifact with the intended server configuration. After switching directories, repeat these checks at the public edge; probing the old live site cannot validate the new release. Request one HTML page, one static resource, and one missing path. Inspect status, content type, cache policy, and security headers:&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;Run each fragment as a standalone shell script or in a disposable shell session. &lt;code&gt;set -eu&lt;/code&gt; stops this sequential script after a failed build, transfer, or comparison, so a later successful command cannot hide an earlier failure. The missing-path request intentionally does not use &lt;code&gt;-f&lt;/code&gt;: a 404 is expected and should still be captured. Check the final status in each header file: &lt;code&gt;200&lt;/code&gt;, &lt;code&gt;200&lt;/code&gt;, then &lt;code&gt;404&lt;/code&gt;; the byte comparisons do not automate that status check. No redirect is followed here. &lt;code&gt;--compressed&lt;/code&gt; decodes supported content codings before comparison; a deliberate server-side HTML transformation needs its own documented comparison rule. &lt;code&gt;cmp&lt;/code&gt; is silent on equality and returns nonzero on a difference. Repeat for &lt;code&gt;/zh/&lt;/code&gt; and its error page rather than assuming language parity.&lt;/p&gt;
&lt;h2 id="keep-rollback-simple"&gt;Keep rollback simple&lt;/h2&gt;
&lt;p&gt;Do not delete the previous directory until the new one has passed the checks. A rollback should select a known-good directory, not reconstruct one from memory during an incident. The &lt;a href="https://hk.xbcatm.com/resources/"&gt;release checklist&lt;/a&gt; and &lt;a href="https://hk.xbcatm.com/resources/"&gt;HTTP header check&lt;/a&gt; are deliberately small enough to run from a clean shell.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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;, status and representation semantics, checked 2026-07-09.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;, response inspection options, checked 2026-07-09.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/resources/"&gt;Static-site release checklist&lt;/a&gt;, local companion resource.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Content-Type should describe the bytes you actually send</title><link>https://hk.xbcatm.com/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/notes/content-type-should-describe-the-bytes-you-actually-send/</guid><description>Check media types at the public edge and use nosniff to make mismatches fail visibly.</description><content:encoded>&lt;p&gt;A static server can return the correct bytes with the wrong &lt;code&gt;Content-Type&lt;/code&gt;. The page may still look acceptable in one browser, while a download, stylesheet, feed reader, or security policy treats the same response differently. The public response header is the contract; the filename is only an input to server configuration.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/content-type.detail.en.svg" alt="A CSS pathname leads to public-response inspection of status, actual bytes, and Content-Type. Correct CSS and an HTML error page are separate cases. The browser script/style destination boundary applies nosniff; failures require fixing responses, and public CDN/proxy headers must be checked independently of origin headers." width="720" height="2080"&gt;&lt;figcaption&gt;A suffix cannot prove what arrived. Read the public response status, bytes, and media type together; an HTML error page at a CSS path is not a mapping test. Nosniff makes script and style type mismatches visible.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="inspect-representative-formats"&gt;Inspect representative formats&lt;/h2&gt;
&lt;p&gt;Check one HTML document, stylesheet, SVG, feed, and downloadable script. Include headers but discard the body:&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;Replace the example asset paths with files that actually exist in the release; an HTML 404 response at an invented CSS path is not a MIME-mapping test. Expected media types depend on the resource, but they should be specific and stable. HTML should normally include a character set. CSS and SVG should not be served as generic binary data. A shell helper offered for download should not be advertised as HTML.&lt;/p&gt;
&lt;p&gt;This loop displays headers; it does not assert success, inspect the discarded bytes, or exercise browser blocking. Confirm the resource against the release artifact and inspect an actual browser load as a stylesheet or script. Opening the same URL as a document tests a different request destination.&lt;/p&gt;
&lt;h2 id="keep-nosniff-consistent"&gt;Keep nosniff consistent&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;X-Content-Type-Options: nosniff&lt;/code&gt; asks supporting browsers to respect the declared type for script and style destinations instead of guessing. For that Fetch check, a script-like destination needs a JavaScript MIME type, and a style destination needs &lt;code&gt;text/css&lt;/code&gt;; a missing or unusable type also fails. This turns a mismatch into an observable failure rather than browser-dependent interpretation.&lt;/p&gt;
&lt;p&gt;The header does not repair a wrong type or make arbitrary content safe. Check the inventory while deploying it, and fix any rejected script/style response rather than removing the protection:&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="check-the-server-mapping"&gt;Check the server mapping&lt;/h2&gt;
&lt;p&gt;Static servers usually derive types from extensions. If a new extension is served as &lt;code&gt;application/octet-stream&lt;/code&gt;, decide whether that is intentional before adding a global override. A narrow mapping is easier to review than a rule that changes every response.&lt;/p&gt;
&lt;p&gt;Repeat the checks at the public hostname. A CDN or reverse proxy can replace a correct origin header, and a friendly error page can return HTML for a path whose suffix looks like CSS or JavaScript.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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;, representation metadata and &lt;code&gt;Content-Type&lt;/code&gt;, checked 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;, media types and charset examples, checked 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; behavior, checked 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 Standard&lt;/a&gt;, script-like and style destination blocking, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Why a small site can stay static</title><link>https://hk.xbcatm.com/posts/why-a-small-site-can-stay-static/</link><pubDate>Mon, 08 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/posts/why-a-small-site-can-stay-static/</guid><description>Render shared documents during a build, publish their artifacts, and verify the boundary between editable sources and served files.</description><content:encoded>&lt;p&gt;A static site serves documents prepared before their requests arrive. The important question is whether visitors need different server-generated representations, rather than how many pages the site contains. Hundreds of articles can share one publishing model: edit sources, build HTML and assets, validate the output, then expose those files through a web server or CDN.&lt;/p&gt;
&lt;h2 id="put-document-generation-at-the-right-boundary"&gt;Put document generation at the right boundary&lt;/h2&gt;
&lt;p&gt;When an article means the same thing for every reader, assembling it on every request often adds little value. Templates, navigation, translations, and indexes can be resolved during a build. The serving path then needs the published files, not the authoring database or generator. Requests can still be logged, encrypted, cached, and access-controlled; static delivery does not remove these responsibilities.&lt;/p&gt;
&lt;p&gt;Static HTML can include JavaScript and interactive controls. A client-side search index may suit a modest public collection, whereas private search or personalized results need a service that enforces access. Forms, comments, payments, and authenticated mutations likewise require appropriate processing. Identify those boundaries separately: making the article static does not make a browser authoritative for permissions or durable state.&lt;/p&gt;
&lt;p&gt;Build-time generation moves work rather than eliminating it. Large collections can have expensive rebuilds, dependencies can fail, and content freshness is limited by the publishing pipeline. A new source edit is not automatically visible. For time-sensitive material, measure edit-to-publication delay, keep build failures visible, and verify the released document instead of assuming a successful editor save reached readers.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;A common failure is serving the repository root. Source data, build scripts, or private drafts may then become downloadable even though no navigation links mention them. Use a dedicated output directory containing only intended artifacts. Excluding a document from an index is different from excluding its file from publication. Inspect the actual release inventory, including copied assets and generated search data.&lt;/p&gt;
&lt;p&gt;Another failure is updating files individually while requests arrive. An HTML page can reference an asset not yet uploaded, or an old page can lose a dependency deleted during cleanup. Prepare complete releases, preserve necessary old assets, and use the hosting platform&amp;rsquo;s supported switch mechanism. Restoring an old source commit alone does not restore dependencies, artifacts, cache state, or external services.&lt;/p&gt;
&lt;p&gt;Static output also carries injection risks. Untrusted text needs context-appropriate escaping; inserting it directly into HTML can turn an article value into active markup. Escaping text does not validate arbitrary URLs, sanitize intentionally allowed HTML, or protect JavaScript contexts. Review the actual template positions and source trust instead of treating static generation as automatic security.&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.en.svg" alt="The fixture serves only public/index.html. Deleting or editing the separate source leaves version one served; rebuilding publishes version two. The source URL returns 404, and literal HTML-like text is escaped. Only one file is replaced." width="560" height="1832"&gt;
&lt;figcaption&gt;Editing a source does not publish it. Static illustration of the source and its limits. Reported fixture outcomes were not rerun for this graphic.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Save the following as &lt;code&gt;static-build.py&lt;/code&gt; and run &lt;code&gt;python3 static-build.py&lt;/code&gt; with Python 3. It creates a temporary source outside a public directory, builds one HTML file, and serves only that directory over loopback. The script exercises publication with actual HTTP requests and removes its temporary files after closing the server.&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;First, the program deletes the editable source and still receives the previously published bytes. It recreates a changed source but confirms that requests continue returning version one until build runs again. The second build changes the document digest and returns version two. Its literal &lt;code&gt;&amp;lt;b&amp;gt;literal text&amp;lt;/b&amp;gt;&lt;/code&gt; is escaped in the HTML, and a request for &lt;code&gt;/source.json&lt;/code&gt; returns 404. Expect all four behavior flags true and two different published hashes.&lt;/p&gt;
&lt;p&gt;These checks passed with Python 3.14.4. The replacement uses os.replace on the same filesystem for one complete file. It does not demonstrate atomic publication of a multi-resource site, crash durability, automatic rollback, or a particular hosting provider&amp;rsquo;s deployment mechanism. The experiment isolates the source-to-artifact boundary. For a real release, inspect all generated files, test representative links and missing paths, and verify caching alongside artifact availability. Choose static delivery when this boundary matches the product, then operate the build and serving layers with their own observable checks.&lt;/p&gt;
&lt;h2 id="further-reading"&gt;Further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/notes/the-advantage-of-small-systems/"&gt;The advantage of small systems&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/notes/a-static-server-should-have-one-obvious-root/"&gt;A static server should have one obvious root&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="sources"&gt;Sources&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;, static hosting from published HTML, CSS and JavaScript files, checked 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;, static and dynamic response generation, checked 2026-09-13.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>A static server should have one obvious root</title><link>https://hk.xbcatm.com/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/notes/a-static-server-should-have-one-obvious-root/</guid><description>Keep the Caddy file server, generated artifact, and rollback path aligned.</description><content:encoded>&lt;p&gt;The most useful property of a static deployment is that an operator can answer “which directory served this response?” without reading application code. Caddy&amp;rsquo;s &lt;code&gt;root&lt;/code&gt; and &lt;code&gt;file_server&lt;/code&gt; directives provide that boundary directly.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/caddy-root.detail.en.svg" alt="A complete directory is verified before becoming the Caddy root, while the prior known-good directory is retained through smoke tests. A dashed publication tree contains HTML, text and assets. An outward symlink can expose an outside target readable by the service user. Private files remain outside the tree; separate edge checks cover status, content type, redirects, certificate names and missing-route behavior." width="720" height="3010"&gt;&lt;figcaption&gt;Make the serving directory explicit. Audit symlinks and read/traverse permissions, then verify the actual response at the edge.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="make-the-root-explicit"&gt;Make the root explicit&lt;/h2&gt;
&lt;p&gt;A minimal site block can be kept close to the artifact layout:&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;The hostname and path are placeholders; &lt;code&gt;www.example.test&lt;/code&gt; is not a publicly issuable production name. Use an owned, resolvable hostname for production and plan local TLS trust separately for a test environment. The release command should publish a complete directory, verify it, and only then make that directory the configured root. Keep the prior known-good directory until the new one has passed smoke tests.&lt;/p&gt;
&lt;h2 id="check-the-edge-not-only-the-file-tree"&gt;Check the edge, not only the file tree&lt;/h2&gt;
&lt;p&gt;Request an HTML page, a downloadable text file, and a missing path. Compare content types and status codes with the local artifact. A 404 page that returns &lt;code&gt;200&lt;/code&gt; is a deployment bug even if the page looks polished.&lt;/p&gt;
&lt;p&gt;When Caddy manages HTTPS, certificate provisioning and HTTP-to-HTTPS redirects are part of the edge behavior. Test the redirect from the bare HTTP listener and the certificate name from the public hostname. Do not infer success from a healthy process alone.&lt;/p&gt;
&lt;h2 id="a-root-is-a-publication-boundary-not-a-sandbox"&gt;A root is a publication boundary, not a sandbox&lt;/h2&gt;
&lt;p&gt;Keep source control metadata, backups, credentials, and private configuration outside the published tree. Caddy follows symlinks inside the root, so a link can expose files outside it. Check both the artifact contents and the serving user’s read/traverse permissions. Also, merely placing &lt;code&gt;404.html&lt;/code&gt; in the tree does not configure a custom error handler; verify the missing-route status and error-page behavior you intend.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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; directive&lt;/a&gt;, file serving, root, and index defaults, checked 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;, automatic TLS and issuance behavior, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/resources/"&gt;Static-site release checklist&lt;/a&gt;, local companion resource.&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; directive&lt;/a&gt;, response header operations, checked 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;, the DENY framing policy, checked 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;, framing options, checked 2026-10-15.&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;, reserved &lt;code&gt;.test&lt;/code&gt; names, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Publish a static site by switching complete directories</title><link>https://hk.xbcatm.com/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/notes/publish-a-static-site-by-switching-complete-directories/</guid><description>Stage a complete artifact beside the live tree and replace the directory name only after verification.</description><content:encoded>&lt;p&gt;Copying new files directly over a live static directory creates a period where visitors can receive HTML from one release and assets from another. A safer pattern stages one complete tree beside the live tree, verifies it, then changes directory names.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/atomic-switch.detail.en.svg" alt="After same-filesystem and stage verification with one deployer, live contains old and stage contains new. Move one sends live to rollback, leaving live absent while stage and rollback remain. Move two sends stage to live. On success live contains new and rollback old; on failure live stays absent and an explicit restore is attempted. A failed restore requires inspection. Published results require public checks." width="720" height="1890"&gt;&lt;figcaption&gt;These two moves leave live absent between them; crashes can prolong the gap. Restore must be attempted and checked. Old asset URLs must remain reachable across separate browser requests.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="keep-all-release-directories-on-one-filesystem"&gt;Keep all release directories on one filesystem&lt;/h2&gt;
&lt;p&gt;Keep the live tree, staged tree, and rollback tree on the same filesystem. These fragments assume one Bash session, GNU Coreutils, and a single deployer with exclusive access to these names. Stop on a failed command or either side of the copy pipeline:&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;Create a uniquely named stage directory under that parent. Do not reuse an unresolved variable or broad wildcard as a deletion target.&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="populate-and-verify-the-stage"&gt;Populate and verify the stage&lt;/h2&gt;
&lt;p&gt;Copy the generated artifact into the empty directory, then check representative files. The final command displays a count; compare it with the verified artifact’s expected count or manifest rather than treating any number as a pass:&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;Run link and checksum checks against the staged path when the tooling supports a directory argument. Also verify that the serving user can traverse the directories and read the files: &lt;code&gt;mktemp -d&lt;/code&gt; starts with restrictive permissions, and the extracted artifact must supply the intended modes. A stage that has not passed verification must never acquire the live name.&lt;/p&gt;
&lt;h2 id="switch-names-with-an-explicit-rollback-path"&gt;Switch names with an explicit rollback path&lt;/h2&gt;
&lt;p&gt;Resolve every path before the first move:&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;This is a two-step switch, not one atomic replacement: the live path is absent between moves, and a crash can leave it absent until recovery. GNU &lt;code&gt;mv -T&lt;/code&gt; prevents an existing directory from silently becoming a container for the moved tree; it does not serialize competing deployers. Do not proceed if the filesystem check differs. Platform-specific atomic exchange or symlink replacement can remove the missing-name interval, but must be tested with the actual server.&lt;/p&gt;
&lt;h2 id="verify-the-public-edge"&gt;Verify the public edge&lt;/h2&gt;
&lt;p&gt;After the switch, request the homepage, one deep route, one fingerprinted asset, and a missing path. Compare public bytes with the staged artifact. Keep the rollback directory until that check and the observation window complete.&lt;/p&gt;
&lt;p&gt;Even an atomic name switch does not make a browser’s HTML and asset requests one transaction. Keep older fingerprinted assets reachable for cached or in-flight HTML; merely retaining an unserved rollback directory does not do that. A filesystem rename also makes no promise of crash durability without the platform-specific persistence steps.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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;, name replacement semantics, checked 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;, filesystem and replacement behavior, checked 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;, archive creation and extraction, checked 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: Target directory&lt;/a&gt;, &lt;code&gt;-T&lt;/code&gt; and directory nesting hazards, checked 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;, filesystem device-number checks, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>