<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Othmane ETTAIB</title>
    <description>The latest articles on DEV Community by Othmane ETTAIB (@indiecoredev).</description>
    <link>https://dev.to/indiecoredev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4105075%2F70960dad-c409-4283-93e0-b560cba08e8f.png</url>
      <title>DEV Community: Othmane ETTAIB</title>
      <link>https://dev.to/indiecoredev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/indiecoredev"/>
    <language>en</language>
    <item>
      <title>I don't review AI code. My build does.</title>
      <dc:creator>Othmane ETTAIB</dc:creator>
      <pubDate>Tue, 01 Sep 2026 22:47:16 +0000</pubDate>
      <link>https://dev.to/indiecoredev/i-dont-review-ai-code-my-build-does-436d</link>
      <guid>https://dev.to/indiecoredev/i-dont-review-ai-code-my-build-does-436d</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.indiecore.net/blog/build-reviews-ai-code/" rel="noopener noreferrer"&gt;indiecore.net&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There's a ratio I spent a while pretending not to know about. Claude produces a plausible&lt;br&gt;
two-hundred-line diff in roughly a minute. Reading two hundred lines of plausible code&lt;br&gt;
properly — not skimming for shape, actually checking it — takes me fifteen or twenty. That&lt;br&gt;
gap is the whole problem with "never merge AI-generated code without reading it," which is&lt;br&gt;
advice I agree with and could not follow.&lt;/p&gt;

&lt;p&gt;For a few months I scrolled diffs, recognised the shape of things, and merged. It felt like&lt;br&gt;
reviewing.&lt;/p&gt;

&lt;p&gt;What eventually gave it away was the &lt;em&gt;kind&lt;/em&gt; of bug getting through. None of them were subtle&lt;br&gt;
logic errors that careful reading would have caught. They were dull, mechanical things: a&lt;br&gt;
dead internal link left behind by a route rename, an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; with no &lt;code&gt;alt&lt;/code&gt;, a canonical URL&lt;br&gt;
pointing at the wrong host, two cache rules that turned out to concatenate. Every one was&lt;br&gt;
something a fifty-line script could find in milliseconds, and none of them were things I was&lt;br&gt;
ever going to reliably catch by eye at eleven at night.&lt;/p&gt;

&lt;p&gt;So I stopped trying to read faster.&lt;/p&gt;
&lt;h2&gt;
  
  
  The verifier
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;scripts/verify.mjs&lt;/code&gt; is 169 lines of dependency-free Node. It walks the built &lt;code&gt;dist/&lt;/code&gt;&lt;br&gt;
directory and exits non-zero on anything I've decided must never ship.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run check      &lt;span class="c"&gt;# build + verify — the same gate CI runs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It runs on every pull request and the deploy job depends on it, so a branch that fails it&lt;br&gt;
doesn't reach the internet. Not because I'm disciplined; because the pipeline is wired that&lt;br&gt;
way. (That plumbing is in &lt;a href="https://www.indiecore.net/blog/static-site-cloudflare-workers/" rel="noopener noreferrer"&gt;the Cloudflare Workers post&lt;/a&gt;,&lt;br&gt;
along with the five traps that cost me an evening.)&lt;/p&gt;

&lt;p&gt;The link and image checks are the whole idea in miniature. Resolve every site-absolute URL&lt;br&gt;
against what's actually on disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** does a site-absolute URL resolve to a real file? */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolves&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;'&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;?&lt;/span&gt;&lt;span class="dl"&gt;'&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;clean&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DIST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DIST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;statSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isFile&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/href="&lt;/span&gt;&lt;span class="se"&gt;(\/[^&lt;/span&gt;&lt;span class="sr"&gt;"#&lt;/span&gt;&lt;span class="se"&gt;][^&lt;/span&gt;&lt;span class="sr"&gt;"&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;"/g&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;resolves&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;where&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`dead internal link → &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing clever. It's the kind of check that's tedious for a person to do on every page of&lt;br&gt;
every build and free for a machine to do forever.&lt;/p&gt;
&lt;h2&gt;
  
  
  Every check has a bug behind it
&lt;/h2&gt;

&lt;p&gt;I didn't sit down and write a best-practices linter. Each rule went in on the day something&lt;br&gt;
got past me:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The check&lt;/th&gt;
&lt;th&gt;What it's remembering&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;exactly one &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; per page&lt;/td&gt;
&lt;td&gt;a generated template that emitted two&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;alt&lt;/code&gt; on every &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;accessibility regressions Lighthouse caught days later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dead internal links&lt;/td&gt;
&lt;td&gt;a renamed route that left pages pointing at a 404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;_redirects&lt;/code&gt; targets resolve&lt;/td&gt;
&lt;td&gt;a redirect pointing at a page I'd since deleted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;no placeholder or filler copy&lt;/td&gt;
&lt;td&gt;draft text left in a page, one merge from production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blogger markup in output&lt;/td&gt;
&lt;td&gt;migration leftovers surfacing weeks after the migration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;every page listed in &lt;code&gt;sitemap.xml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;new pages that silently never got submitted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Resist the urge to write the exhaustive rulebook up front.&lt;/strong&gt; I tried; you end up with forty&lt;br&gt;
rules, six of which ever fire, and the other thirty-four generate enough noise that you stop&lt;br&gt;
reading the output at all. Which is worse than having no verifier, because now you also&lt;br&gt;
believe you have one.&lt;/p&gt;
&lt;h2&gt;
  
  
  The checks that pay for themselves are invisible
&lt;/h2&gt;

&lt;p&gt;The rules I'd least want to lose guard things I would never notice by looking at the site,&lt;br&gt;
because the site looks fine.&lt;/p&gt;

&lt;p&gt;This one validates the IndexNow key file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/seo-ping.mjs submits URLs under this key; the crawlers reject the&lt;/span&gt;
&lt;span class="c1"&gt;// submission unless the matching file is live at the site root.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;KEY_FILE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-f0-9&lt;/span&gt;&lt;span class="se"&gt;]{8,128}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;indexnow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`key is not 8-128 hex chars: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;         &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;indexnow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`missing key file /&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.txt`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask an agent to "add IndexNow submission" and you'll get a correct submission script and no&lt;br&gt;
key file, because nothing in the request mentions one. It isn't being careless. It has no way&lt;br&gt;
to know what the silent failure costs, and a missing key file fails silently by design — the&lt;br&gt;
crawler just ignores you. Same story with &lt;code&gt;app-ads.txt&lt;/code&gt;, which is read by ad networks and&lt;br&gt;
nobody else, and which is now parsed field-by-field at build time against the IAB spec.&lt;/p&gt;
&lt;h2&gt;
  
  
  Numbers for the things a script can't see
&lt;/h2&gt;

&lt;p&gt;Alongside the verifier there's a Lighthouse budget, and it isn't negotiable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;performance ≥ 90, accessibility ≥ 100, best-practices ≥ 100, seo ≥ 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches the failure the verifier can't: slow drift. No single change makes a site slow;&lt;br&gt;
twenty changes, each fine on its own, do, and you never see the moment it happens because you&lt;br&gt;
were looking at diffs rather than at scores.&lt;/p&gt;

&lt;p&gt;A real one: the grey I use for code comments in these posts failed WCAG contrast. It looked&lt;br&gt;
deliberate. Honestly, it looked good. One-line fix, and I would never have found it by&lt;br&gt;
reading a stylesheet — the budget found it on the pull request (commit &lt;code&gt;d64bce0&lt;/code&gt;, if you're&lt;br&gt;
curious).&lt;/p&gt;

&lt;p&gt;Performance measurement on shared CI runners is noisy, so the gate re-measures instead of&lt;br&gt;
failing on one bad sample:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Only performance is re-measured, and only when it misses.&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;performance&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;BUDGET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;performance&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;RETRIES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;…&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That detail matters more than it looks. &lt;strong&gt;A gate that fails at random gets ignored, then&lt;br&gt;
disabled, then deleted.&lt;/strong&gt; Flaky checks teach you to click through failures, which is the exact&lt;br&gt;
habit you built the gate to prevent.&lt;/p&gt;
&lt;h2&gt;
  
  
  Write the error message for the agent
&lt;/h2&gt;

&lt;p&gt;One thing I'd underrated. Compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ERROR  /blog/index.html: dead internal link → /games/word-slot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with a generic "validation failed". The first I can paste straight into a session and get a&lt;br&gt;
correct fix in one turn; the second starts a conversation. Good error messages were always&lt;br&gt;
worth writing. They're worth roughly double now, because the agent reads them too, and a&lt;br&gt;
precise message is the difference between a fix and a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;It has no opinion about whether the code is any good. It won't tell me a function is a mess,&lt;br&gt;
that an abstraction is wrong, or that the feature was a bad idea in the first place. That's&lt;br&gt;
still my job, and now it's the only reviewing job I have — which is a better trade than it&lt;br&gt;
sounds, because the mechanical layer is where all the volume is.&lt;/p&gt;

&lt;p&gt;It also can't tell me the agent touched something it shouldn't have gone near at all. That's&lt;br&gt;
a different problem and I handle it differently; it's in&lt;br&gt;
&lt;a href="https://www.indiecore.net/blog/blast-radius-rule-ai-coding/" rel="noopener noreferrer"&gt;the blast radius rule&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The transferable bit isn't "write a verifier for your static site." It's the question I now&lt;br&gt;
ask every time I catch something in a diff: could a script have caught this? When the answer&lt;br&gt;
is yes, writing the script beats remembering the lesson, because I don't reliably remember&lt;br&gt;
lessons and the script doesn't get tired.&lt;/p&gt;




&lt;p&gt;Originally published at &lt;strong&gt;&lt;a href="https://www.indiecore.net/blog/build-reviews-ai-code/" rel="noopener noreferrer"&gt;I don't review AI code. My build does.&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The code is on GitHub: &lt;a href="https://gist.github.com/IndieCoreDev/6c707bf89e5d215225f3bdac6e4a4b25" rel="noopener noreferrer"&gt;Both scripts, ready to drop in&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>cicd</category>
      <category>workflow</category>
    </item>
    <item>
      <title>The blast radius rule for AI coding</title>
      <dc:creator>Othmane ETTAIB</dc:creator>
      <pubDate>Tue, 01 Sep 2026 22:34:35 +0000</pubDate>
      <link>https://dev.to/indiecoredev/the-blast-radius-rule-for-ai-coding-4a57</link>
      <guid>https://dev.to/indiecoredev/the-blast-radius-rule-for-ai-coding-4a57</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.indiecore.net/blog/blast-radius-rule-ai-coding/" rel="noopener noreferrer"&gt;indiecore.net&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My domain carries email as well as the website. When I moved this site to Cloudflare I sat&lt;br&gt;
looking at the DNS panel for a good ten minutes before touching anything, because deleting&lt;br&gt;
the wrong record there wouldn't break something I could see. It would break mail delivery:&lt;br&gt;
quietly, days later, for messages that no longer existed to resend.&lt;/p&gt;

&lt;p&gt;I have never once felt that way about a stylesheet.&lt;/p&gt;

&lt;p&gt;That gap is what I use now to decide how much of a task an agent does on its own. Not how&lt;br&gt;
hard the task is, and not how much I trust the model. Just one question — how expensive is&lt;br&gt;
this to undo after it ships?&lt;/p&gt;
&lt;h2&gt;
  
  
  The sorting mistake I made first
&lt;/h2&gt;

&lt;p&gt;My instinct was to hand over the tedious work and keep the interesting work. Boilerplate,&lt;br&gt;
config, CSS, build scripts, redirect maps: all yours. The "real" logic: mine.&lt;/p&gt;

&lt;p&gt;That's backwards, and it took me an embarrassingly long time to see why. The tedious stuff is&lt;br&gt;
tedious &lt;em&gt;because it's plumbing&lt;/em&gt;, and plumbing is precisely the part that touches things&lt;br&gt;
outside your repository — DNS, caches, crawlers, store metadata, ad configuration. Difficulty&lt;br&gt;
and consequence aren't correlated at all. Some of the most trivial edits in this project are&lt;br&gt;
the ones I'd least like to get wrong.&lt;/p&gt;

&lt;p&gt;There's a well-documented pattern now of projects that moved fast with AI for a few months&lt;br&gt;
and then hit a wall: features breaking other features, a codebase nobody understands, a&lt;br&gt;
rewrite costing more than the original build. Reading through those post-mortems, the thing&lt;br&gt;
that struck me is that it isn't mainly a code-quality problem. It's a reversibility problem.&lt;br&gt;
You don't hit a wall because the code is ugly. You hit it because too many changes went&lt;br&gt;
somewhere you can't cheaply back out of.&lt;/p&gt;
&lt;h2&gt;
  
  
  Zone 1 — one command undoes it
&lt;/h2&gt;

&lt;p&gt;Anything that lives in the repo, renders into &lt;code&gt;dist/&lt;/code&gt;, and can be reverted with &lt;code&gt;git revert&lt;/code&gt;&lt;br&gt;
plus a redeploy. Page copy, styles, the generator, build scripts, blog posts, most refactors.&lt;/p&gt;

&lt;p&gt;The agent works alone here. I describe what I want, it does it, CI checks it, I open the&lt;br&gt;
preview URL. If it's wrong it's wrong for about two and a half minutes (the length of a&lt;br&gt;
deploy) and the fix is one command. At that price, reading every line is a worse use of my&lt;br&gt;
attention than occasionally fixing something.&lt;/p&gt;

&lt;p&gt;This is the bulk of the work, and it's where all the speed comes from.&lt;/p&gt;
&lt;h2&gt;
  
  
  Zone 2 — recoverable, but only if you notice
&lt;/h2&gt;

&lt;p&gt;Things that are technically in the repo but whose failure mode is silence. Cache headers,&lt;br&gt;
redirect maps, &lt;code&gt;robots.txt&lt;/code&gt;, canonical URLs, structured data, the sitemap. Anything consumed&lt;br&gt;
by a crawler, a browser cache or a third party instead of by a person.&lt;/p&gt;

&lt;p&gt;Reverting isn't the issue. Nothing tells you. The site looks perfect, and it goes on looking&lt;br&gt;
perfect for three weeks, until you notice traffic is off or a cache never invalidated or a&lt;br&gt;
page was never indexed at all.&lt;/p&gt;

&lt;p&gt;My cache rules looked completely fine in the diff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;/&lt;span class="n"&gt;assets&lt;/span&gt;/&lt;span class="n"&gt;images&lt;/span&gt;/*
  &lt;span class="n"&gt;Cache&lt;/span&gt;-&lt;span class="n"&gt;Control&lt;/span&gt;: &lt;span class="n"&gt;public&lt;/span&gt;, &lt;span class="n"&gt;max&lt;/span&gt;-&lt;span class="n"&gt;age&lt;/span&gt;=&lt;span class="m"&gt;31536000&lt;/span&gt;, &lt;span class="n"&gt;immutable&lt;/span&gt;

/&lt;span class="n"&gt;assets&lt;/span&gt;/*
  &lt;span class="n"&gt;Cache&lt;/span&gt;-&lt;span class="n"&gt;Control&lt;/span&gt;: &lt;span class="n"&gt;public&lt;/span&gt;, &lt;span class="n"&gt;max&lt;/span&gt;-&lt;span class="n"&gt;age&lt;/span&gt;=&lt;span class="m"&gt;86400&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what came back over the wire:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;cache-control: public, max-age=86400, public, max-age=31536000, immutable
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Overlapping rules concatenate. Browsers take the first value, so the year-long immutable&lt;br&gt;
cache did precisely nothing. Nobody reading that diff would flag it, human or model, because&lt;br&gt;
the diff &lt;em&gt;is&lt;/em&gt; fine. Only the response was wrong.&lt;/p&gt;

&lt;p&gt;So the rule in Zone 2 isn't "read more carefully." Reading was never going to find that. It's&lt;br&gt;
verify against reality: either the check goes into the build, which is&lt;br&gt;
&lt;a href="https://www.indiecore.net/blog/build-reviews-ai-code/" rel="noopener noreferrer"&gt;what I mostly do now&lt;/a&gt;, or I &lt;code&gt;curl&lt;/code&gt; the deployed thing and read&lt;br&gt;
what actually came back. "The deploy succeeded" and "the new behaviour is live and correct"&lt;br&gt;
are two different claims and only one of them is checkable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zone 3 — the undo doesn't live in git
&lt;/h2&gt;

&lt;p&gt;Here I don't delegate, and it isn't about competence. It's that I need to be the person who&lt;br&gt;
typed it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DNS records.&lt;/strong&gt; See above. &lt;code&gt;MX&lt;/code&gt; and &lt;code&gt;TXT&lt;/code&gt; are a category of their own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Play Console.&lt;/strong&gt; Store listings, content ratings, data safety declarations. Rolled
out to production, a wrong answer is a policy problem rather than a bug, and the round trip
to fix it is measured in days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Any URL somebody else has already published.&lt;/strong&gt; The privacy policy URL for each of my
games is entered in its Play Console listing. Those aren't mine to break. They get a 301 and
a build check, permanently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets, tokens, scopes.&lt;/strong&gt; A permission set is a decision, not a task.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Anything where the dashboard uses the word "delete".&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common property isn't danger exactly. It's that &lt;code&gt;git revert&lt;/code&gt; can't fix it. If the undo&lt;br&gt;
isn't in version control, I do it by hand, slowly, with the docs open in another window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the agent can't make this call
&lt;/h2&gt;

&lt;p&gt;An agent optimises for finishing the task you described, and it's genuinely good at that.&lt;br&gt;
What it can't weigh is what you'd lose if it's wrong, because that information isn't in the&lt;br&gt;
repository and mostly isn't written down anywhere.&lt;/p&gt;

&lt;p&gt;Nothing in my code says "these five routes are referenced from live Play Store listings that&lt;br&gt;
take days to update." They look like any other routes. When an agent renames one it isn't&lt;br&gt;
being reckless — it's being exactly as careful as the information it was handed.&lt;/p&gt;

&lt;p&gt;Which points at the fix. Write the constraint into the repo, where the work happens. Mine&lt;br&gt;
sits near the top of the README in plain language:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It also hosts the privacy policy for every game. &lt;strong&gt;Those URLs are referenced from Google&lt;br&gt;
Play Console listings — they must not break.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I wrote that sentence for a future human. It happens to work just as well on an agent, which&lt;br&gt;
turned out to be a much bigger deal than I expected and is&lt;br&gt;
&lt;a href="https://www.indiecore.net/blog/context-engineering-repo/" rel="noopener noreferrer"&gt;a whole post of its own&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shrinking the radius beats classifying it
&lt;/h2&gt;

&lt;p&gt;The real leverage isn't getting better at sorting. It's moving work &lt;em&gt;down&lt;/em&gt; a zone until the&lt;br&gt;
sorting stops mattering.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Move&lt;/th&gt;
&lt;th&gt;What it converts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Never commit to &lt;code&gt;main&lt;/code&gt;; everything is a PR&lt;/td&gt;
&lt;td&gt;production mistakes → preview-URL mistakes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A preview deploy per branch&lt;/td&gt;
&lt;td&gt;"looks right in the diff" → "I loaded it"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config in version control, not the dashboard&lt;/td&gt;
&lt;td&gt;untracked drift → a reviewable diff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build-time checks for the silent stuff&lt;/td&gt;
&lt;td&gt;Zone 2 → Zone 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One deploy pipeline, not two&lt;/td&gt;
&lt;td&gt;racing publishes → one gated publish&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row bit me. Cloudflare offers to connect your Git repository directly, and if&lt;br&gt;
you're already deploying from GitHub Actions you quietly end up with two pipelines publishing&lt;br&gt;
the same site: yours, which runs checks, and theirs, which doesn't. Whichever finishes last&lt;br&gt;
wins. A build my pipeline had correctly refused to ship could get published anyway. Two paths&lt;br&gt;
to production means your gate is advisory, and an advisory gate is decoration.&lt;/p&gt;

&lt;p&gt;Every row there is dull infrastructure work. Together they're the reason it's reasonable to&lt;br&gt;
let an agent write most of the code — not because the code got more trustworthy, but because&lt;br&gt;
being wrong got cheap.&lt;/p&gt;

&lt;h2&gt;
  
  
  In practice
&lt;/h2&gt;

&lt;p&gt;Before handing something over I ask what it costs me if this ships broken and I find out in a&lt;br&gt;
week. Two minutes and a revert, and I let it run without reading every line. Wouldn't find&lt;br&gt;
out in a week, and I stop and build a check instead, because reading doesn't catch silent&lt;br&gt;
failures and never has. Undo isn't in git, and I do it myself; it's ten minutes, once, and&lt;br&gt;
it's the ten minutes that was actually worth my time.&lt;/p&gt;

&lt;p&gt;The useful property of that rule is that it has nothing to do with how good the model is.&lt;br&gt;
It's held up unchanged through three of them.&lt;/p&gt;




&lt;p&gt;Originally published at &lt;strong&gt;&lt;a href="https://www.indiecore.net/blog/blast-radius-rule-ai-coding/" rel="noopener noreferrer"&gt;The blast radius rule for AI coding&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>workflow</category>
      <category>googleplay</category>
    </item>
    <item>
      <title>Deploying a static site to Cloudflare Workers</title>
      <dc:creator>Othmane ETTAIB</dc:creator>
      <pubDate>Tue, 01 Sep 2026 21:40:38 +0000</pubDate>
      <link>https://dev.to/indiecoredev/deploying-a-static-site-to-cloudflare-workers-1n2c</link>
      <guid>https://dev.to/indiecoredev/deploying-a-static-site-to-cloudflare-workers-1n2c</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.indiecore.net/blog/static-site-cloudflare-workers/" rel="noopener noreferrer"&gt;indiecore.net&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I moved this site off a hosted blogging platform onto Cloudflare, with GitHub Actions doing&lt;br&gt;
the building and Cloudflare doing the serving. It costs nothing, deploys in about two and a&lt;br&gt;
half minutes, and refuses to publish anything that fails its checks.&lt;/p&gt;

&lt;p&gt;Getting there took longer than it should have. Here is the setup, and the five things that&lt;br&gt;
tripped me up — none of which are obvious from the documentation.&lt;/p&gt;
&lt;h2&gt;
  
  
  The shape of it
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git push
   └─ GitHub Actions
        ├─ build          generate the site
        ├─ verify         dead links, missing images, bad metadata, broken redirects
        ├─ Lighthouse     fail if performance/accessibility/SEO drop below budget
        └─ deploy         upload to Cloudflare
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The important part is that &lt;strong&gt;deploy depends on the checks&lt;/strong&gt;. A broken build never reaches&lt;br&gt;
the internet. Pull requests get a preview URL; merges to &lt;code&gt;main&lt;/code&gt; go live.&lt;/p&gt;

&lt;p&gt;The triggers and permissions that make that safe:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ci-cd.yml&lt;/code&gt;&lt;/strong&gt; — triggers and permissions&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI/CD&lt;/span&gt;

&lt;span class="c1"&gt;# Build and verify every change; deploy previews for PRs and production from main.&lt;/span&gt;
&lt;span class="c1"&gt;# Deploy jobs depend on the quality gates, so nothing ships unverified.&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# The SEO watch ledger is machine-written, is never part of dist/, and is&lt;/span&gt;
    &lt;span class="c1"&gt;# committed daily. Deploying the site again for it would be pure noise — and&lt;/span&gt;
    &lt;span class="c1"&gt;# would re-trigger the SEO ping through workflow_run every single day.&lt;/span&gt;
    &lt;span class="na"&gt;paths-ignore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;_source/seo-watch.json'&lt;/span&gt;
      &lt;span class="c1"&gt;# hub/ publishes to a different repo entirely and never reaches dist/,&lt;/span&gt;
      &lt;span class="c1"&gt;# so a change to it alone has nothing to deploy.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hub/**'&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="c1"&gt;# Least privilege by default; individual jobs elevate only what they need.&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="c1"&gt;# Supersede in-flight runs for a branch, but never interrupt a production deploy.&lt;/span&gt;
&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ci-cd-${{ github.ref }}&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.ref != 'refs/heads/main' }}&lt;/span&gt;

&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# wrangler ships as a pinned devDependency; never phone home from CI&lt;/span&gt;
  &lt;span class="na"&gt;WRANGLER_SEND_METRICS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;permissions: contents: read&lt;/code&gt; at the top means every job starts with the minimum, and only&lt;br&gt;
the one that comments on pull requests gets more. The &lt;code&gt;concurrency&lt;/code&gt; block cancels superseded&lt;br&gt;
runs on a branch but never interrupts a production deploy.&lt;/p&gt;
&lt;h2&gt;
  
  
  Trap 1: "Upload assets" gives you a Worker, not Pages
&lt;/h2&gt;

&lt;p&gt;Cloudflare has two products that host static sites: &lt;strong&gt;Pages&lt;/strong&gt;, the older one, and &lt;strong&gt;Workers&lt;/strong&gt;,&lt;br&gt;
the current one. Their dashboard now puts the &lt;em&gt;Upload assets&lt;/em&gt; button under Workers.&lt;/p&gt;

&lt;p&gt;So you follow a guide that says "create a Pages project", you click the obvious button, and&lt;br&gt;
you end up with a Worker. Your site is live at &lt;code&gt;something.workers.dev&lt;/code&gt;, and the&lt;br&gt;
&lt;code&gt;something.pages.dev&lt;/code&gt; address every tutorial tells you to visit returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DNS_PROBE_FINISHED_NXDOMAIN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing is broken. You simply never created a Pages project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both products work.&lt;/strong&gt; Workers is the one Cloudflare is investing in, and it supports&lt;br&gt;
everything a static site needs — I verified each of these on a live deployment:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Workers static assets&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_redirects&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_headers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom 404 page&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom domains&lt;/td&gt;
&lt;td&gt;yes, if the nameservers are Cloudflare's&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pick Workers and stop worrying about &lt;code&gt;pages.dev&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Trap 2: the API token needs the right template
&lt;/h2&gt;

&lt;p&gt;To deploy from CI you create a scoped API token. The obvious move is to build a custom one&lt;br&gt;
with a single permission. Don't.&lt;/p&gt;

&lt;p&gt;A Worker deploy needs several permissions working together — Workers Scripts, plus read&lt;br&gt;
access to account and user details. Miss one and you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authentication error [code: 10000]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which tells you nothing about which permission is absent. Use the built-in&lt;br&gt;
&lt;strong&gt;"Edit Cloudflare Workers"&lt;/strong&gt; template instead; it ticks exactly the right boxes. Scope it to&lt;br&gt;
your account and your zone, and give it an expiry.&lt;/p&gt;

&lt;p&gt;If you already made a &lt;em&gt;Cloudflare Pages · Edit&lt;/em&gt; token because you thought you were using&lt;br&gt;
Pages, it will not deploy a Worker.&lt;/p&gt;

&lt;p&gt;Test it before wiring it into CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CLOUDFLARE_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...
npx wrangler &lt;span class="nb"&gt;whoami&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten seconds, and it prints the permissions it actually has.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 3: wrangler turns things off when you're not looking
&lt;/h2&gt;

&lt;p&gt;This one cost me the most time. Once you add a &lt;code&gt;wrangler.jsonc&lt;/code&gt;, that file becomes the&lt;br&gt;
complete description of your Worker — and &lt;strong&gt;anything absent from it is treated as disabled.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Deploy with a minimal config and you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;▲ [WARNING] Because 'workers_dev' is not in your Wrangler file,
  it will be disabled for this deployment by default.

▲ [WARNING] Because your 'workers.dev' route is disabled and your
  'preview_urls' setting is not in your Wrangler file, Preview URLs
  will be disabled for this deployment by default.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two warnings among the normal output, and easy to skim past. The result: my staging URL&lt;br&gt;
started returning 404, and per-version preview URLs stopped being generated — which would&lt;br&gt;
have silently broken pull request previews, since the CI job reads the preview URL out of&lt;br&gt;
&lt;code&gt;wrangler versions upload&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the job that publishes a preview per pull request, and reads the URL back out of&lt;br&gt;
wrangler's output:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ci-cd.yml&lt;/code&gt;&lt;/strong&gt; — preview job&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;preview&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy preview&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;lighthouse&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# Skip where secrets are unavailable rather than failing:&lt;/span&gt;
    &lt;span class="c1"&gt;#  - fork PRs never receive them&lt;/span&gt;
    &lt;span class="c1"&gt;#  - Dependabot runs use a separate secret store, so repository secrets are&lt;/span&gt;
    &lt;span class="c1"&gt;#    empty even though the branch lives in this repo&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
      &lt;span class="s"&gt;github.event_name == 'pull_request' &amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="s"&gt;github.event.pull_request.head.repo.full_name == github.repository &amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="s"&gt;github.actor != 'dependabot[bot]'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;preview&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.deploy.outputs.url }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1&lt;/span&gt; &lt;span class="c1"&gt;# v7.0.1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;persist-credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Node&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@820762786026740c76f36085b0efc47a31fe5020&lt;/span&gt; &lt;span class="c1"&gt;# v7.0.0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.nvmrc&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Download site&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c&lt;/span&gt; &lt;span class="c1"&gt;# v8.0.1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dist&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dist&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload preview version&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;CLOUDFLARE_API_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CLOUDFLARE_API_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;CLOUDFLARE_ACCOUNT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CLOUDFLARE_ACCOUNT_ID }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;set -o pipefail&lt;/span&gt;
          &lt;span class="s"&gt;npx wrangler versions upload 2&amp;gt;&amp;amp;1 | tee upload.log&lt;/span&gt;
          &lt;span class="s"&gt;url=$(grep -oE 'https://[a-z0-9-]+\.[a-z0-9-]+\.workers\.dev' upload.log | head -1)&lt;/span&gt;
          &lt;span class="s"&gt;if [ -z "$url" ]; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "::warning::could not parse a preview URL from wrangler output"&lt;/span&gt;
          &lt;span class="s"&gt;else&lt;/span&gt;
            &lt;span class="s"&gt;echo "url=$url" &amp;gt;&amp;gt; "$GITHUB_OUTPUT"&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Comment preview URL&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.deploy.outputs.url != ''&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3&lt;/span&gt; &lt;span class="c1"&gt;# v9.0.0&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;PREVIEW_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.deploy.outputs.url }}&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;const marker = '&amp;lt;!-- cf-preview --&amp;gt;';&lt;/span&gt;
            &lt;span class="s"&gt;const body = `${marker}\n**Preview deployed** → ${process.env.PREVIEW_URL}`;&lt;/span&gt;
            &lt;span class="s"&gt;const { owner, repo } = context.repo;&lt;/span&gt;
            &lt;span class="s"&gt;const issue_number = context.issue.number;&lt;/span&gt;
            &lt;span class="s"&gt;const comments = await github.rest.issues.listComments({ owner, repo, issue_number });&lt;/span&gt;
            &lt;span class="s"&gt;const mine = comments.data.find(c =&amp;gt; c.body?.startsWith(marker));&lt;/span&gt;
            &lt;span class="s"&gt;if (mine) await github.rest.issues.updateComment({ owner, repo, comment_id: mine.id, body });&lt;/span&gt;
            &lt;span class="s"&gt;else await github.rest.issues.createComment({ owner, repo, issue_number, body });&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A dashboard-created Worker has both on by default. Writing a config turns them off. Say so&lt;br&gt;
explicitly — this is the whole config this site runs on:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;wrangler.jsonc&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./node_modules/wrangler/config-schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-site"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compatibility_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-26"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*.workers.dev&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;URL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;alive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;staging&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;per-version&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Preview&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;URLs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;CI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;preview&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;job&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;publishes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`wrangler&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;versions&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;upload`&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;reads&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;preview&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;URL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;its&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;output.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Wrangler&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;disables&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;both&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;when&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;they&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;absent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;file.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"workers_dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"preview_urls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Static-assets-only&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Worker:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Cloudflare&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;serves&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;dist/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;directly.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;_headers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;_redirects&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;inside&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;dist/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;honoured&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;natively.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"assets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"directory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/about/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;about/index.html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;strip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;.html&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;URLs&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"html_handling"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auto-trailing-slash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;unknown&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;paths&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;get&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;dist/&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="err"&gt;.html&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;real&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;status&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"not_found_handling"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"404-page"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Attaching&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;domains&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;here&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;rather&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;than&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;dashboard:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`wrangler&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;deploy`&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;creates&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;custom&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;domains&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;their&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;DNS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;records&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;re-asserts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;them&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;on&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;every&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;deploy.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;custom_domain:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;means&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Cloudflare&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;manages&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;record&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;TLS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;certificate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;each&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;hostname.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"routes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"www.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"custom_domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"custom_domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;not_found_handling: "404-page"&lt;/code&gt; serves your &lt;code&gt;404.html&lt;/code&gt; with a real 404 status.&lt;br&gt;
&lt;code&gt;auto-trailing-slash&lt;/code&gt; makes &lt;code&gt;/about/&lt;/code&gt; resolve to &lt;code&gt;about/index.html&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Trap 4: the Custom Domain dialog matches zones, not hostnames
&lt;/h2&gt;

&lt;p&gt;Adding &lt;code&gt;www.example.com&lt;/code&gt; through the dashboard gave me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No zones match www.example.com.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I own the domain. &lt;code&gt;www&lt;/code&gt; is a subdomain of it, covered by the same zone. But the dialog was&lt;br&gt;
matching against zone names — typing &lt;code&gt;example.com&lt;/code&gt; found it, typing &lt;code&gt;www.example.com&lt;/code&gt; found&lt;br&gt;
nothing.&lt;/p&gt;

&lt;p&gt;The error offers a helpful-looking link to "add the domain to Cloudflare". &lt;strong&gt;Do not click&lt;br&gt;
it.&lt;/strong&gt; That wizard creates a &lt;em&gt;separate zone&lt;/em&gt; for &lt;code&gt;www.example.com&lt;/code&gt; and offers to import DNS&lt;br&gt;
records into it. You would end up with two overlapping zones and a genuine mess.&lt;/p&gt;

&lt;p&gt;The fix is to skip the dashboard. Those &lt;code&gt;routes&lt;/code&gt; entries above create the custom domains and&lt;br&gt;
their DNS records on deploy, and re-assert them every time — so the configuration can't drift.&lt;/p&gt;

&lt;p&gt;One thing to know: attaching fails while an old record is in the way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hostname 'example.com' already has externally managed DNS records (A, CNAME, etc).
Delete them first or try a different hostname. [code: 100117]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete the old records for that hostname, then deploy again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 5: overlapping &lt;code&gt;_headers&lt;/code&gt; rules concatenate
&lt;/h2&gt;

&lt;p&gt;Cache headers looked right in my config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;/&lt;span class="n"&gt;assets&lt;/span&gt;/&lt;span class="n"&gt;images&lt;/span&gt;/*
  &lt;span class="n"&gt;Cache&lt;/span&gt;-&lt;span class="n"&gt;Control&lt;/span&gt;: &lt;span class="n"&gt;public&lt;/span&gt;, &lt;span class="n"&gt;max&lt;/span&gt;-&lt;span class="n"&gt;age&lt;/span&gt;=&lt;span class="m"&gt;31536000&lt;/span&gt;, &lt;span class="n"&gt;immutable&lt;/span&gt;

/&lt;span class="n"&gt;assets&lt;/span&gt;/*
  &lt;span class="n"&gt;Cache&lt;/span&gt;-&lt;span class="n"&gt;Control&lt;/span&gt;: &lt;span class="n"&gt;public&lt;/span&gt;, &lt;span class="n"&gt;max&lt;/span&gt;-&lt;span class="n"&gt;age&lt;/span&gt;=&lt;span class="m"&gt;86400&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What actually came back over the wire:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;cache-control: public, max-age=86400, public, max-age=31536000, immutable
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Every matching rule is applied and the values are joined.&lt;/strong&gt; An image matches both, so it&lt;br&gt;
gets two &lt;code&gt;max-age&lt;/code&gt; values in one header. Browsers take the first — 1 day — and the year-long&lt;br&gt;
immutable cache silently does nothing.&lt;/p&gt;

&lt;p&gt;I only caught it because I curled the response headers after deploying. Make the rules&lt;br&gt;
non-overlapping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;/&lt;span class="n"&gt;assets&lt;/span&gt;/&lt;span class="n"&gt;images&lt;/span&gt;/*
  &lt;span class="n"&gt;Cache&lt;/span&gt;-&lt;span class="n"&gt;Control&lt;/span&gt;: &lt;span class="n"&gt;public&lt;/span&gt;, &lt;span class="n"&gt;max&lt;/span&gt;-&lt;span class="n"&gt;age&lt;/span&gt;=&lt;span class="m"&gt;31536000&lt;/span&gt;, &lt;span class="n"&gt;immutable&lt;/span&gt;

/&lt;span class="n"&gt;assets&lt;/span&gt;/&lt;span class="n"&gt;fonts&lt;/span&gt;/*
  &lt;span class="n"&gt;Cache&lt;/span&gt;-&lt;span class="n"&gt;Control&lt;/span&gt;: &lt;span class="n"&gt;public&lt;/span&gt;, &lt;span class="n"&gt;max&lt;/span&gt;-&lt;span class="n"&gt;age&lt;/span&gt;=&lt;span class="m"&gt;31536000&lt;/span&gt;, &lt;span class="n"&gt;immutable&lt;/span&gt;

/&lt;span class="n"&gt;assets&lt;/span&gt;/&lt;span class="n"&gt;app&lt;/span&gt;.&lt;span class="n"&gt;js&lt;/span&gt;
  &lt;span class="n"&gt;Cache&lt;/span&gt;-&lt;span class="n"&gt;Control&lt;/span&gt;: &lt;span class="n"&gt;public&lt;/span&gt;, &lt;span class="n"&gt;max&lt;/span&gt;-&lt;span class="n"&gt;age&lt;/span&gt;=&lt;span class="m"&gt;86400&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  If your domain also handles email
&lt;/h2&gt;

&lt;p&gt;This is the part that makes people nervous, so it's worth being plain about it.&lt;/p&gt;

&lt;p&gt;Your domain has two independent groups of records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Email&lt;/strong&gt; lives in &lt;code&gt;MX&lt;/code&gt; and &lt;code&gt;TXT&lt;/code&gt; records (SPF, DKIM)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Website&lt;/strong&gt; lives in &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;CNAME&lt;/code&gt; records&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mail servers only read the first group. Browsers only read the second. &lt;strong&gt;Repointing your&lt;br&gt;
website cannot affect mail delivery.&lt;/strong&gt; The only way to break email is to delete an &lt;code&gt;MX&lt;/code&gt; or&lt;br&gt;
&lt;code&gt;TXT&lt;/code&gt; record by hand.&lt;/p&gt;

&lt;p&gt;So: change records one at a time, never use a bulk delete, and if a screen offers to remove&lt;br&gt;
&lt;code&gt;MX&lt;/code&gt; or &lt;code&gt;TXT&lt;/code&gt; records, say no. Take a zone export first — Cloudflare has an &lt;em&gt;Export records&lt;/em&gt;&lt;br&gt;
button — and check afterwards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short MX example.com
dig +short TXT example.com | &lt;span class="nb"&gt;grep &lt;/span&gt;spf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then send a real test email. DNS looking correct is not the same as mail flowing.&lt;/p&gt;

&lt;h2&gt;
  
  
  One deployment system, not two
&lt;/h2&gt;

&lt;p&gt;The Worker's settings page offers to connect a Git repository. If you are deploying from&lt;br&gt;
GitHub Actions, &lt;strong&gt;leave it empty.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Connecting it enables Cloudflare's own build system, and you end up with two pipelines&lt;br&gt;
publishing the same site: yours, which runs checks, and Cloudflare's, which doesn't. They&lt;br&gt;
race, and whichever finishes last wins. A build your pipeline correctly refused to ship can&lt;br&gt;
get published anyway.&lt;/p&gt;

&lt;p&gt;GitHub pushes to Cloudflare. Cloudflare never pulls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worth doing while you're in there
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verify the deploy actually landed.&lt;/strong&gt; Change something with a visible fingerprint — a
header, a version string — and &lt;code&gt;curl&lt;/code&gt; for it. "The deploy succeeded" and "the new build is
serving" are different claims.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirect old URLs, don't drop them.&lt;/strong&gt; A &lt;code&gt;_redirects&lt;/code&gt; file with &lt;code&gt;301&lt;/code&gt; lines preserves
search ranking and keeps any link you've published elsewhere working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget your Lighthouse scores in CI.&lt;/strong&gt; A number that only gets checked when you remember
to check it will drift.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is exotic. It is a static site on free hosting. But the gap between "it&lt;br&gt;
deployed" and "it deployed correctly, and nothing else broke" is where the evening goes.&lt;/p&gt;




&lt;p&gt;Originally published at &lt;strong&gt;&lt;a href="https://www.indiecore.net/blog/static-site-cloudflare-workers/" rel="noopener noreferrer"&gt;Deploying a static site to Cloudflare Workers&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The code is on GitHub: &lt;a href="https://gist.github.com/IndieCoreDev/11369ffb01a68f04f0bfc9f803922b17" rel="noopener noreferrer"&gt;Both files, ready to copy&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>cicd</category>
      <category>githubactions</category>
      <category>hosting</category>
    </item>
  </channel>
</rss>
