<?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: 301ST</title>
    <description>The latest articles on DEV Community by 301ST (@301st).</description>
    <link>https://dev.to/301st</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%2F4043581%2Ffe5435fc-2111-44a0-8981-964a6a57b876.png</url>
      <title>DEV Community: 301ST</title>
      <link>https://dev.to/301st</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/301st"/>
    <language>en</language>
    <item>
      <title>How to audit agent readiness when the scanner cannot reach the site</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Fri, 07 Aug 2026 14:00:58 +0000</pubDate>
      <link>https://dev.to/301st/how-to-audit-agent-readiness-when-the-scanner-cannot-reach-the-site-gg4</link>
      <guid>https://dev.to/301st/how-to-audit-agent-readiness-when-the-scanner-cannot-reach-the-site-gg4</guid>
      <description>&lt;p&gt;Cloudflare runs a scanner that grades a domain on how ready it is for AI agents. We went through &lt;a href="https://301.sh/agent-readiness-audit/" rel="noopener noreferrer"&gt;all twenty one of its checks&lt;/a&gt; in July: which four were worth implementing, and why eight of the failures were the right answer.&lt;/p&gt;

&lt;p&gt;You point it at your site, get a number, and fix what it flags. Then you try to check the version that actually matters — the staging build with the change in it, the site that has not launched, the pages behind a login — and there is nothing to point the scanner at.&lt;/p&gt;

&lt;p&gt;That is not a bug in the scanner. It runs in somebody else's datacenter, so it can only answer for origins that datacenter can reach. Both failure modes are one &lt;code&gt;curl&lt;/code&gt; away, checked 4 August 2026:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://isitagentready.com/api/scan &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    -H 'content-type: application/json' -d '{"url":"http://localhost:3000"}'
{"error":"Invalid URL provided"}

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://isitagentready.com/api/scan &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    -H 'content-type: application/json' -d '{"url":"https://staging.301.sh"}'
{"url":"https://staging.301.sh","scannedAt":"2026-08-04T18:31:38.462Z",
 "siteError":{"httpStatus":530,"statusText":"","bodyPreview":null,
 "retryAfter":null,"server":"cloudflare"}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A local address is rejected outright. A host the scanner cannot resolve returns a site error instead of a scorecard. Not a low score: no score.&lt;/p&gt;

&lt;p&gt;This matters more than it sounds, because of when you want the answer. Taking this site from Level 1 to Level 5 meant a batch of static files, then a &lt;code&gt;/mcp&lt;/code&gt; server, then DNS records, and every one of those steps was a deploy to production first and a scan second. Publish, then find out. For a redirect you would never accept that loop.&lt;/p&gt;

&lt;p&gt;The other place to run the checks is the browser, which reaches whatever you can reach. This article is about what that changes, what it costs, and the one rule that decides whether the result is worth anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the browser has that the datacenter does not
&lt;/h2&gt;

&lt;p&gt;Three things, and only the first one is obvious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reachability.&lt;/strong&gt; &lt;code&gt;localhost:5173&lt;/code&gt;, a staging host behind a VPN, an internal tool, a domain whose DNS is not public yet. If the tab loads, the audit runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A session.&lt;/strong&gt; Your cookies are attached to the page request, so a site that shows an anonymous visitor a login wall gets graded on what a logged-in client actually receives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An in-page runtime.&lt;/strong&gt; Some of what an agent consumes is not an HTTP response at all. WebMCP exposes tools through &lt;code&gt;document.modelContext&lt;/code&gt; inside the page, after scripts run. An HTTP probe from outside cannot see it; a script in the page can.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that keeps a browser audit honest
&lt;/h2&gt;

&lt;p&gt;The session is also the trap, and it is worth understanding before you write your own script, never mind which tool you use.&lt;/p&gt;

&lt;p&gt;An agent readiness audit fetches two very different classes of thing. There is the page, which may legitimately live behind a login. And there are the machine files — &lt;code&gt;robots.txt&lt;/code&gt;, the sitemap, &lt;code&gt;llms.txt&lt;/code&gt;, everything under &lt;code&gt;/.well-known/&lt;/code&gt; — which are public by definition, because the client reading them has no account and never will.&lt;/p&gt;

&lt;p&gt;Send your session to both and you grade a site that nobody but you can see. A &lt;code&gt;/.well-known/&lt;/code&gt; document that only answers for authenticated requests is a document an agent gets a 401 or a login page from. Passing it in your browser tells you nothing true.&lt;/p&gt;

&lt;p&gt;So the split is per request, not per scan:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What is fetched&lt;/th&gt;
&lt;th&gt;Credentials&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The page itself&lt;/td&gt;
&lt;td&gt;&lt;code&gt;include&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;It may be behind a login, and that is the case the external scanner cannot cover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Accept: text/markdown&lt;/code&gt; on the page&lt;/td&gt;
&lt;td&gt;&lt;code&gt;include&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same URL, same session, a different representation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;robots.txt&lt;/code&gt;, &lt;code&gt;sitemap.xml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;omit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Public machine files — judge them as a crawler receives them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;llms.txt&lt;/code&gt;, &lt;code&gt;llms-full.txt&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;omit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Everything under &lt;code&gt;/.well-known/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;omit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Discovery documents are read by clients with no account&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Redirects follow the same logic. The probe follows hops the way a navigation does, each hop attaching its own cookies, and the response records the final URL. A check that passed on a URL you did not request is a check you want to see the address for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes shape when the audit moves into a browser
&lt;/h2&gt;

&lt;p&gt;Moving the audit into the browser is not a free upgrade. It trades one set of blind spots for another, and a tool that hides the trade is worse than the gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DNS-AID needs a different question, not a different tool.&lt;/strong&gt; The extension APIs have no DNS resolver, which is where this write-up originally stopped — wrongly, because a resolver is not what you need. DNS-over-HTTPS is an ordinary &lt;code&gt;fetch&lt;/code&gt;, and its JSON answer carries both halves of the verdict: the SVCB records under &lt;code&gt;_index._agents&lt;/code&gt;, and &lt;code&gt;AD&lt;/code&gt;, the resolver's DNSSEC judgement. That second half matters, because a site publishing records without a validated chain is failed by the scanner too. No token and no account are involved, so the answer is the same for every user. On this site it was &lt;a href="https://301.sh/agent-readiness-audit/" rel="noopener noreferrer"&gt;the last check to pass&lt;/a&gt; — the resolver cache in front of it took about forty minutes of rescans after the records were already live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebMCP only works here.&lt;/strong&gt; In-page detection returns one of three answers, and the third one is the point: detected, confirmed absent, or detection unavailable. A browser without the API, or a page where the injected script could not run, produces "unavailable" — not a failure. Not detectable is not the same as not present, and a check that scores those the same way manufactures a defect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The MCP Server Card needs five addresses.&lt;/strong&gt; The audit &lt;a href="https://301.sh/agent-readiness-audit/" rel="noopener noreferrer"&gt;already flagged this&lt;/a&gt;, and it has not improved: the draft-canonical path, the IETF one, the two Cloudflare probes and the de-facto legacy file are all in play because the specification moved and the scanners did not move with it. A checker that probes one of them reports the state of that address, not the state of your server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it breaks: the number is not one number
&lt;/h2&gt;

&lt;p&gt;Three separate reasons a score you read today will not match a score somewhere else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not-applicable checks leave the denominator.&lt;/strong&gt; The composite is passing checks over applicable ones, and a check that cannot apply is excluded rather than failed. Anything the browser genuinely cannot reach — a resolver that will not answer, an in-page runtime that never ran — drops out there and stays in from outside. Same site, same day, different arithmetic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare's own two surfaces disagree.&lt;/strong&gt; On 30 July 2026 this site read 79 of 100 and Level 5 "Agent-Native" in the web interface, while &lt;code&gt;/api/scan&lt;/code&gt; reported Level 4 for the same domain in the same hour. That is stable, not a glitch we caught mid-deploy, and it means "what level are we" has two correct answers depending on which endpoint you ask.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The check matrix itself moves.&lt;/strong&gt; The MCP Server Card above is one example; DNS-AID is another, resting on an individual Internet-Draft that expires on 28 November 2026, probed through labels that draft does not define. And &lt;code&gt;llms.txt&lt;/code&gt; is not a scored check in either tool: the audit measures content negotiation, so a site can publish a complete &lt;code&gt;llms.txt&lt;/code&gt; surface and score zero on content accessibility, which is exactly how the July audit opened. What counts as readiness is being decided while you measure it.&lt;/p&gt;

&lt;p&gt;The conclusion is not that the score is worthless. It is that the score is only meaningful against itself. What did this origin do yesterday, and what changed after the last deploy. Comparing your number to another site's number compares two arithmetics.&lt;/p&gt;

&lt;h2&gt;
  
  
  The extension
&lt;/h2&gt;

&lt;p&gt;That is what &lt;a href="https://chromewebstore.google.com/detail/agent-readiness-inspector/diofmjhnegmcccocikabageabmaokobd" rel="noopener noreferrer"&gt;Agent Readiness Inspector&lt;/a&gt; does, and shipping it is the occasion for this article. It is our own extension, it runs 22 checks in the browser against whatever origin the current tab is on, and it is free. Firefox and Edge builds are with the stores for review; today the link above is the one that works.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;22 checks, versioned as data.&lt;/strong&gt; Robots and AI crawler rules, sitemaps, &lt;code&gt;Link&lt;/code&gt; headers, Markdown negotiation, Content Signals, Agent Skills, API Catalog, MCP Server Card, OAuth discovery, Web Bot Auth, WebMCP, plus the agentic commerce protocols as an unscored preview. The matrix carries a version and a CI job watches the upstream one for drift, because the section above is a permanent condition rather than a bad month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence per check.&lt;/strong&gt; The response that produced the verdict, including the final URL after redirects, and a copy-ready prompt for the fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saved sites and regressions.&lt;/strong&gt; Scheduled rescans, history, and an alert when a check that used to pass stops passing. Alerts land in a local inbox first; the notification permission is optional and asked for only if you turn it on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local-first.&lt;/strong&gt; Scans, history, settings and alerts live in browser storage. The optional outside comparison uses your own Cloudflare URL Scanner credentials and is off by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is an independent implementation of open web standards, not affiliated with or endorsed by Cloudflare.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which tool to use
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You need&lt;/th&gt;
&lt;th&gt;isitagentready.com&lt;/th&gt;
&lt;th&gt;Agent Readiness Inspector&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A public production site&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staging, localhost, pre-launch, VPN&lt;/td&gt;
&lt;td&gt;no, site error or invalid URL&lt;/td&gt;
&lt;td&gt;yes, if the tab loads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pages behind a login&lt;/td&gt;
&lt;td&gt;anonymous view only&lt;/td&gt;
&lt;td&gt;your session, on the page probes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebMCP in the page&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes, in-page detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS-AID&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes, over DNS-over-HTTPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The score Cloudflare will quote at you&lt;/td&gt;
&lt;td&gt;yes, this is the source&lt;/td&gt;
&lt;td&gt;no, a second opinion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rescan on a schedule, alert on regression&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes, on the machine it is installed on&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where the extension stops
&lt;/h2&gt;

&lt;p&gt;It audits the origin of the tab you have open, on the machine it is installed on, while the browser is running. Scheduled rescans go through a bounded batch per cycle, so a watch list is a handful of sites you care about, not an inventory.&lt;/p&gt;

&lt;p&gt;For getting a site agent-ready before you publish it, that is the whole job, and it costs nothing. The point where it stops is the point where the question changes from "is this site ready" to "are all of them still ready" — every domain in a portfolio, checked on a schedule that does not depend on someone's laptop being awake, with the alert going to whoever is on call. That is the same boundary the &lt;a href="https://301.sh/see-the-redirect-chain-your-browser-followed/" rel="noopener noreferrer"&gt;redirect chain recorder&lt;/a&gt; runs into, for the same reason, and it is what &lt;a href="https://301.st/features" rel="noopener noreferrer"&gt;301.st&lt;/a&gt; does across a portfolio. For the site in front of you, install the extension and read the evidence.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>browser</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A unique click id on a free Cloudflare redirect, no Worker involved</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Thu, 06 Aug 2026 14:00:21 +0000</pubDate>
      <link>https://dev.to/301st/a-unique-click-id-on-a-free-cloudflare-redirect-no-worker-involved-1750</link>
      <guid>https://dev.to/301st/a-unique-click-id-on-a-free-cloudflare-redirect-no-worker-involved-1750</guid>
      <description>&lt;p&gt;You send paid or partner traffic through a redirect and you want every click to arrive at the destination carrying its own identifier — something to join against a conversion later. The obvious tool is a Worker, and for anything that needs to &lt;em&gt;record&lt;/em&gt; the click, a Worker &lt;a href="https://301.sh/count-clicks-on-a-redirect/" rel="noopener noreferrer"&gt;is still the only primitive that can&lt;/a&gt;. But if all you need is for each click to &lt;em&gt;carry&lt;/em&gt; a unique id, there is a way to get one on the free plan, inside a plain Single Redirect, with no code in the request path at all.&lt;/p&gt;

&lt;p&gt;We tested it on this site's zone on 30 July 2026. It works, and three of its edges bite. Here is the rule, the measurements, and the traps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The function that is banned, and the field that is not
&lt;/h2&gt;

&lt;p&gt;The ruleset expression language has &lt;code&gt;uuidv4()&lt;/code&gt;. You cannot use it here. The documentation restricts it to rewrite expressions of Transform Rules, and the API enforces that at validation time. This is the live answer to an attempt to put it in a redirect target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"'concat(\"https://301.sh/?click_id=\", uuidv4(cf.random_seed))' is not a valid
value for target_url because the use of field cf.random_seed is not allowed,
the use of function uuidv4 is not allowed" (code 20083)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both the function and its seed field are named as forbidden. So the expression language cannot &lt;em&gt;mint&lt;/em&gt; randomness in a redirect. What it can do is reuse an identifier Cloudflare has already minted for the request: &lt;code&gt;cf.ray_id&lt;/code&gt;, the ray ID that every request through Cloudflare gets, the same value the &lt;code&gt;CF-RAY&lt;/code&gt; response header and the dashboard logs show. Its field documentation carries no product restriction, and the validator accepts it in a redirect target.&lt;/p&gt;

&lt;h2&gt;
  
  
  One rule, measured
&lt;/h2&gt;

&lt;p&gt;A dynamic Single Redirect. The match is ordinary; the target is an expression instead of a static URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expression:  (http.host eq "301.sh" and http.request.uri.path eq "/go")
Target URL:  concat("https://destination.example/?click_id=", cf.ray_id)
Status:      302, preserve query string off
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five requests in a row through that rule, on this zone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Location: https://301.sh/…/?click_id=a23684c8a885b183-BTS   CF-RAY: a23684c8a885b183-BTS
Location: https://301.sh/…/?click_id=a23684cbfd5a2915-BTS   CF-RAY: a23684cbfd5a2915-BTS
Location: https://301.sh/…/?click_id=a23684cf493d696b-BTS   CF-RAY: a23684cf493d696b-BTS
Location: https://301.sh/…/?click_id=a23684d2ddcf1948-BTS   CF-RAY: a23684d2ddcf1948-BTS
Location: https://301.sh/…/?click_id=a23684d64c0a46d5-BTS   CF-RAY: a23684d64c0a46d5-BTS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three properties, all visible in the output. Every request got a different id. Each id is identical, byte for byte, to the &lt;code&gt;CF-RAY&lt;/code&gt; header of the same response — which means the click id in your destination's logs joins directly against Cloudflare's own logs and dashboard. And the substituted value is the full form with the data center suffix (&lt;code&gt;-BTS&lt;/code&gt; here), not just the sixteen hex characters, so parse accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The preserve_query_string trap
&lt;/h2&gt;

&lt;p&gt;The obvious next step is to keep the campaign parameters the click arrived with. The rule has a switch for that, and combining it with a target expression that already carries a query is where it breaks — measured, not guessed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Incoming request&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;Location&lt;/code&gt; actually sent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;preserve_query_string: true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/go?utm_source=tg&amp;amp;gclid=abc123&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;…/?utm_source=tg&amp;amp;gclid=abc123&lt;/code&gt; — click_id &lt;strong&gt;gone&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;preserve_query_string: true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;…/&lt;/code&gt; — no query at all, even the target's own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;manual merge, flag off&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/go?utm_source=tg&amp;amp;gclid=abc123&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;…/?click_id=a2368ff0…-BTS&amp;amp;utm_source=tg&amp;amp;gclid=abc123&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;preserve_query_string&lt;/code&gt; does not merge the incoming query into your target. It replaces the target's query entirely — including when the incoming query is empty, which deletes the &lt;code&gt;click_id&lt;/code&gt; you just built. The two features are mutually exclusive.&lt;/p&gt;

&lt;p&gt;The working form leaves the switch off and does the merge in the expression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;concat("https://destination.example/?click_id=", cf.ray_id, "&amp;amp;", http.request.uri.query)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One cosmetic edge: when the incoming query is empty, the result ends in a bare &lt;code&gt;&amp;amp;&lt;/code&gt;. Every query parser we care about ignores it, but it is in the URL, so know it is there before you diff logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 301 will replay yesterday's id
&lt;/h2&gt;

&lt;p&gt;The same rule with status 301 substitutes a fresh ray ID per request at the edge — we measured that too. The problem is in front of the edge: the 301 comes back with no &lt;code&gt;Cache-Control&lt;/code&gt; header, and a permanent redirect without explicit freshness information is exactly what browsers are allowed to cache heuristically. A repeat visitor can then be redirected by their own cache, carrying the click id from their first visit, and the request never reaches Cloudflare at all — a duplicate id in your logs and an undercounted click. For this pattern the status you want is 302 or 307.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it breaks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Nothing on Cloudflare's side records the id.&lt;/strong&gt; Redirect is a terminating action in the first request phase; &lt;a href="https://301.sh/count-clicks-on-a-redirect/" rel="noopener noreferrer"&gt;no analytics product ever sees the request&lt;/a&gt;, and the rule cannot write anywhere. The id exists only in the &lt;code&gt;Location&lt;/code&gt; URL. If the destination does not log its query string, the id evaporates in flight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A ray ID is an identifier, not a secret.&lt;/strong&gt; It is unique, but it is not random in any adversarial sense — do not use it as a token that authorizes anything. Join key: yes. Capability: no.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every fetch gets an id, not every human.&lt;/strong&gt; Link prefetchers, scanners and preview bots follow redirects too, and each gets its own perfectly valid click id. The rule cannot tell them apart; dedup and filtering stay the destination's job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The quota is real but roomy.&lt;/strong&gt; Ten Single Redirect rules per zone on the free plan — per zone, not per account — then 25 on Pro, 50 on Business, 300 on Enterprise (checked 30 July 2026). One tracking rule with a wildcard covers a whole path family, so ten goes further than it sounds. Regular expressions need Business; this pattern does not need them.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you need more than a stamp
&lt;/h2&gt;

&lt;p&gt;If the destination is yours, this rule plus your own analytics reading &lt;code&gt;click_id&lt;/code&gt; may be the whole system: no Worker, no code, nothing to maintain, and the id joins against Cloudflare's logs by construction.&lt;/p&gt;

&lt;p&gt;The pattern stops being enough exactly where the &lt;a href="https://301.sh/attribution-when-the-conversion-page-is-not-yours/" rel="noopener noreferrer"&gt;attribution article&lt;/a&gt; starts: the destination is not yours, or you need the click recorded even when the destination logs nothing, or you run this across a portfolio of domains rather than one zone. Recording at the redirect side is precisely the thing a terminating rule cannot do — that side needs to be operated, which is what &lt;a href="https://301.st" rel="noopener noreferrer"&gt;301.st&lt;/a&gt; is for: it owns the redirect end, mints and records the id there, and hands the same id downstream, so the join works whether or not the destination cooperates.&lt;/p&gt;

</description>
      <category>tracking</category>
      <category>redirects</category>
      <category>cloudflare</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Every job your VPS does, and what Cloudflare's free plan does with it</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Wed, 05 Aug 2026 14:00:26 +0000</pubDate>
      <link>https://dev.to/301st/every-job-your-vps-does-and-what-cloudflares-free-plan-does-with-it-5eao</link>
      <guid>https://dev.to/301st/every-job-your-vps-does-and-what-cloudflares-free-plan-does-with-it-5eao</guid>
      <description>&lt;p&gt;The fear of leaving a VPS is rarely about any one feature. A small server quietly does ten jobs at once — serves a site, renews certificates, answers DNS, redirects old URLs, runs a cron script, forwards mail, keeps a database, stores uploads, hosts a small API, rate limits a scraper — and the migration question is really ten questions asked as one. Ask "can Cloudflare replace my VPS" and you get advocacy in both directions. Ask it per job and you get answers you can check.&lt;/p&gt;

&lt;p&gt;So here is the job by job mapping, every number read off the live documentation on 29 July 2026, with the allowance, the price of lifting it, and the honest rows where the answer is "keep the server." At the end: two systems that already live this way, one of them a production SaaS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mapping
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;VPS job&lt;/th&gt;
&lt;th&gt;On the free plan&lt;/th&gt;
&lt;th&gt;The allowance&lt;/th&gt;
&lt;th&gt;Where it bites&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Serve a static site&lt;/td&gt;
&lt;td&gt;Workers static assets&lt;/td&gt;
&lt;td&gt;asset requests "free and unlimited"; 20,000 files, 25 MiB each&lt;/td&gt;
&lt;td&gt;paths routed through Worker code count against 100,000 requests a day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS certificates&lt;/td&gt;
&lt;td&gt;Universal SSL&lt;/td&gt;
&lt;td&gt;90 day certs, auto renewed from 30 days out&lt;/td&gt;
&lt;td&gt;proxied hostnames only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;td&gt;free, with unmetered DDoS protection&lt;/td&gt;
&lt;td&gt;200 records per zone (zones created since September 2024; older free zones keep 1,000)&lt;/td&gt;
&lt;td&gt;nameservers must move to Cloudflare&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redirects&lt;/td&gt;
&lt;td&gt;Single + Bulk Redirects&lt;/td&gt;
&lt;td&gt;10 rules; 10,000 bulk URLs&lt;/td&gt;
&lt;td&gt;&lt;a href="https://301.sh/cloudflare-free-plan-redirect-limits/" rel="noopener noreferrer"&gt;the bulk quota may lag in your account&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cron scripts&lt;/td&gt;
&lt;td&gt;Cron Triggers on a Worker&lt;/td&gt;
&lt;td&gt;5 per account documented, minimum interval 1 minute&lt;/td&gt;
&lt;td&gt;see the next section&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small APIs, dynamic bits&lt;/td&gt;
&lt;td&gt;Workers&lt;/td&gt;
&lt;td&gt;100,000 requests a day, 10 ms CPU each&lt;/td&gt;
&lt;td&gt;computing breaks the CPU budget; serving does not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key value storage&lt;/td&gt;
&lt;td&gt;Workers KV&lt;/td&gt;
&lt;td&gt;1 GB, 100,000 reads / 1,000 writes a day&lt;/td&gt;
&lt;td&gt;1,000 writes is less than it sounds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A real database&lt;/td&gt;
&lt;td&gt;D1 (SQLite)&lt;/td&gt;
&lt;td&gt;5 GB, 5 million reads / 100,000 writes a day&lt;/td&gt;
&lt;td&gt;daily caps error out, not throttle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object storage&lt;/td&gt;
&lt;td&gt;R2&lt;/td&gt;
&lt;td&gt;10 GB, free egress&lt;/td&gt;
&lt;td&gt;free tier covers Standard storage only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mail forwarding&lt;/td&gt;
&lt;td&gt;Email Routing&lt;/td&gt;
&lt;td&gt;free on all plans; 200 rules, 200 destinations&lt;/td&gt;
&lt;td&gt;forwarding, not sending&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firewall, rate limits&lt;/td&gt;
&lt;td&gt;WAF free tier&lt;/td&gt;
&lt;td&gt;5 custom rules, 1 rate limiting rule&lt;/td&gt;
&lt;td&gt;one rate limit means choosing what to protect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request logs&lt;/td&gt;
&lt;td&gt;Workers Logs, Analytics Engine&lt;/td&gt;
&lt;td&gt;logs with sampling; 100,000 data points a day&lt;/td&gt;
&lt;td&gt;Logpush is Enterprise; Workers Trace Events unlock at $5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three of those rows deserve more than a cell.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cron entry that should not fire, and does
&lt;/h2&gt;

&lt;p&gt;The documentation says the free plan allows 5 Cron Triggers per account. The account this blog runs on executes 7, every day, across five distribution Workers — and has for a week, posting to five platforms on schedule. We did not negotiate anything; the limit simply is not enforced against this account.&lt;/p&gt;

&lt;p&gt;That is not a loophole to build on. It is the same phenomenon we documented when &lt;a href="https://301.sh/cloudflare-free-plan-redirect-limits/" rel="noopener noreferrer"&gt;the Bulk Redirects quota said 10,000 and dashboards enforced 20&lt;/a&gt;: Cloudflare's documented numbers and per account entitlements drift apart, in both directions, sometimes for months. Read the number, then test the number. On a migration that habit costs you an afternoon and saves you an architecture built around a limit that was never real for you — or one that assumed an allowance you do not actually have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Certificates are the quiet argument
&lt;/h2&gt;

&lt;p&gt;On a VPS the TLS routine was a yearly renewal, or a certbot timer. That era is ending on a schedule: since 15 March 2026 the CA/Browser Forum rules cap public certificates at 200 days, in March 2027 the cap drops to 100 days, and in March 2029 to 47 days with domain validation reuse down to 10 days. Manual renewal stops being a viable habit and becomes an incident generator — a portfolio of parked domains on a VPS meets that wave at its first short renewal this autumn.&lt;/p&gt;

&lt;p&gt;Behind the proxy, Universal SSL already issues 90 day certificates and renews them starting 30 days out, which sits below every step of that schedule. Of everything in the table, this is the row where the free plan is not a cheaper version of the VPS job but a categorically better one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ceiling is real, and it is measurable
&lt;/h2&gt;

&lt;p&gt;The 10 ms CPU budget is the free plan's one hard wall, and we have measured both sides of it on this site. A Worker that serves — &lt;a href="https://301.sh/mcp-server-on-workers-free-plan/" rel="noopener noreferrer"&gt;our MCP server&lt;/a&gt; answering from static assets — spends 0 to 2 ms of the budget, because waiting on I/O does not count as CPU. A Worker that computes — the template engine we measured in &lt;a href="https://301.sh/agent-readiness-audit/" rel="noopener noreferrer"&gt;the agent readiness audit&lt;/a&gt; — blew through the same budget by a factor of eight. That is the honest split for a migration: routing, redirecting, serving, forwarding all fit with room to spare; parsing, rendering and hashing at volume do not.&lt;/p&gt;

&lt;p&gt;Per this site's house rule, the price of the lift sits next to the limit: the Workers Paid plan at $5 a month raises the per request cap to 30 seconds by default and includes 10 million requests and 30 million CPU milliseconds a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  What honestly stays on the server
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Long running processes.&lt;/strong&gt; Anything that holds a socket open for hours, speaks a protocol that is not HTTP, or needs SSH. Arbitrary TCP proxying exists (Spectrum) but only as an Enterprise extra — that is not a migration, that is procurement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy compute&lt;/strong&gt;, per the measurement above, unless $5 and 30 seconds cover it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The paid tier gaps.&lt;/strong&gt; Snippets are absent from Free entirely, Logpush is Enterprise, Load Balancing and Argo are paid extras. If your VPS job maps to one of these, the free plan answer is "no", not "yes with effort".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control you did not know you had.&lt;/strong&gt; On Cloudflare Registrar you cannot change nameservers at all, and &lt;a href="https://301.sh/auto-renew-on-domain-still-expired/" rel="noopener noreferrer"&gt;renewal is an instruction, not a guarantee&lt;/a&gt;. A migration hands real levers to one vendor; that is a decision, not a detail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And there is a bridge for the undecided: Cloudflare Tunnel is available on all plans, so the VPS can stay exactly where it is, hidden behind the edge with no open ports, while jobs migrate off it one row at a time. Nothing about this table is all or nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two systems that already live like this
&lt;/h2&gt;

&lt;p&gt;This blog is the small case: six Workers — the site plus five distribution posters — seven cron firings a day, a shared KV namespace, 317 static files, an MCP server, at a running cost of exactly zero, with the measurements published as articles.&lt;/p&gt;

&lt;p&gt;The larger case is &lt;a href="https://301.st" rel="noopener noreferrer"&gt;301.st&lt;/a&gt;, the platform this blog belongs to: a production SaaS for domains, redirects and traffic distribution, built as a serverless application on Cloudflare Workers with D1 as the source of truth and KV for cache and sessions. Its architecture documentation states the design goal plainly: customers do not need a paid Cloudflare plan — everything runs on free Workers, and Workers Paid is the growth path, not the entry fee. A platform whose job is other people's domains chose the same table you just read. That is as honest an endorsement of the free plan as we can offer. And where a portfolio outgrows the rows you can run yourself, taking that work over is exactly what the platform exists for.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>dns</category>
      <category>webdev</category>
    </item>
    <item>
      <title>An MCP server on Cloudflare's free plan, measured against the 10 ms CPU limit</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Tue, 04 Aug 2026 14:00:54 +0000</pubDate>
      <link>https://dev.to/301st/an-mcp-server-on-cloudflares-free-plan-measured-against-the-10-ms-cpu-limit-468b</link>
      <guid>https://dev.to/301st/an-mcp-server-on-cloudflares-free-plan-measured-against-the-10-ms-cpu-limit-468b</guid>
      <description>&lt;p&gt;Two things happened this week that turn "can I run an MCP server for free" from a compromise into a plain yes, with one measurable condition. Today the Model Context Protocol shipped its &lt;code&gt;2026-07-28&lt;/code&gt; revision, and the headline change is that the protocol core went stateless: sessions, the &lt;code&gt;Mcp-Session-Id&lt;/code&gt; header, and the &lt;code&gt;initialize&lt;/code&gt; handshake are gone. The release post states the consequence outright:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Any request can now land on any server instance behind a plain round-robin load balancer without needing shared storage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And yesterday, one day ahead of the release, Cloudflare's own documentation flipped. The &lt;code&gt;McpAgent&lt;/code&gt; class — the official path that backed every MCP server with a Durable Object because the transport needed somewhere to keep its session — is now marked deprecated and feature frozen, with a stateless request handler as the recommended replacement for new servers.&lt;/p&gt;

&lt;p&gt;Those two moves close the same gap from both ends. A Worker on the free plan was always a natural place for a small MCP server except for one thing: the transport demanded state, and state meant a Durable Object and a sticky instance. Now the protocol itself promises that every request is self contained. What remains is the free plan's one hard ceiling — 10 ms of CPU per request — and whether a real server fits under it is not a question you argue about. It is a question you measure. So we built one, on this site, and measured it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the revision actually removed
&lt;/h2&gt;

&lt;p&gt;The old lifecycle opened every connection with an &lt;code&gt;initialize&lt;/code&gt; round trip, minted a session id, and required the client to carry it on every call. All of that is deleted. Version, client identity, and capabilities now travel inside each request's &lt;code&gt;_meta&lt;/code&gt; fields, and the one thing a server must implement is &lt;code&gt;server/discover&lt;/code&gt;, which reports its supported versions and capabilities to anyone who asks.&lt;/p&gt;

&lt;p&gt;The transport got stricter in exchange. Every POST must carry an &lt;code&gt;MCP-Protocol-Version&lt;/code&gt; header that matches the version inside the body, plus an &lt;code&gt;Mcp-Method&lt;/code&gt; header mirroring the JSON-RPC method — and &lt;code&gt;Mcp-Name&lt;/code&gt; on tool calls — so load balancers and gateways can route without parsing bodies. A mismatch is a hard &lt;code&gt;400&lt;/code&gt;. Batching stays gone, &lt;code&gt;ping&lt;/code&gt; is removed entirely, an unknown method is now an HTTP &lt;code&gt;404&lt;/code&gt;, and the 2024 era HTTP+SSE transport is formally classified as deprecated. A server that never pushes messages may answer every request with plain JSON and refuse the streaming path with a &lt;code&gt;405&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For a server that only reads, the protocol surface left over is small: &lt;code&gt;server/discover&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt;, &lt;code&gt;tools/call&lt;/code&gt;, and the header discipline. That is the whole list.&lt;/p&gt;

&lt;h2&gt;
  
  
  The server this site now runs
&lt;/h2&gt;

&lt;p&gt;This blog already publishes an agent surface — Markdown mirrors of every article and a &lt;code&gt;posts.json&lt;/code&gt; feed. The MCP server is those same files behind two tools: &lt;code&gt;list_articles&lt;/code&gt; returns the feed, &lt;code&gt;get_article&lt;/code&gt; returns an article's full Markdown. It lives at &lt;code&gt;https://301.sh/mcp&lt;/code&gt; on the same Worker that does the site's &lt;a href="https://301.sh/agent-readiness-audit/" rel="noopener noreferrer"&gt;content negotiation&lt;/a&gt;, with zero dependencies — after the revision, the remaining protocol surface is short enough to write out by hand. Cloudflare ships an SDK route for the same job (&lt;code&gt;createMcpHandler&lt;/code&gt; plus the protocol SDK), which is the right answer the moment you need resources, prompts, or elicitation; a two tool read only server does not.&lt;/p&gt;

&lt;p&gt;The dispatch, condensed to its shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;server/discover&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="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;resultType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;complete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;supportedVersions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-07-28&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;ttlMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="nx"&gt;_600_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cacheScope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tools/list&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="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;resultType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;complete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TOOLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ttlMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="nx"&gt;_600_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;cacheScope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tools/call&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="c1"&gt;// Mcp-Name header must equal params.name, or 400 + HeaderMismatch.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mirror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ASSETS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&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="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.md`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;resultType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;complete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mirror&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="na"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;32601&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`Method not found: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&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 in it computes. Every answer is an already deployed static asset fetched through the assets binding, and that choice is exactly what the measurement rewards.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement
&lt;/h2&gt;

&lt;p&gt;Cloudflare's definition of the limit is the part most people skip past:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;CPU time measures how long the CPU spends executing your Worker code. Waiting on network requests (such as &lt;code&gt;fetch()&lt;/code&gt; calls, KV reads, or database queries) does &lt;strong&gt;not&lt;/strong&gt; count toward CPU time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the 10 ms budget is spent on parsing, validating, and assembling JSON — not on fetching the article body. We sent 40 requests at the production endpoint and read the per invocation &lt;code&gt;cpuTime&lt;/code&gt; that Workers Logs record:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;CPU, ms (min / median / max)&lt;/th&gt;
&lt;th&gt;Wall, ms (min / median / max)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;server/discover&lt;/td&gt;
&lt;td&gt;0 / 0 / 1&lt;/td&gt;
&lt;td&gt;0 / 1 / 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tools/list&lt;/td&gt;
&lt;td&gt;0 / 0 / 0&lt;/td&gt;
&lt;td&gt;0 / 1 / 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;list_articles&lt;/td&gt;
&lt;td&gt;0 / 1 / 1&lt;/td&gt;
&lt;td&gt;10 / 13 / 23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;get_article, largest article&lt;/td&gt;
&lt;td&gt;0 / 1 / 2&lt;/td&gt;
&lt;td&gt;6 / 10 / 88&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The worst case spends 2 ms of the 10 ms budget, and the slowest response — 88 ms of wall time serving the longest article on the site — is almost entirely waiting on the asset fetch, which the meter ignores. A serving tool runs at a fifth of the free ceiling with room to spare, and the free plan's other number, 100,000 requests a day, is a different class of problem entirely for an endpoint agents call a few times per conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where 10 ms actually bites
&lt;/h2&gt;

&lt;p&gt;The ceiling is real; it just lives somewhere else than the transport. When we &lt;a href="https://301.sh/agent-readiness-audit/" rel="noopener noreferrer"&gt;audited this site's agent surface&lt;/a&gt;, we measured a template engine we considered exposing as a service, and it blew through the same 10 ms by a factor of eight. That is the honest division: a tool that serves bytes costs one or two milliseconds; a tool that computes — parses documents, renders templates, hashes at scale — eats the budget almost immediately. Cloudflare does allow occasional overruns ("Each isolate has some built-in flexibility"), but a Worker that exceeds the limit consistently is terminated with error 1102, and burst tolerance is not a plan.&lt;/p&gt;

&lt;p&gt;The lift has a price tag, and per this site's house rule it belongs next to the limit: the Workers Paid plan at $5 a month raises the per request cap to a default of 30 seconds (configurable up to 5 minutes) and includes 30 million CPU milliseconds and 10 million requests a month. If your tools compute, that is the number to compare against, not the free tier's 10 ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it, and what this does not need
&lt;/h2&gt;

&lt;p&gt;The endpoint is live, and one request shows the whole new shape of the protocol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://301.sh/mcp &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'MCP-Protocol-Version: 2026-07-28'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Mcp-Method: server/discover'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"server/discover",
       "params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No session to open, no state to hold, no Durable Object on the bill. If your site already publishes Markdown mirrors or a feed — and after the &lt;a href="https://301.sh/agent-readiness-audit/" rel="noopener noreferrer"&gt;agent readiness audit&lt;/a&gt; ours did — the distance from "static files" to "MCP server" is one route on a Worker you may already be running. The spec finally matches what a small read only server always wanted to be: a plain HTTP endpoint that answers questions about content you have already built. The free plan carries that easily. It is the computing tools that were never going to be free, and now you know the number that decides.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cloudflare</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The conversion fires on someone else's domain. Here is how to attribute it.</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Mon, 03 Aug 2026 14:00:53 +0000</pubDate>
      <link>https://dev.to/301st/the-conversion-fires-on-someone-elses-domain-here-is-how-to-attribute-it-37ae</link>
      <guid>https://dev.to/301st/the-conversion-fires-on-someone-elses-domain-here-is-how-to-attribute-it-37ae</guid>
      <description>&lt;p&gt;You buy the traffic, the sale happens on a domain you cannot touch. An affiliate offer, a product page on a marketplace, a partner's checkout, any program where the "thank you" page belongs to the platform. You cannot place a pixel there, and the ad platform is optimizing blind: the network dashboard shows dozens of conversions, the ad account registers a handful, and the algorithm steers toward clicks, because clicks are all it can see.&lt;/p&gt;

&lt;p&gt;The question surfaces in PPC and affiliate communities year after year, asked about Meta traffic to marketplaces, about ClickBank offers, about white label programs, and the public answers have not moved in a decade. They come in two kinds: "you cannot implement anything on their side", which is true, and "buy a tracker", which starts at three figures a month. At least one asker gave up and publicly put a €100 bounty on any working idea. Nobody collected.&lt;/p&gt;

&lt;p&gt;The fallback everyone lands on instead is counting clicks on the outbound link as stand in conversions. It rots, because a click is not a sale and bots click too — that €100 was in fact offered for a way to filter bots out of exactly this surrogate metric. Money on the symptom, because the cure was not written down anywhere.&lt;/p&gt;

&lt;p&gt;The "nothing on their side" answer is correct, and it is also not the end. Everything you need can live on yours, in the one place every click still passes through: a redirect hop you own. That pattern is what this article builds — with what it costs, where it breaks, and what the ad platforms allow, checked against their live documentation on 27 July 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop: click_id out, postback in
&lt;/h2&gt;

&lt;p&gt;The mechanics have been standard in affiliate networks for a decade. What is missing in every one of those threads is someone laying the loop out end to end.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The click leaves your page through a redirect you host: &lt;code&gt;/go&lt;/code&gt;. The hop generates a &lt;code&gt;click_id&lt;/code&gt;, stores it alongside everything you know about the click — the &lt;code&gt;gclid&lt;/code&gt; from the ad, the source, the timestamp — and sends a &lt;code&gt;302&lt;/code&gt; to the network link with the &lt;code&gt;click_id&lt;/code&gt; in the network's subid parameter.&lt;/li&gt;
&lt;li&gt;The network carries your subid through to the sale. When the conversion happens on their side, their system fires a &lt;strong&gt;postback&lt;/strong&gt;: a server to server request to a URL you registered, carrying the subid back, usually with a payout amount.&lt;/li&gt;
&lt;li&gt;Your postback endpoint looks the &lt;code&gt;click_id&lt;/code&gt; up, and now the conversion is joined to the exact click, campaign, and creative that produced it — on your infrastructure, from server logs, with no pixel anywhere.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note what this does not depend on. No JavaScript on the conversion page, because you have no JavaScript there. No cookies surviving anything, because the join key travels inside URLs and server calls. No consent banner in the path, because nothing runs in the visitor's browser beyond the redirect itself. The hop is server side by construction, which is also why ad blockers never see it: the click is a request to your own host before the browser goes anywhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the hop must not be your ad's final URL
&lt;/h2&gt;

&lt;p&gt;The tempting shortcut is to point the ad directly at &lt;code&gt;/go&lt;/code&gt; and skip the landing page. On Google Ads that shortcut is closed twice over, and knowing exactly how saves you a policy strike.&lt;/p&gt;

&lt;p&gt;First, tracking templates do not carry the user anymore. Parallel tracking is mandatory for Search, Shopping, Display, Video, and Performance Max, and Google's description of it is unambiguous:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Parallel tracking sends customers directly from your ad to your final URL while click measurement happens in the background (without sending them to the tracking URLs first).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The template URL still gets requested — as a background ping from the browser — so a hop in the tracking template can count the click. But it cannot route the visitor, rewrite where they land, or hand the network a subid on the visit that actually converts.&lt;/p&gt;

&lt;p&gt;Second, making the hop the final URL is a listed policy violation. Google's destination requirements name it directly: "Redirects from the final URL that take the user to a different domain" is a destination mismatch. That is the rule that gets affiliate accounts suspended for direct linking.&lt;/p&gt;

&lt;p&gt;So the compliant shape on Google Ads is a bridge: the final URL is a real page on your domain, and the hop is the outbound link on that page. Make sure the page passes its own query string through to the hop link — the &lt;code&gt;gclid&lt;/code&gt; arrives on the landing page URL thanks to auto tagging, and &lt;a href="https://301.sh/find-the-redirect-that-drops-your-gclid/" rel="noopener noreferrer"&gt;a redirect that drops it&lt;/a&gt; ends attribution before it starts. Meta does not run parallel tracking, but the same bridge shape is where community answers converge for Meta traffic too: a page you own, your pixel on it, subids outbound.&lt;/p&gt;

&lt;p&gt;Closing the loop back into the ad platform is the step everyone skips. On Google Ads it has an official name: offline conversion import. You stored the &lt;code&gt;gclid&lt;/code&gt; next to the &lt;code&gt;click_id&lt;/code&gt; at hop time; when the postback lands, you upload the conversion keyed by that &lt;code&gt;gclid&lt;/code&gt;. Google retains a &lt;code&gt;gclid&lt;/code&gt; for 90 days for this purpose, auto tagging is the only prerequisite, and the feature is current, not legacy. The campaign stops optimizing toward clicks and starts optimizing toward the sales your postbacks confirm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the hop on Cloudflare's free plan
&lt;/h2&gt;

&lt;p&gt;A redirect rule cannot do this job, and it is worth being precise about why. Single Redirects are a terminating action in the first request phase — &lt;a href="https://301.sh/count-clicks-on-a-redirect/" rel="noopener noreferrer"&gt;nothing downstream ever sees the click&lt;/a&gt;, and the rule itself cannot write anything anywhere. The expression language cannot even mint an id: &lt;code&gt;uuidv4()&lt;/code&gt; exists, but the documentation restricts it to rewrite expressions of Transform Rules. Among &lt;a href="https://301.sh/every-way-to-redirect-on-cloudflare/" rel="noopener noreferrer"&gt;every way to redirect on Cloudflare&lt;/a&gt;, the only primitive that can mint, store, and answer postbacks is a Worker.&lt;/p&gt;

&lt;p&gt;The whole hop is one Worker with two routes and a D1 table:&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&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;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/go&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT INTO clicks (id, gclid, src, ts) VALUES (?1, ?2, ?3, ?4)&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gclid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&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;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OFFER_URL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&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;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&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="mi"&gt;302&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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/postback&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;POSTBACK_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UPDATE clicks SET payout = ?2, converted = ?3 WHERE id = ?1&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&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;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The free plan quotas fit this comfortably, all checked live on 27 July 2026: Workers allows 100,000 requests a day, D1 writes 100,000 rows a day and reads five million — a click and a postback are one row each. Outgrowing them is a billing decision, not a redesign: the Workers Paid plan at $5 a month moves Workers to 10 million requests and D1 to 50 million written rows a month. The one storage choice to get right: this is a lookup workload, not a counting workload, which is why the table is D1 and not Workers KV — KV writes cap at 1,000 a day on Free, a limit that &lt;a href="https://301.sh/count-clicks-on-a-redirect/" rel="noopener noreferrer"&gt;has misled people before&lt;/a&gt;. Registering the postback URL with the network is one form field on their side: your &lt;code&gt;/postback&lt;/code&gt; address with the &lt;code&gt;key&lt;/code&gt; and their subid macro in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it breaks
&lt;/h2&gt;

&lt;p&gt;The hop is the reliable half of the loop. The other half belongs to the network, and when the join fails, it fails in one of five recurring ways — this list is assembled from a decade of affiliate forum threads asking why the postback never came:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;What it looks like&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Token name mismatch&lt;/td&gt;
&lt;td&gt;Network expects &lt;code&gt;{clickid}&lt;/code&gt;, you sent &lt;code&gt;{click_id}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Their macro list, character for character&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reserved subids&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sub1&lt;/code&gt; is taken by the network's own tracking&lt;/td&gt;
&lt;td&gt;Ask which slot is passthrough&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offer strips subids&lt;/td&gt;
&lt;td&gt;Clicks arrive, subid column is empty&lt;/td&gt;
&lt;td&gt;Test link, then check their reporting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postback registered late&lt;/td&gt;
&lt;td&gt;Conversions before registration are gone for good&lt;/td&gt;
&lt;td&gt;Register before the first paid click&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Never tested by hand&lt;/td&gt;
&lt;td&gt;Everything "configured", nothing verified&lt;/td&gt;
&lt;td&gt;Open your postback URL in a browser with a fake subid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is the one that catches people who did everything else right. A postback URL you have never fired manually is a guess, not a setup: request it once yourself and watch the row update before any money is spent.&lt;/p&gt;

&lt;p&gt;Two structural limits are worth naming too. If the program offers no postback and no subid reporting at all, no hop can conjure the loop — the join needs their half, and your fallback is reconciling their dashboard against your click log by time and creative, which is exactly the manual matching those community threads end up settling for. And the offline conversion window is real: a sale confirmed more than 90 days after the click cannot be imported against its &lt;code&gt;gclid&lt;/code&gt;, which matters for products with long approval cycles.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the Worker is enough, and when it stops being
&lt;/h2&gt;

&lt;p&gt;For one offer and one traffic source, the Worker above genuinely closes the €100 question: every click logged with its &lt;code&gt;gclid&lt;/code&gt;, every conversion joined server side, the ad platform fed real sales instead of proxy clicks. Run it and stop paying for attribution you can build in an afternoon.&lt;/p&gt;

&lt;p&gt;The pattern stops being an afternoon when it multiplies. Ten offers mean ten target URLs and ten postback registrations; three traffic sources mean the hop has to split by source; a dead offer means rerouting yesterday's links without breaking yesterday's subids. At that point the hop is not a script anymore, it is a routing layer with a database behind it — which is the thing &lt;a href="https://301.st" rel="noopener noreferrer"&gt;301.st&lt;/a&gt; already is: streams that route each click by rules, &lt;code&gt;click_id&lt;/code&gt; minted and logged on every hop, postbacks received and matched as the default path, not as your weekend project. For a single campaign, keep the Worker. It is honest work and it is yours.&lt;/p&gt;

</description>
      <category>tracking</category>
      <category>redirects</category>
      <category>cloudflare</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to see the redirect chain your browser actually followed</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Sun, 02 Aug 2026 14:00:53 +0000</pubDate>
      <link>https://dev.to/301st/how-to-see-the-redirect-chain-your-browser-actually-followed-1ifm</link>
      <guid>https://dev.to/301st/how-to-see-the-redirect-chain-your-browser-actually-followed-1ifm</guid>
      <description>&lt;p&gt;A link lands somewhere it should not, and you need to know what happened between the click and the final page. The standard first move is &lt;code&gt;curl&lt;/code&gt;, and it is the right one: the &lt;a href="https://301.sh/find-the-redirect-that-drops-your-gclid/" rel="noopener noreferrer"&gt;60 second gclid test&lt;/a&gt; on this site is built on &lt;code&gt;curl -sIL&lt;/code&gt;, and for server redirects it tells the truth. Then the chain looks clean in the terminal while the browser still ends up somewhere else, and the tool has quietly run out.&lt;/p&gt;

&lt;p&gt;The reason is that only some redirects live in HTTP responses. The rest happen inside the page, or inside the browser itself, and no amount of &lt;code&gt;-L&lt;/code&gt; will surface them. This article is about seeing a chain, not building one; every way to create a redirect on Cloudflare is &lt;a href="https://301.sh/every-way-to-redirect-on-cloudflare/" rel="noopener noreferrer"&gt;its own comparison&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three redirects curl cannot show you
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A JavaScript redirect is a 200.&lt;/strong&gt; The server answers with a normal page, the page runs &lt;code&gt;location.replace()&lt;/code&gt; or sets &lt;code&gt;location.href&lt;/code&gt;, and the browser navigates. &lt;code&gt;curl&lt;/code&gt; prints &lt;code&gt;HTTP/2 200&lt;/code&gt;, reports no redirect at all, and exits. Every interstitial, every tracking hop built on a script, every "checking your browser" page falls in this class.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A meta refresh is also a 200.&lt;/strong&gt; &lt;code&gt;&amp;lt;meta http-equiv="refresh" content="0;url=..."&amp;gt;&lt;/code&gt; sits in the HTML body, and &lt;code&gt;curl&lt;/code&gt; does not parse HTML. Same clean status line, same silent navigation in the browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An HSTS upgrade never leaves the browser.&lt;/strong&gt; After one visit to a site that sends &lt;code&gt;Strict-Transport-Security&lt;/code&gt;, the browser rewrites &lt;code&gt;http://&lt;/code&gt; to &lt;code&gt;https://&lt;/code&gt; before any request is made. DevTools shows it as &lt;code&gt;307 Internal Redirect&lt;/code&gt;, a response the server never sent. &lt;code&gt;curl&lt;/code&gt; keeps no HSTS state by default, so it hits the &lt;code&gt;http://&lt;/code&gt; URL fresh every time and shows you the server's &lt;code&gt;301&lt;/code&gt;, a hop your returning visitors stopped taking long ago.&lt;/p&gt;

&lt;p&gt;On top of the mechanics there is the session. &lt;code&gt;curl&lt;/code&gt; carries no cookies, so a chain that branches on login state, a consent cookie, or an A/B assignment sends &lt;code&gt;curl&lt;/code&gt; down a different path than the person who reported the problem. The chain you tested is not the chain they followed, even when both are real.&lt;/p&gt;

&lt;h2&gt;
  
  
  DevTools shows it, if you hold it right
&lt;/h2&gt;

&lt;p&gt;The browser obviously knows the full chain, and the Network panel will show it, with ceremony. Preserve log has to be on, otherwise the panel clears itself on every navigation and the hop you care about erases its own evidence. The Doc filter cuts the noise from the hundred subrequests a modern page makes. Server hops then appear as separate rows with their &lt;code&gt;301&lt;/code&gt; and &lt;code&gt;302&lt;/code&gt; statuses.&lt;/p&gt;

&lt;p&gt;The client side hops are there too, but unlabeled. A JavaScript redirect is just another document request; nothing in the row says "this was a redirect", and connecting it to the page that triggered it means reading the Initiator column. A meta refresh looks the same. Do this once and it is five minutes of archaeology. Do it for every report and you start wanting the browser to just keep the record.&lt;/p&gt;

&lt;h2&gt;
  
  
  A recorder instead of a stakeout
&lt;/h2&gt;

&lt;p&gt;That is what &lt;a href="https://chromewebstore.google.com/detail/redirect-inspector/jkeijlkbgkdnhmejgofbbapdbhjljdgg" rel="noopener noreferrer"&gt;Redirect Inspector&lt;/a&gt; does. It is our own open source extension (&lt;a href="https://github.com/investblog/redirect-inspector" rel="noopener noreferrer"&gt;Apache 2.0, code on GitHub&lt;/a&gt;), and version 2.3 shipped as its largest update yet, which is the occasion for this article. The extension listens to the browser's own request pipeline and records every chain as you browse: server hops with their status codes, client side navigations (a script or a meta refresh) marked with a &lt;code&gt;JS&lt;/code&gt; badge, and the browser's internal upgrades marked &lt;code&gt;HSTS&lt;/code&gt;. Chains are grouped by browsing session, tracking pixels and ad beacons are filtered out by default, and each card carries the total chain time with per hop timing in the analysis drawer.&lt;/p&gt;

&lt;p&gt;What 2.3 added, briefly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check a URL without visiting it.&lt;/strong&gt; Paste a URL into the dock at the bottom of the panel; it opens in an invisible background tab, the chain is captured through the normal pipeline, and the tab closes itself. The suspicious link from the report gets traced without ever taking over your screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export that fits a bug report.&lt;/strong&gt; Copy a clean summary, download the raw chain as JSON, or copy a ready &lt;code&gt;curl&lt;/code&gt; command that reproduces the server side of the chain, which closes the loop back to the terminal where the investigation started.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A side panel instead of a popup&lt;/strong&gt; on Chrome and Edge, so the chain list stays open while you browse. Firefox keeps the popup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local analysis.&lt;/strong&gt; Nine checks per chain: loops, hops that bounce back and forth, chain length, mixed redirect types, auth bounces, consent hops, tracking noise, CDN detection, final outcome.&lt;/li&gt;
&lt;li&gt;Search across all captured URLs, an undoable Clear, keyboard shortcuts, and full UI translations in seven languages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The part that matters for a tool watching your traffic: everything runs locally. No accounts, no analytics, no telemetry, and by default no network requests at all; captured chains live in browser storage and never leave the machine. It is on the &lt;a href="https://chromewebstore.google.com/detail/redirect-inspector/jkeijlkbgkdnhmejgofbbapdbhjljdgg" rel="noopener noreferrer"&gt;Chrome Web Store&lt;/a&gt;, &lt;a href="https://addons.mozilla.org/firefox/addon/redirect-inspector/" rel="noopener noreferrer"&gt;Firefox Add-ons&lt;/a&gt;, and &lt;a href="https://microsoftedge.microsoft.com/addons/detail/ckblhiaefgkhpgilekhcpapnkpihdlaa" rel="noopener noreferrer"&gt;Edge Add-ons&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which tool sees what
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You need&lt;/th&gt;
&lt;th&gt;curl -sIL&lt;/th&gt;
&lt;th&gt;DevTools Network&lt;/th&gt;
&lt;th&gt;Redirect Inspector&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Server hops (301, 302, 307, 308)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;with Preserve log on&lt;/td&gt;
&lt;td&gt;yes, recorded as you browse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HSTS upgrade&lt;/td&gt;
&lt;td&gt;no, sees the server 301 every time&lt;/td&gt;
&lt;td&gt;shown as 307 Internal Redirect&lt;/td&gt;
&lt;td&gt;labeled HSTS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JS and meta refresh&lt;/td&gt;
&lt;td&gt;no, both are a 200&lt;/td&gt;
&lt;td&gt;an unlabeled document request&lt;/td&gt;
&lt;td&gt;captured, marked as a client redirect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The chain your session gets (cookies, login)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timing&lt;/td&gt;
&lt;td&gt;one total with &lt;code&gt;-w&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;per request, spread across rows&lt;/td&gt;
&lt;td&gt;total per chain, delta per hop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A record you can search and export later&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;cleared when the panel closes&lt;/td&gt;
&lt;td&gt;sessions, search, JSON and curl export&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Online redirect checkers are missing from the table on purpose: they request the URL from their own datacenter, so a chain that branches on country, language, or cookies shows you the datacenter's version, which is the same blind spot &lt;code&gt;curl&lt;/code&gt; has plus somebody else's IP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the extension stops
&lt;/h2&gt;

&lt;p&gt;The extension records what &lt;strong&gt;your&lt;/strong&gt; browser was sent. A chain that branches by geography still shows you the branch for your country and nobody else's, and what Googlebot sees on the same URL is a different question entirely. The URL check is a real navigation in an inactive tab, so your cookies apply and the visit lands in your history; it is a convenience, not an isolation chamber. And it can only look at chains one browser at a time, on the machine where it is installed.&lt;/p&gt;

&lt;p&gt;For debugging the link in front of you, that is the whole job, and the extension is free. The point where it stops is the point where the question changes from "what happened to this click" to "which of my domains are redirecting wrong right now". Answering that means sending the same probe to every domain on a schedule and comparing against yesterday, which no browser tool can do. The &lt;a href="https://301.sh/audit-300-domains-one-script/" rel="noopener noreferrer"&gt;audit script&lt;/a&gt; is the manual version of that answer, and &lt;a href="https://301.st" rel="noopener noreferrer"&gt;301.st&lt;/a&gt; is the version that runs continuously, across the portfolio, with an alert when a chain changes. For the tab you have open, install the extension and read the chain.&lt;/p&gt;

</description>
      <category>redirects</category>
      <category>browser</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Redirecting domains you own to one site, without tripping Google's spam rules</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Sat, 01 Aug 2026 14:00:16 +0000</pubDate>
      <link>https://dev.to/301st/redirecting-domains-you-own-to-one-site-without-tripping-googles-spam-rules-1d5l</link>
      <guid>https://dev.to/301st/redirecting-domains-you-own-to-one-site-without-tripping-googles-spam-rules-1d5l</guid>
      <description>&lt;p&gt;You own a small pile of domains — an old brand you moved away from, a couple of typo variants you grabbed so nobody else would, a name from a project that folded — and you want them all pointing at your current site. Then you read that redirecting domains is a spam signal now, that Google's latest update torched sites for doing exactly that, and you stop with your hand on the button.&lt;/p&gt;

&lt;p&gt;Two different things are being run together under one scary headline. One of them Google is actively penalizing. The other Google names, in its own policy, as a legitimate reason to redirect. The whole question is which one you are doing — and the line is clearer than the headlines make it sound. This is the SEO side of the &lt;a href="https://301.sh/redirect-200-parked-domains/" rel="noopener noreferrer"&gt;parked-domain runbook&lt;/a&gt; and of &lt;a href="https://301.sh/every-way-to-redirect-on-cloudflare/" rel="noopener noreferrer"&gt;every way to redirect on Cloudflare&lt;/a&gt;: the same &lt;code&gt;301&lt;/code&gt;, and whether Google honours it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Google actually went after
&lt;/h2&gt;

&lt;p&gt;The thing that got hit has a name in Google's spam policies:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Expired domain abuse is where an expired domain name is purchased and repurposed primarily to manipulate search rankings by hosting content that provides little to no value to users.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the play: buy a dropped domain that still carries ranking signals from its old life, point it at your site, and inherit authority you never earned. Google's March 2026 spam update did not invent a rule for this — expired domain abuse was codified back in 2024 — it enforced the existing one harder and faster. The market felt it. The advice in the expired-domain trade has visibly moved from hunting aged domains to starting fresh ones, and the sites that leaned hardest on authority-by-redirect are the ones that lost traffic. If the plan was to buy a stranger's old authority and &lt;code&gt;301&lt;/code&gt; it into your rankings, that plan is done.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Google still blesses
&lt;/h2&gt;

&lt;p&gt;Now the other thing. Redirecting a domain you own to a site you own, because the two belong together, is not on any spam list. It is the opposite: Google's own policy on sneaky redirects lists consolidating pages among the legitimate reasons to redirect, right next to moving to a new address. What makes a redirect sneaky is intent.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When examining if a redirect is sneaky, consider whether or not the redirect is intended to deceive either the users or search engines.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is no deception in sending an old brand's domain to that brand's current home. The mechanics agree: a &lt;code&gt;301&lt;/code&gt; to a topically related destination still passes its signals, the way it has for years. Where it stops working is relevance. A &lt;code&gt;301&lt;/code&gt; from an unrelated domain gets treated like a soft &lt;code&gt;404&lt;/code&gt;, and Google ignores the equity rather than handing it over. That is the tell for the whole distinction — a relevant consolidation is a redirect Google honours, an unrelated authority grab is a redirect Google declines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one are you doing
&lt;/h2&gt;

&lt;p&gt;The line, as a handful of questions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Consolidation — do it&lt;/th&gt;
&lt;th&gt;Authority grab — dead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Whose domain&lt;/td&gt;
&lt;td&gt;yours, related to the target&lt;/td&gt;
&lt;td&gt;bought for its old rankings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The link between them&lt;/td&gt;
&lt;td&gt;same brand, product, or topic&lt;/td&gt;
&lt;td&gt;unrelated to your site&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What the redirect is for&lt;/td&gt;
&lt;td&gt;keeping your own traffic and name&lt;/td&gt;
&lt;td&gt;inheriting a stranger's authority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where it points&lt;/td&gt;
&lt;td&gt;the page that replaces the old one&lt;/td&gt;
&lt;td&gt;your homepage, to pass equity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google's verdict&lt;/td&gt;
&lt;td&gt;legitimate, passes signals&lt;/td&gt;
&lt;td&gt;expired domain abuse, or ignored&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If every answer sits in the left column, you are doing the thing Google's policy names as fine, and the spam update is not about you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing it cleanly on Cloudflare
&lt;/h2&gt;

&lt;p&gt;Being on the right side of the line is most of the job. Doing it cleanly is the rest, and a few Cloudflare-specific choices keep a legitimate consolidation from reading like a lazy one.&lt;/p&gt;

&lt;p&gt;Point each old URL at the page that replaces it, not at your homepage. A blanket redirect of everything to the root is what muddies your anchor-text profile and looks like a link grab even when it isn't; where the old domain had real pages, map them to their real equivalents. The &lt;a href="https://301.sh/redirect-200-parked-domains/" rel="noopener noreferrer"&gt;parked-domain runbook&lt;/a&gt; has the Cloudflare pattern for a domain with no server behind it — a proxied &lt;code&gt;A&lt;/code&gt; record on &lt;code&gt;192.0.2.1&lt;/code&gt; and a redirect rule — and &lt;a href="https://301.sh/cloudflare-free-plan-redirect-limits/" rel="noopener noreferrer"&gt;Bulk Redirects&lt;/a&gt; is where a whole portfolio lives: one account-level list across every domain, rather than a rule per zone.&lt;/p&gt;

&lt;p&gt;Keep it a single &lt;code&gt;301&lt;/code&gt;. Redirect chains — the old domain to a second domain to the site — leak signal at every hop and are slower for the visitor, and there is no reason to build one when the destination is fixed. One permanent hop, straight to the relevant page.&lt;/p&gt;

&lt;p&gt;One honest aside, for the case this guide is not about: if what you actually want is to take over a dropped domain's ranking, a redirect was always the fragile way to do it, and Google just made it fragiler. The durable version is a real site on that domain, standing on its own. That is a different project from this one, which is about domains that already belong to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this is less tidy than it looks
&lt;/h2&gt;

&lt;p&gt;A domain's past comes with it. If the name you own spent an earlier life inside a link scheme, a &lt;code&gt;301&lt;/code&gt; carries that history into your site too, and a clean consolidation can still inherit a dirty reputation — worth reading the backlink profile of an acquired domain before you wire it up.&lt;/p&gt;

&lt;p&gt;Relevance is not binary either. A domain only loosely related to your site sits in the grey zone where Google may pass some of the signal, all of it, or none, and no dashboard tells you which ahead of time. The safe reading is that an unrelated &lt;code&gt;301&lt;/code&gt; is a waste rather than a punishment: the equity you hoped to move never arrives, while the redirect sits there doing nothing for anyone. The Google policy wording here was checked on 25 July 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What one setup does not give you
&lt;/h2&gt;

&lt;p&gt;For a handful of domains you own, the whole job is the runbook and one Bulk Redirects list, pointed at the right pages and checked once. That is genuinely all of it, and you should start there.&lt;/p&gt;

&lt;p&gt;At the scale where a portfolio is dozens of names funnelling into one site, the work is staying on the right side of the line continuously: every redirect relevant, every one a single hop to a live page, and none quietly turned into a chain or a homepage dump as the site changes underneath them — plus a re-check each time Google tightens the screws. That standing watch across the whole portfolio is the part &lt;a href="https://301.st" rel="noopener noreferrer"&gt;301.st&lt;/a&gt; runs for you. For a few owned domains, a Bulk Redirects list and the runbook are enough.&lt;/p&gt;

</description>
      <category>redirects</category>
      <category>bulkredirects</category>
      <category>cloudflare</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Auto-renew was on and the domain expired anyway</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Fri, 31 Jul 2026 14:00:17 +0000</pubDate>
      <link>https://dev.to/301st/auto-renew-was-on-and-the-domain-expired-anyway-1kch</link>
      <guid>https://dev.to/301st/auto-renew-was-on-and-the-domain-expired-anyway-1kch</guid>
      <description>&lt;p&gt;The domain is on auto-renew. You set it years ago and stopped thinking about it, which was the whole point. Then one morning it returns &lt;code&gt;NXDOMAIN&lt;/code&gt;, and everything riding on it is gone at once: the site, the email on that domain, and &lt;a href="https://301.sh/every-way-to-redirect-on-cloudflare/" rel="noopener noreferrer"&gt;every redirect&lt;/a&gt; pointed through it. Auto-renew was the one thing that was supposed to make this impossible.&lt;/p&gt;

&lt;p&gt;It isn't, and it fails people who register domains for a living, not only the careless. The setting sits there looking healthy while the three things it quietly depends on rot out of your sight. Knowing where the net has holes — and what actually catches a domain falling through them — is the difference between finding out on a schedule and finding out from a customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto-renew is a standing instruction, not a guarantee
&lt;/h2&gt;

&lt;p&gt;Auto-renew is one sentence: charge this card, file this renewal, on this date. It inherits every way those two things quietly rot.&lt;/p&gt;

&lt;p&gt;The card expires or is declined. Auto-renew is a card on file for a domain you registered in 2019, and a card from 2019 has almost certainly been reissued since. The instruction is intact; the payment behind it is not.&lt;/p&gt;

&lt;p&gt;The notification you were counting on never arrives. It goes to an address you no longer read, or a filter files it under a tab you never open, or it is simply misrouted to spam. The single message that would tell you the charge failed is the message you don't see, and its absence looks exactly like everything being fine.&lt;/p&gt;

&lt;p&gt;And you have stopped looking. Renewals you automate are renewals you take off your desk — that is the appeal and also the failure. You might have renewed the domain's TLS certificate in the spring and half-filed the registration under the same mental heading, so the date never stood out. The domain drops out of your attention on the day you trust a machine to hold it, and when the machine misses there is no second reader.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Cloudflare Registrar actually does
&lt;/h2&gt;

&lt;p&gt;Cloudflare Registrar is better than most on the mechanics, and it is worth knowing the real schedule instead of assuming a single make-or-break charge. From its documentation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cloudflare Registrar enrolls your domain to auto-renew by default.&lt;/p&gt;

&lt;p&gt;Cloudflare attempts to renew these domains automatically 30 days before their expiration date. Several more attempts are made if the first attempt fails. The last attempt to renew is made on the day before expiration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a month of retries, not one roll of the dice. And if every attempt in that window fails:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you do not renew your domain before the expiration date, your domain will enter a Redemption Grace Period (RGP) for 30 days.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So a Cloudflare-registered name that lapses is not gone the next morning. It can be restored during redemption. For domains actually on Cloudflare Registrar this is a genuine net, and it costs nothing — checked against Cloudflare's documentation on 25 July 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the net has holes
&lt;/h2&gt;

&lt;p&gt;The schedule is good. The things it still depends on are the same three that rot out of sight.&lt;/p&gt;

&lt;p&gt;It still needs a card that works. Thirty days of retries against a declined card end where a single failed charge does — on the day before expiration — and then you are counting redemption days instead of holding a domain. The retries buy time only if someone notices them.&lt;/p&gt;

&lt;p&gt;It only covers domains registered with Cloudflare. A portfolio is rarely all in one place. Names accrete over years across three or four registrars, each with its own renewal timing and its own notification habits, most of them worse than Cloudflare's. The one you forgot you owned is the one no auto-renew setting is watching.&lt;/p&gt;

&lt;p&gt;And renewed is not the same question as resolving. &lt;code&gt;NXDOMAIN&lt;/code&gt; is what a lapsed registration returns, but it is also what a zone hold, a nameserver that drifted to &lt;a href="https://301.sh/redirect-silently-fails/" rel="noopener noreferrer"&gt;an account nobody can find&lt;/a&gt;, or a never-activated zone returns. "Did it renew?" is answered in a billing page you have to remember to open. "Is it resolving?" is answered only by asking the domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually catches it
&lt;/h2&gt;

&lt;p&gt;The lesson is not "check your card." It is that every signal pointing inward — the charge, the email, your own memory — can fail quietly and at the same time. The only signal that can't is the one pointing at the result: does the name still resolve, and did that answer change since yesterday.&lt;/p&gt;

&lt;p&gt;Line up the failures against the things that might catch each one, and only one column is filled all the way down:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What fails&lt;/th&gt;
&lt;th&gt;Auto-renew&lt;/th&gt;
&lt;th&gt;Registrar email&lt;/th&gt;
&lt;th&gt;A reminder you keep&lt;/th&gt;
&lt;th&gt;Probing the domain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Card declined&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;if you read it&lt;/td&gt;
&lt;td&gt;the date, not the failure&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notification misrouted or ignored&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain at a registrar you forgot&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Renewed, still &lt;code&gt;NXDOMAIN&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is the one no renewal-tracking method can catch by definition: the registration is fine and the domain still does not answer. A spreadsheet with &lt;code&gt;=DATEDIF()&lt;/code&gt; counting down to a renewal date — the advice that has not changed in ten years — tracks the date you typed in, not the domain. If the name lapses early because a charge failed, or the date in the sheet was wrong, the sheet stays green while the domain is dead.&lt;/p&gt;

&lt;p&gt;Probing the domain is the same loop as the &lt;a href="https://301.sh/audit-300-domains-one-script/" rel="noopener noreferrer"&gt;audit script&lt;/a&gt;, with the one column that matters here: whether a name that resolved last time returns &lt;code&gt;NXDOMAIN&lt;/code&gt; now. Run it on a schedule, compare each answer to the last, and a name that quietly lapsed raises a flag while it is still inside the redemption window, instead of after a customer emails to say the site is down.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this is worth setting up, and when it isn't
&lt;/h2&gt;

&lt;p&gt;For domains on Cloudflare Registrar, the free path is genuinely good: auto-renew by default, a month of retries, and a redemption window if all of them miss. Turn it on, keep a working card, and for a handful of names that is the whole job — a calendar reminder you control, kept somewhere other than the registrar's own email, covers the rest.&lt;/p&gt;

&lt;p&gt;Where it runs out is a portfolio spread across registrars, where no single renewal setting sees all of it and the only thing that does is a probe asking every domain, on every registrar, whether it is still there — and comparing today's answer to yesterday's. That cross-registrar watch, on a loop, with an alert when a name changes, is the part &lt;a href="https://301.st" rel="noopener noreferrer"&gt;301.st&lt;/a&gt; runs for you. Auto-renew keeps the domains it owns; this keeps track of the ones nobody remembered to look at. For a dozen names at one registrar, auto-renew and a reminder on your own calendar are genuinely enough.&lt;/p&gt;

</description>
      <category>dns</category>
      <category>cloudflare</category>
      <category>redirects</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your redirect is configured and still returns 522, 525, or a parking page</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Thu, 30 Jul 2026 14:00:46 +0000</pubDate>
      <link>https://dev.to/301st/your-redirect-is-configured-and-still-returns-522-525-or-a-parking-page-3g4i</link>
      <guid>https://dev.to/301st/your-redirect-is-configured-and-still-returns-522-525-or-a-parking-page-3g4i</guid>
      <description>&lt;p&gt;You built the redirect. The dashboard shows the domain active, the cloud is orange, and the rule is sitting in the list where you left it. Then someone opens the domain and gets a &lt;code&gt;522&lt;/code&gt;, or a certificate warning, or a page offering to sell them the name. None of those is your &lt;code&gt;301&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The redirect did not fail loudly. It is simply not the thing answering, and the dashboard is the wrong place to notice. Three different misconfigurations produce this, each with its own signature, and one request separates them. This is the failure column of &lt;a href="https://301.sh/audit-300-domains-one-script/" rel="noopener noreferrer"&gt;the audit script&lt;/a&gt; seen up close, and the underside of &lt;a href="https://301.sh/every-way-to-redirect-on-cloudflare/" rel="noopener noreferrer"&gt;every way to redirect on Cloudflare&lt;/a&gt;: the same methods, and the ways each one goes quiet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule never ran, so Cloudflare called an origin that is not there
&lt;/h2&gt;

&lt;p&gt;A redirect on a parked domain is two parts that look like one. There is a DNS record, an &lt;code&gt;A&lt;/code&gt; record on the apex proxied through Cloudflare, and there is a rule that does the redirecting. The &lt;a href="https://301.sh/redirect-200-parked-domains/" rel="noopener noreferrer"&gt;runbook for parked domains&lt;/a&gt; points that record at &lt;code&gt;192.0.2.1&lt;/code&gt;, and Cloudflare's own note explains why an address that goes nowhere is the right choice:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This address does not route traffic to an origin server but allows Cloudflare to apply rules, redirects, and Workers to incoming traffic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The rule is what makes the pattern work. A redirect rule runs at the edge and answers the request there, so Cloudflare never opens a connection to &lt;code&gt;192.0.2.1&lt;/code&gt;. Take the rule away, scope it to a hostname that does not match, upload the list to the wrong account, or proxy the record before the rule exists, and Cloudflare does exactly what the orange cloud tells it to. It proxies the request to &lt;code&gt;192.0.2.1&lt;/code&gt;. Nothing is listening there, on purpose.&lt;/p&gt;

&lt;p&gt;What the visitor gets is a &lt;code&gt;522&lt;/code&gt;. Cloudflare's definition is literal:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Error 522 occurs when Cloudflare times out contacting the origin web server.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The wait is not instant. Cloudflare sends a SYN and, in the documented case, gives up when the origin web server does not return a SYN+ACK within 19 seconds, retrying on a backoff of 1,1,1,1,1,2,4,8. So the domain hangs for the better part of half a minute and then shows a Cloudflare error page. The &lt;code&gt;A&lt;/code&gt; record is present, the proxy is on, everything on the DNS tab looks done, and the one piece that was missing left no mark there.&lt;/p&gt;

&lt;p&gt;Swap the dummy address for a real origin and the same missing rule shows up as a different code from the same family: &lt;code&gt;521&lt;/code&gt; when the origin refuses the connection, &lt;code&gt;523&lt;/code&gt; when it is unreachable, &lt;code&gt;524&lt;/code&gt; when the connection opens but no response comes back in time. The cause does not change. The request reached an origin because no edge rule caught it first.&lt;/p&gt;

&lt;p&gt;That gives you the tell. A redirect rule that fires never touches the origin, so it cannot time out against one. Any &lt;code&gt;52x&lt;/code&gt; on a domain that is supposed to redirect is proof that the redirect did not run for that request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full (Strict) turns a redirect at the origin into a certificate error
&lt;/h2&gt;

&lt;p&gt;Not every redirect lives at the edge. Plenty of them are a &lt;code&gt;301&lt;/code&gt; sent by the origin web server, with Cloudflare proxying in front. That arrangement only works if Cloudflare can reach the origin over TLS and accept the certificate it presents, and the SSL/TLS encryption mode decides how strict it is about that.&lt;/p&gt;

&lt;p&gt;Set the mode to Full (Strict) and the certificate has to be one Cloudflare trusts. When the handshake itself does not complete, the visitor gets a &lt;code&gt;525&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This error indicates that the SSL handshake between Cloudflare and the origin web server failed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cloudflare lists the usual reasons: no valid certificate installed, port &lt;code&gt;443&lt;/code&gt; closed, no SNI support, or a cipher mismatch. When the handshake does complete but the certificate does not check out, the code is &lt;code&gt;526&lt;/code&gt; instead:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cloudflare cannot validate the SSL certificate at your origin web server.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That one is paired with Full (Strict) specifically, and it fires on a certificate that has expired, been revoked, carries the wrong name, is missing part of its chain, or was signed by nobody a browser would trust. The redirect the origin wants to send is fine. The visitor never sees it, because the connection that would carry it never finishes.&lt;/p&gt;

&lt;p&gt;There is a gentler version of the same problem on a domain you just added. Universal SSL takes time to issue, and it covers one level of subdomain, not two. A request that arrives before the certificate is active, or to a name the certificate does not cover, gets a browser TLS error before any redirect has a chance to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The domain never moved, so Cloudflare is not even in the path
&lt;/h2&gt;

&lt;p&gt;The third failure has no Cloudflare error code, because Cloudflare is not involved. You added the zone, you wrote the rule, and the nameservers at the registrar were never changed to the pair Cloudflare assigned. The zone sits &lt;code&gt;pending&lt;/code&gt;. Everything you configured is real and none of it is live.&lt;/p&gt;

&lt;p&gt;There is a quieter way to land in the same place. Presetting nameservers in bulk before creating the zones, which is the instinct at scale, now backfires:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;To prevent domain hijacking, you can no longer preset Cloudflare nameservers at your registrar before creating the respective zone in Cloudflare.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Do it anyway and the zone is assigned a different pair, the registrar points at nameservers that do not serve it, and the domain never activates. Nothing in either interface says why.&lt;/p&gt;

&lt;p&gt;While the domain is pending, whatever the registrar serves is what the visitor sees. Often that is a parking page, sometimes one offering the name for sale, and it usually answers with a &lt;code&gt;200&lt;/code&gt;. A check that only asks whether the domain is up will pass, because something did answer. It just was not you, and the redirect you wrote is asleep behind a zone that never woke.&lt;/p&gt;

&lt;h2&gt;
  
  
  One request tells the three apart
&lt;/h2&gt;

&lt;p&gt;Each mode answers a &lt;code&gt;curl&lt;/code&gt; differently, which is why the audit reads the status line before anything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; &lt;span class="s2"&gt;"https://the-domain/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;52x&lt;/code&gt; on the first line means Cloudflare is in the path and the rule did not fire. A TLS error before any HTTP status means the handshake to the origin failed, or the edge certificate is not ready. A &lt;code&gt;200&lt;/code&gt;, or a &lt;code&gt;302&lt;/code&gt; whose &lt;code&gt;location&lt;/code&gt; points at a registrar rather than a &lt;code&gt;301&lt;/code&gt; to your target, means the domain is not on Cloudflare at all. The &lt;a href="https://301.sh/audit-300-domains-one-script/" rel="noopener noreferrer"&gt;audit script&lt;/a&gt; puts exactly this in its &lt;code&gt;CODE&lt;/code&gt; and &lt;code&gt;DESTINATION&lt;/code&gt; columns, which is how you find these across a portfolio instead of one domain at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this is less tidy than it looks
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;522&lt;/code&gt; against a real origin can be intermittent rather than constant, usually when the origin firewall blocks some Cloudflare IP ranges and not others, so the same domain passes and fails by the hour. On the &lt;code&gt;192.0.2.1&lt;/code&gt; pattern there is no such ambiguity: the address is reserved and never answers, so the &lt;code&gt;522&lt;/code&gt; is every time.&lt;/p&gt;

&lt;p&gt;A pending zone is the one case the dashboard actively hides from you, because from inside the account nothing is broken. The rule is valid, the record is valid, the zone is waiting. The evidence only exists outside, in what the domain returns to a request that does not go through Cloudflare.&lt;/p&gt;

&lt;p&gt;And a registrar parking page does not always sit still on a &lt;code&gt;200&lt;/code&gt;. Some registrars answer with a &lt;code&gt;302&lt;/code&gt; to their own parking host, which is a redirect, just not yours. The audit script catches that by reading where the &lt;code&gt;location&lt;/code&gt; points, not merely that a redirect happened. The codes and definitions here were checked against Cloudflare's documentation on 23 July 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What one look does not give you
&lt;/h2&gt;

&lt;p&gt;All three of these are green at a glance and only visible from outside, so catching them is a matter of asking every domain the same question on a schedule rather than trusting the dashboard. For a handful of domains, the loop from the &lt;a href="https://301.sh/audit-300-domains-one-script/" rel="noopener noreferrer"&gt;audit script&lt;/a&gt; on a cron entry is the whole answer, and you should start there.&lt;/p&gt;

&lt;p&gt;At the scale where a portfolio carries a long tail of names nobody has opened in months, the question has to be asked continuously, and the answer compared against the last one so a domain that quietly changed raises a flag. That is the part &lt;a href="https://301.st" rel="noopener noreferrer"&gt;301.st&lt;/a&gt; runs for you: the same probe across the whole portfolio, on a loop, with an alert when a row changes. For twenty domains, the cron entry and the script are genuinely enough.&lt;/p&gt;

</description>
      <category>redirects</category>
      <category>dns</category>
      <category>cloudflare</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Which of your 300 domains still redirect? One script, and what to fix first</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Wed, 29 Jul 2026 14:00:31 +0000</pubDate>
      <link>https://dev.to/301st/which-of-your-300-domains-still-redirect-one-script-and-what-to-fix-first-343h</link>
      <guid>https://dev.to/301st/which-of-your-300-domains-still-redirect-one-script-and-what-to-fix-first-343h</guid>
      <description>&lt;p&gt;Someone asked on r/Domains in March 2025 how to identify all the domains currently redirecting to their main website. The thread has no answers. A company monitoring hundreds of domains asked something similar on r/selfhosted in 2024, and the best reply admitted the gap rather than closing it: for monitoring WHOIS changes, it said, "the situation is even worse". The closest thing to a canonical answer is a Server Fault question from 2013 whose accepted reply is to "double check your current DNS and your future DNS, just to be sure".&lt;/p&gt;

&lt;p&gt;Meanwhile people keep rebuilding the tool. Uptime Kuma has had requests for domain expiry checks open since November 2021, and GitHub carries a steady supply of one-off clones.&lt;/p&gt;

&lt;p&gt;The question deserves a better answer than a spreadsheet. Here is a script that gives one, and a second column that decides what you actually do with the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the dashboard cannot tell you
&lt;/h2&gt;

&lt;p&gt;Cloudflare's interface is organised by zone. Everything worth knowing here is a property of the set of zones, and there is no screen for that.&lt;/p&gt;

&lt;p&gt;The raw logs that would answer it are Enterprise only: Logpush lists Free, Pro and Business as &lt;strong&gt;No&lt;/strong&gt;. Per-rule statistics never arrived either — a feature request for Page Rule statistics was opened in May 2017, raised again by its author twice, and &lt;a href="https://301.sh/count-clicks-on-a-redirect/" rel="noopener noreferrer"&gt;closed in June 2025 without being implemented&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So the cross-domain view is something you assemble yourself. The good news is that the API gives you the list for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start from the account, not from your spreadsheet
&lt;/h2&gt;

&lt;p&gt;The list of domains you think you own and the list Cloudflare is actually serving are different lists, and the difference is the first finding of any audit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$CF_API_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.cloudflare.com/client/v4/zones?per_page=50&amp;amp;page=1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
| jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.result[] | "\(.name)\t\(.status)"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A zone's &lt;code&gt;status&lt;/code&gt; is one of &lt;code&gt;initializing&lt;/code&gt;, &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;active&lt;/code&gt; or &lt;code&gt;moved&lt;/code&gt;. Anything that is not &lt;code&gt;active&lt;/code&gt; is a finding on its own: a &lt;code&gt;pending&lt;/code&gt; zone is a domain whose nameservers never switched at the registrar, which means every rule you wrote for it has never once run. On a portfolio built in batches, that is &lt;a href="https://301.sh/redirect-200-parked-domains/" rel="noopener noreferrer"&gt;the long tail the runbook warns about&lt;/a&gt;, showing up months later.&lt;/p&gt;

&lt;p&gt;Mind the pagination. The endpoint pages, and a portfolio of three hundred will not arrive in one response — loop &lt;code&gt;page&lt;/code&gt; until you get fewer rows than you asked for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check, and why each check is a different failure
&lt;/h2&gt;

&lt;p&gt;Three questions, three different ways a domain can be quietly broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it answer at all?&lt;/strong&gt; A dead domain and a domain that answers wrongly look identical in a spreadsheet and nothing alike in a browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it go where you think?&lt;/strong&gt; A redirect that works and points at last year's landing page is worse than a broken one, because nothing will ever alert you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the certificate current?&lt;/strong&gt; Cloudflare's Universal SSL certificates "have a 90-day validity period" and "the auto renewal period starts 30 days before expiration". Read that carefully before you set a threshold: on a healthy proxied domain, fewer than 30 days remaining is normal, not an alarm. What matters is a certificate that is already expired, or one on a domain that left Cloudflare and has nobody renewing it.&lt;/p&gt;

&lt;p&gt;And the failure that no HTTP check catches: the domain simply stopped existing. A domain on auto-renew can still go NXDOMAIN — a card that quietly expired, a renewal notice lost to spam — because auto-renew is the advice everyone gives and the thing that fails before anyone notices. &lt;a href="https://301.sh/auto-renew-on-domain-still-expired/" rel="noopener noreferrer"&gt;Why it is not the safety net it looks like&lt;/a&gt; is its own subject.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second column: what is worth fixing
&lt;/h2&gt;

&lt;p&gt;Three hundred rows of findings is not an answer, it is a new problem. Nobody fixes three hundred domains. You fix the ones that carry weight, and until recently working out which those were meant a subscription.&lt;/p&gt;

&lt;p&gt;That changed. Ahrefs' Domain Rating endpoint is now open, and its documentation is explicit: "Requests to this endpoint are free and do not consume any API units."&lt;/p&gt;

&lt;p&gt;Checked today, it answers without a subscription:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://api.ahrefs.com/v3/public/domain-rating-free?target=cloudflare.com&amp;amp;output=json"&lt;/span&gt;
&lt;span class="go"&gt;{"domain_rating": {"domain_rating": 94.0, "license": "...", "warning": "..."}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three conditions come with it, and all three belong in this article rather than in a footnote.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A free key is required from 10 August 2026.&lt;/strong&gt; The endpoint currently answers unauthenticated and tells you so itself, in a &lt;code&gt;warning&lt;/code&gt; field in its own response: "Unauthenticated access to this endpoint will be removed on 2026-08-10." The script below sends a key already. Generating one is free, and doing it now costs less than debugging the script in three weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attribution, if you display the number.&lt;/strong&gt; The licence grants the right to "use, display, publish or integrate DR Data" and requires the credit "Domain Rating by Ahrefs" with a working hyperlink that "shall not be hidden, obscured or removed". A private terminal report is one thing; a dashboard your team reads is a place where that line has to appear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your own domains, at a civil pace.&lt;/strong&gt; The same licence prohibits scraping or harvesting DR data "in bulk or systematically". Walking the portfolio you own, once, with a pause between requests, is the use the endpoint exists for. Sweeping other people's domains to build a DR dataset is the thing it forbids. Ten sequential requests spaced by 0.3 seconds went through without a complaint; the limits are not documented and Ahrefs reserves the right to change them without notice, so pace it and do not assume the ceiling is high.&lt;/p&gt;

&lt;p&gt;One caveat on the metric itself: DR is Ahrefs' number, not Google's. It is a good relative scale for "which of these domains carries more weight than the others", and it says nothing about rankings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;

&lt;p&gt;Needs &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;openssl&lt;/code&gt; and &lt;code&gt;jq&lt;/code&gt;. The date arithmetic is GNU; on macOS use &lt;code&gt;date -j -f '%b %d %T %Y %Z'&lt;/code&gt;.&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="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail
: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;AHREFS_KEY&lt;/span&gt;:?export&lt;span class="p"&gt; AHREFS_KEY — free key from ahrefs.com&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%-34s %-5s %-32s %-6s %s\n'&lt;/span&gt; DOMAIN CODE DESTINATION CERT DR

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; domain&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;continue

  &lt;/span&gt;&lt;span class="nb"&gt;head&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 10 &lt;span class="s2"&gt;"https://&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$head&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'NR==1{print $2}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;dest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$head&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'tolower($1)=="location:"{print $2}'&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\r'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;until&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; | openssl s_client &lt;span class="nt"&gt;-servername&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-connect&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="s2"&gt;:443"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
          | openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-enddate&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-f2&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$until&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$until&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;86400&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;

  &lt;span class="nv"&gt;dr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 10 &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AHREFS_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="s2"&gt;"https://api.ahrefs.com/v3/public/domain-rating-free?target=&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;output=json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
       | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.domain_rating.domain_rating // "-"'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%-34s %-5s %-32s %-6s %s\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;DEAD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;dest&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;days&lt;/span&gt;:+&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;days&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="nv"&gt;d&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dr&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;0.3
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; domains.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A real run, against this site and two others, on 22 July 2026:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DOMAIN                             CODE  DESTINATION                      CERT   DR
www.301.sh                         301   https://301.sh/                  89d    8
301.sh                             200   -                                89d    8
spintax.net                        200   -                                85d    56
this-domain-does-not-exist-9f3.com DEAD  -                                       0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four rows and four different states. The &lt;code&gt;www&lt;/code&gt; form redirects to the apex, which is what it is supposed to do. Two domains answer directly. The last one does not resolve at all, and the script says &lt;code&gt;DEAD&lt;/code&gt; rather than pretending to have an opinion about it.&lt;/p&gt;

&lt;p&gt;Note the certificate column: 89 and 85 days. That is Universal SSL at the start of its 90-day life, which is what healthy looks like. A row reading &lt;code&gt;4d&lt;/code&gt; on a domain nobody is renewing is a different matter, and a row with no number at all means the handshake never happened.&lt;/p&gt;

&lt;p&gt;Two shapes worth recognising that are not in that sample. A &lt;code&gt;522&lt;/code&gt; means Cloudflare answered but your origin did not, so visitors are getting an error page where you think a redirect lives. And a &lt;code&gt;200&lt;/code&gt; on a domain that is supposed to forward means the redirect is simply gone: a rule deleted, or one that never applied because the zone is still &lt;code&gt;pending&lt;/code&gt;. Each of these is &lt;a href="https://301.sh/redirect-silently-fails/" rel="noopener noreferrer"&gt;a redirect that is configured and still failing&lt;/a&gt;, and the &lt;code&gt;CODE&lt;/code&gt; column is where they surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that turns the table into decisions
&lt;/h2&gt;

&lt;p&gt;Sort by broken first, then by DR descending, and the top of that list is your afternoon. A dead domain with real authority is the most expensive row in the table: every link pointing at it is value you already paid for and are currently throwing away. A dead domain with a DR of zero is a renewal invoice you can stop paying.&lt;/p&gt;

&lt;p&gt;That second conclusion is worth as much as the first, and no health check alone can reach it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What one pass does not give you
&lt;/h2&gt;

&lt;p&gt;It gives you a snapshot, and portfolios do not break on the day you look at them.&lt;/p&gt;

&lt;p&gt;A certificate expires in ninety days. A registrar's auto-renew fails silently one Tuesday. A rule gets edited on a zone nobody has opened in a year, and the redirect starts landing on the wrong page while returning a perfectly healthy 301. The script above finds all of these — the next time somebody remembers to run it.&lt;/p&gt;

&lt;p&gt;That is the difference between an audit and a system, and it is the honest place to say what &lt;a href="https://301.st" rel="noopener noreferrer"&gt;301.st&lt;/a&gt; is for: it runs this continuously across the portfolio and tells you when a row changes. For twenty domains, a cron entry and the script above are genuinely enough, and you should start there.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Domain Rating by &lt;a href="https://ahrefs.com/" rel="noopener noreferrer"&gt;Ahrefs&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dns</category>
      <category>redirects</category>
      <category>cloudflare</category>
      <category>webdev</category>
    </item>
    <item>
      <title>An agent readiness audit scored our docs site 21 out of 100</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Tue, 28 Jul 2026 14:00:32 +0000</pubDate>
      <link>https://dev.to/301st/an-agent-readiness-audit-scored-our-docs-site-21-out-of-100-4027</link>
      <guid>https://dev.to/301st/an-agent-readiness-audit-scored-our-docs-site-21-out-of-100-4027</guid>
      <description>&lt;p&gt;Cloudflare runs a scanner at &lt;a href="https://isitagentready.com" rel="noopener noreferrer"&gt;isitagentready.com&lt;/a&gt; that grades a domain on how ready it is for AI agents. Twenty one checks across five categories: discoverability, content accessibility, bot access control, protocol discovery, and commerce.&lt;/p&gt;

&lt;p&gt;We pointed it at &lt;a href="https://spintax.net" rel="noopener noreferrer"&gt;spintax.net&lt;/a&gt;, a documentation site that had shipped a full &lt;code&gt;llms.txt&lt;/code&gt; surface the week before. A curated index, eighteen clean Markdown mirrors of the documentation pages, and a concatenated &lt;code&gt;llms-full.txt&lt;/code&gt; for a single fetch. The score came back 21 out of 100, Level 1, "Basic Web Presence". Content accessibility scored zero out of one.&lt;/p&gt;

&lt;p&gt;That gap is the interesting part. The score is not a measure of how readable your site is to a model. It is a measure of which HTTP mechanisms you implement. Those are different things, and only one of them is what most sites think they are buying when they publish an &lt;code&gt;llms.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is what the audit actually measures, which four checks were worth implementing, and why eight of the failures were correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a complete llms.txt scores zero on content accessibility
&lt;/h2&gt;

&lt;p&gt;The check is not looking for a file. It sends a request with &lt;code&gt;Accept: text/markdown&lt;/code&gt; and reads the &lt;code&gt;Content-Type&lt;/code&gt; that comes back. If the response is &lt;code&gt;text/html&lt;/code&gt;, the check fails, no matter how much Markdown you publish at other URLs.&lt;/p&gt;

&lt;p&gt;This is HTTP content negotiation, and it has been in the specification since before anyone was building agents. One URL, several representations, the client states a preference in a header. &lt;code&gt;llms.txt&lt;/code&gt; is a convention: a file at a known path that a client has to know about in advance. Content negotiation is a mechanism: any client that already speaks HTTP gets the Markdown by asking for it, without knowing your site exists.&lt;/p&gt;

&lt;p&gt;We had eighteen Markdown files sitting on disk and no way for a client to reach them except by reading the index first. The audit was right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing content negotiation on Pages, on the free plan
&lt;/h2&gt;

&lt;p&gt;Cloudflare sells the easy version of this. A zone level setting called Markdown for Agents intercepts responses when the request carries &lt;code&gt;Accept: text/markdown&lt;/code&gt; and converts the HTML to Markdown on the fly. It is one toggle in AI Crawl Control, or one &lt;code&gt;PATCH&lt;/code&gt; to the &lt;code&gt;content_converter&lt;/code&gt; zone setting.&lt;/p&gt;

&lt;p&gt;It is available on "Pro, Business and Enterprise plans, and SSL for SaaS customers at no cost". The site is on Free.&lt;/p&gt;

&lt;p&gt;The toggle would also have been the worse option even if it were available. Our mirrors are generated at build time from the same HTML, through a turndown configuration with custom rules for the card grids that would otherwise convert into unreadable blocks nested inside links. An automatic conversion at the edge cannot know about those. Hand tuned output beats generic output when you already build it.&lt;/p&gt;

&lt;p&gt;So: a Pages Function. Note that &lt;code&gt;_headers&lt;/code&gt; and &lt;code&gt;_redirects&lt;/code&gt; cannot do this job at all, because neither can branch on a request header. Content negotiation needs code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// functions/_middleware.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;wantsMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accept&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mirror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ASSETS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mirrorFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&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="nx"&gt;mirror&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mirror&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;status&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="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/markdown; charset=utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Vary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accept&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Robots-Tag&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;noindex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Link&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;; rel="canonical"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;next&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;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Vary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accept&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;out&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;Three details in there earn their place.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;env.ASSETS.fetch&lt;/code&gt; reads the already deployed static file. The Function does no conversion and holds no copy of the content.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Vary: Accept&lt;/code&gt; goes on both branches. One URL now has two representations, and a cache that does not key on the header will serve the Markdown to a browser.&lt;/p&gt;

&lt;p&gt;The fallback is a miss on the mirror, not an error. If &lt;code&gt;env.ASSETS.fetch&lt;/code&gt; returns 404, the code falls through to the HTML. Adding a page that has no mirror yet degrades quietly.&lt;/p&gt;

&lt;p&gt;Detecting the preference deserves more than a substring match. Browsers never send &lt;code&gt;text/markdown&lt;/code&gt;, so presence is enough signal, but a client can write &lt;code&gt;Accept: text/markdown;q=0&lt;/code&gt; to mean the opposite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;wantsMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accept&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="nx"&gt;accept&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;false&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;entry&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;accept&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="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entry&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&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="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/markdown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&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;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;trim&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;q=&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&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;false&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;The result is checkable from any terminal, which is the point of using a mechanism rather than a convention:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Accept: text/markdown'&lt;/span&gt; https://spintax.net/
&lt;span class="go"&gt;HTTP/1.1 200 OK
&lt;/span&gt;&lt;span class="gp"&gt;Content-Type: text/markdown;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;charset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;utf-8
&lt;span class="gp"&gt;Link: &amp;lt;https://spintax.net/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;rel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"canonical"&lt;/span&gt;
&lt;span class="go"&gt;Vary: Accept
X-Robots-Tag: noindex
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Keeping a static site static
&lt;/h3&gt;

&lt;p&gt;A root &lt;code&gt;_middleware&lt;/code&gt; matches every request by default. On a site of 116 pages plus assets, that turns a fully static deployment into one where every request invokes a Worker, to serve Markdown on nineteen of them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;_routes.json&lt;/code&gt; in the build output fixes it:&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;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"include"&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="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/docs/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/docs/syntax"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/docs/variables/"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exclude"&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;We generate that file from the same list that generates the mirrors, so the routed paths and the negotiable paths cannot drift apart. Ninety seven localized pages, the 404 and every asset are served from static storage and never reach the Function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Pages traps we hit on the way
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Overlapping _headers rules concatenate
&lt;/h3&gt;

&lt;p&gt;This one cost real time. The site already served its Markdown mirrors with a wildcard:&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;md&lt;/span&gt;
  &lt;span class="n"&gt;Content&lt;/span&gt;-&lt;span class="n"&gt;Type&lt;/span&gt;: &lt;span class="n"&gt;text&lt;/span&gt;/&lt;span class="n"&gt;plain&lt;/span&gt;; &lt;span class="n"&gt;charset&lt;/span&gt;=&lt;span class="n"&gt;utf&lt;/span&gt;-&lt;span class="m"&gt;8&lt;/span&gt;
  &lt;span class="n"&gt;X&lt;/span&gt;-&lt;span class="n"&gt;Robots&lt;/span&gt;-&lt;span class="n"&gt;Tag&lt;/span&gt;: &lt;span class="n"&gt;noindex&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding skill files under &lt;code&gt;/.well-known/agent-skills/&lt;/code&gt; meant adding a more specific rule for them, with &lt;code&gt;text/markdown&lt;/code&gt;. Both rules match &lt;code&gt;/.well-known/agent-skills/spintax-syntax/SKILL.md&lt;/code&gt;. The result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content-Type: text/plain; charset=utf-8, text/markdown; charset=utf-8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pages merged the two rules by appending the values. It did not let the specific rule win. The output is not a valid &lt;code&gt;Content-Type&lt;/code&gt; and nothing warns you.&lt;/p&gt;

&lt;p&gt;The fix was to delete the wildcard and generate one explicit rule per mirror from the build. Eighteen rules instead of one pattern, well under the limit of 100. If two &lt;code&gt;_headers&lt;/code&gt; rules can match the same path and set the same header name, assume they will both apply.&lt;/p&gt;

&lt;h3&gt;
  
  
  wrangler pages dev parses _headers once
&lt;/h3&gt;

&lt;p&gt;The local dev server reads &lt;code&gt;_headers&lt;/code&gt; at startup and logs how many rules it parsed. It does not reparse after a rebuild. We fixed the wildcard, rebuilt, retested, and saw the same broken &lt;code&gt;Content-Type&lt;/code&gt; because the server was still holding the rules it read two builds ago.&lt;/p&gt;

&lt;p&gt;Restart the dev server after any change to &lt;code&gt;_headers&lt;/code&gt;. The rule count in the startup log is the quickest confirmation that it picked up the new file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three cheap checks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Content Signals.&lt;/strong&gt; A directive in &lt;code&gt;robots.txt&lt;/code&gt; that states how the content may be used by AI systems. It goes inside the &lt;code&gt;User-agent&lt;/code&gt; group, under a preamble from contentsignals.org that carries the legal weight:&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;User&lt;/span&gt;-&lt;span class="n"&gt;agent&lt;/span&gt;: *
&lt;span class="n"&gt;Content&lt;/span&gt;-&lt;span class="n"&gt;Signal&lt;/span&gt;: &lt;span class="n"&gt;ai&lt;/span&gt;-&lt;span class="n"&gt;train&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;, &lt;span class="n"&gt;search&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;, &lt;span class="n"&gt;ai&lt;/span&gt;-&lt;span class="n"&gt;input&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;

&lt;span class="n"&gt;Allow&lt;/span&gt;: /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three signals, three values each: &lt;code&gt;search&lt;/code&gt; for indexing, &lt;code&gt;ai-input&lt;/code&gt; for retrieval and grounding, &lt;code&gt;ai-train&lt;/code&gt; for model training. A documentation site that publishes Markdown mirrors for machines has an obvious answer to all three. Keep the preamble. It is what turns the line from a preference into a reservation of rights under Article 4 of the EU copyright directive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Link response headers.&lt;/strong&gt; RFC 8288, and the audit accepts only registered relation types. &lt;code&gt;describedby&lt;/code&gt; for machine readable descriptions, &lt;code&gt;alternate&lt;/code&gt; for a different representation of the same page, &lt;code&gt;service-doc&lt;/code&gt; for the documentation entry point:&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;Link: &amp;lt;/llms.txt&amp;gt;; rel="describedby"; type="text/plain",
      &amp;lt;/docs/variables.md&amp;gt;; rel="alternate"; type="text/markdown",
      &amp;lt;/docs/&amp;gt;; rel="service-doc"; type="text/html"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An agent that lands on any page can find the rest from the response headers, without parsing HTML and without knowing about &lt;code&gt;llms.txt&lt;/code&gt;. This one costs nothing at runtime: it is static &lt;code&gt;_headers&lt;/code&gt; output, no Function involved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent Skills.&lt;/strong&gt; A discovery index at &lt;code&gt;/.well-known/agent-skills/index.json&lt;/code&gt; listing skill documents that an agent can install. This is the only one of the four with product value rather than audit value. A skill is a condensed, imperative document addressed to a machine that is about to do something, which is a different artifact from a documentation page written for a person.&lt;/p&gt;

&lt;p&gt;The index entries carry a SHA256 digest of each artifact. Generate the index, never write it by hand. A digest written by hand is wrong the first time anyone edits a skill, and a wrong digest tells a client the file was tampered with. We read the &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; from each skill's own frontmatter and hash the bytes as served, so there is one source for the metadata and no second list to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eight failures that were correct
&lt;/h2&gt;

&lt;p&gt;The checks still failing are DNS for AI Discovery, API Catalog, OAuth discovery, OAuth Protected Resource, Auth.md, MCP Server Card, A2A Agent Card and WebMCP. Five commerce checks sit alongside them, and the audit marks those neutral rather than failed, because it can tell the site sells nothing.&lt;/p&gt;

&lt;p&gt;A documentation site has no API, no authentication and nothing to sell. Publishing an empty OAuth discovery document to raise a score advertises an authorization server that does not exist. Every one of those documents is a promise to a client about what it will find. A promise you cannot keep is worse than a missing file, because the client that reads it wastes a round trip and then has to guess whether you are broken or lying.&lt;/p&gt;

&lt;p&gt;Three of them are worth more detail, because the reasoning generalizes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The MCP Server Card points at a path the specification abandoned
&lt;/h3&gt;

&lt;p&gt;The audit probes three locations for an MCP Server Card: &lt;code&gt;/.well-known/mcp.json&lt;/code&gt;, &lt;code&gt;/.well-known/mcp/server-cards.json&lt;/code&gt;, and &lt;code&gt;/.well-known/mcp/server-card.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The proposal behind it, SEP-2127, is open and unmerged, status Draft, on the Extensions Track. The normative wire format does not live in the specification repository at all. It lives in a repository named &lt;code&gt;experimental-ext-server-card&lt;/code&gt;. The discovery path has moved at least three times, and the current canonical one is &lt;code&gt;/.well-known/ai-catalog.json&lt;/code&gt;, with media type &lt;code&gt;application/ai-catalog+json&lt;/code&gt;, a catalog that indexes cards rather than being one.&lt;/p&gt;

&lt;p&gt;None of the three paths the audit probes is the path the specification now uses. Passing that check today means publishing to an address the spec has already left.&lt;/p&gt;

&lt;h3&gt;
  
  
  DNS-AID: the check its own author does not pass
&lt;/h3&gt;

&lt;p&gt;This one is worth the most, because it is the cleanest example of why you read the failures instead of clearing them.&lt;/p&gt;

&lt;p&gt;DNS for AI Discovery asks you to publish SVCB records under an &lt;code&gt;_agents&lt;/code&gt; namespace so agents can find your agent endpoints through DNS. It is a pure DNS record: no Worker, no runtime, no cost, and Cloudflare supports SVCB on the free plan. By the usual arithmetic it is the cheapest check on the entire list.&lt;/p&gt;

&lt;p&gt;Three things say otherwise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The specification is an individual Internet-Draft.&lt;/strong&gt; Version 02, updated 27 May 2026, not adopted by any working group, no formal standing in the standards process, and it expires on 28 November 2026. That is not a reason to ignore it. It is a reason to know what you are building on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The scanner probes labels the draft does not define.&lt;/strong&gt; It queries &lt;code&gt;_index._agents&lt;/code&gt;, &lt;code&gt;_mcp._agents&lt;/code&gt; and &lt;code&gt;_a2a._agents&lt;/code&gt;. The draft defines the entry point at &lt;code&gt;_index._agents&lt;/code&gt; and selects protocols through the &lt;code&gt;alpn&lt;/code&gt; service parameter, not through per-protocol underscore labels. &lt;code&gt;_mcp&lt;/code&gt; and &lt;code&gt;_a2a&lt;/code&gt; are the checker's, not the specification's — the same mismatch as the MCP Server Card above, from the same cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody publishes it.&lt;/strong&gt; Not a rhetorical nobody. Checked over DNS-over-HTTPS on 22 July 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Domain&lt;/th&gt;
&lt;th&gt;&lt;code&gt;_index._agents&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;isitagentready.com&lt;/td&gt;
&lt;td&gt;NXDOMAIN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cloudflare.com&lt;/td&gt;
&lt;td&gt;no records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agents.cloudflare.com&lt;/td&gt;
&lt;td&gt;no records&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The audit site itself, and the company that wrote the audit.&lt;/p&gt;

&lt;p&gt;And the conclusion that follows: point the scanner at itself. &lt;code&gt;isitagentready.com&lt;/code&gt; comes back &lt;strong&gt;Level 4, Agent-Integrated&lt;/strong&gt;, with &lt;code&gt;dnsAid&lt;/code&gt; among its own failures — the same level this blog reached, by a tool that fails its own check.&lt;/p&gt;

&lt;p&gt;To be fair to the draft, one objection against publishing does not hold. It is tempting to say we have no agents to index, but the draft puts the content of the index endpoint out of scope: it can be a live service or a static document, and there is a &lt;code&gt;well-known&lt;/code&gt; service parameter pointing at RFC 8615 metadata. We could honestly point &lt;code&gt;_index._agents&lt;/code&gt; at the Agent Skills index we already publish.&lt;/p&gt;

&lt;p&gt;We are not going to, and the reason is not honesty. It is that a record satisfying a check that its own author does not implement, against a draft expiring in four months, in a namespace whose labels the checker invented, buys a number and nothing else.&lt;/p&gt;

&lt;h3&gt;
  
  
  An A2A Agent Card names a live endpoint
&lt;/h3&gt;

&lt;p&gt;The A2A Agent Card requires a &lt;code&gt;url&lt;/code&gt; for the A2A service. It describes an agent that other agents can hand work to. We do not run one. Publishing the card would advertise a service that answers nothing.&lt;/p&gt;

&lt;p&gt;This also puts a ceiling on the score. Level 5 requires Auth.md, an MCP Server Card, an A2A Agent Card and an API Catalog. A content site can honestly reach maybe one of those. Level 4 is the top of the range for a site that only publishes documents, and that is a property of the ladder rather than a failing of the site.&lt;/p&gt;

&lt;h2&gt;
  
  
  The MCP server we did not build, and one number that decided it
&lt;/h2&gt;

&lt;p&gt;The one item on the list with genuine value for us is an MCP server: give an agent a &lt;code&gt;validate&lt;/code&gt; tool that returns structured diagnostics with line and column, and the loop from "model writes a template" to "model fixes the template" closes without a human in it.&lt;/p&gt;

&lt;p&gt;Two findings pushed it out of this week.&lt;/p&gt;

&lt;p&gt;The protocol is mid break. The 2026-07-28 revision makes Streamable HTTP stateless. The &lt;code&gt;Mcp-Session-Id&lt;/code&gt; header "and the protocol-level session that came with it are also removed", the &lt;code&gt;initialize&lt;/code&gt;/&lt;code&gt;initialized&lt;/code&gt; handshake is removed, and the transport "now requires &lt;code&gt;Mcp-Method&lt;/code&gt; and &lt;code&gt;Mcp-Name&lt;/code&gt; headers so load balancers, gateways, and rate-limiters can route on the operation", with a new &lt;code&gt;server/discover&lt;/code&gt; method for capabilities. Upstream calls it the largest revision of the protocol since launch. Building against the current shape means rewriting in a week.&lt;/p&gt;

&lt;p&gt;The second finding is a number. Cloudflare's documentation is clear that a stateless MCP server needs no Durable Objects and no paid plan, which sounds like the free tier is enough. Then you check what the free tier gives you: 10 ms of CPU per request. We measured the engine we would be wrapping, on Node, so treat it as an order of magnitude rather than a measurement of the target:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Work&lt;/th&gt;
&lt;th&gt;CPU&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;400 byte template, one render, warm&lt;/td&gt;
&lt;td&gt;0.13 to 0.28 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400 byte template, twenty variants&lt;/td&gt;
&lt;td&gt;3 to 6 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First call on a cold isolate&lt;/td&gt;
&lt;td&gt;about 12.5 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 KB template, twenty variants&lt;/td&gt;
&lt;td&gt;about 85 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cold isolate alone is over budget before any template is parsed. A 16 KB template overruns by a factor of eight. A dependable server on the free plan would need caps tight enough to be annoying, around 4 KB of template and five variants. The Workers Paid plan at five dollars a month raises the limit to 30 seconds of CPU and removes the question.&lt;/p&gt;

&lt;p&gt;That is a design decision to make before writing code, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the score landed, and where this site sits
&lt;/h2&gt;

&lt;p&gt;Three checks passing became seven, and the level went from 1 to 4, "Agent-Integrated". Six more are marked neutral because they do not apply, and the eight that fail are the eight that should.&lt;/p&gt;

&lt;p&gt;The useful output was not the number. It was being told that a week of work on &lt;code&gt;llms.txt&lt;/code&gt; had produced files no client could negotiate for, which is a specific and fixable thing that no amount of reading our own documentation would have surfaced.&lt;/p&gt;

&lt;p&gt;For the record, this blog scored Level 1 when the article was written, three of twenty one: exactly where the other site started. Running the same four checks against it the same day produced a different answer, and the difference is worth more than the score.&lt;/p&gt;

&lt;p&gt;All four went in, and it now reads Level 4, the same as the site this article is about. Content Signals, &lt;code&gt;Link&lt;/code&gt; headers, an Agent Skills index, and a Markdown mirror of every article indexed in &lt;a href="https://301.sh/llms.txt" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt;. The interesting part is the fourth one, because the shape of the site changes what it costs.&lt;/p&gt;

&lt;p&gt;This site is not on Pages. It is a Worker serving static assets, and until today it had no script at all, which is why "requests to static assets are free and unlimited" covered every page view. Negotiation needs code in front of those assets, and any page that runs that code becomes an ordinary Worker request against the free plan's 100,000 per day.&lt;/p&gt;

&lt;p&gt;So the code runs on as little as possible: &lt;code&gt;run_worker_first&lt;/code&gt; lists the apex and single-segment paths, nothing else. Stylesheets, images, the mirrors themselves, &lt;code&gt;robots.txt&lt;/code&gt; and the sitemap never reach it and stay free.&lt;/p&gt;

&lt;p&gt;One detail is worth stealing. Cloudflare's cache key does not include &lt;code&gt;Vary&lt;/code&gt; unless a Cache Rule puts it there, so one URL with two representations can serve Markdown to a person out of cache. The fix is not a zone rule: it is &lt;code&gt;Cache-Control: no-store&lt;/code&gt; on the Markdown branch only. Agents are a trickle, the HTML keeps its normal caching, and the failure mode disappears.&lt;/p&gt;

&lt;p&gt;The other one is embarrassing and cheap. Handle &lt;code&gt;HEAD&lt;/code&gt;, not just &lt;code&gt;GET&lt;/code&gt;. RFC 9110 says a &lt;code&gt;HEAD&lt;/code&gt; must answer with the headers a &lt;code&gt;GET&lt;/code&gt; would send, and the first version of this did not — which &lt;code&gt;curl -I&lt;/code&gt; reported immediately, that being exactly how anyone would check it by hand.&lt;/p&gt;

&lt;p&gt;Same four checks, same day, two different sites, and the work was different on each. That is the actual lesson of running an audit: the checks are generic and your infrastructure is not.&lt;/p&gt;

&lt;p&gt;Run it against your own domain. Then read the failures and decide which ones describe a site you are actually running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://isitagentready.com/api/scan &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"url":"https://example.com"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON endpoint returns every check with its status, message and the requests it made, which is faster than the browser and easier to diff after a deploy. It reports the level rather than the score; the number out of 100 is the web interface's.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cloudflare</category>
      <category>pages</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A redirect runs no JavaScript. Here is how to count the click anyway.</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Mon, 27 Jul 2026 14:00:12 +0000</pubDate>
      <link>https://dev.to/301st/a-redirect-runs-no-javascript-here-is-how-to-count-the-click-anyway-4acb</link>
      <guid>https://dev.to/301st/a-redirect-runs-no-javascript-here-is-how-to-count-the-click-anyway-4acb</guid>
      <description>&lt;p&gt;The question is older than most of the tools people suggest for it. A feature request titled "Page Rule statistics" was opened on the Cloudflare Community on 23 May 2017, raised again by its author in 2018 and in 2019, and closed on 6 June 2025 without ever being implemented. In between, someone running short links on printed material asked whether there is a way to know how many hits a redirect rule got in a given timeframe, and received no substantive answer at all before the thread auto closed.&lt;/p&gt;

&lt;p&gt;The advice that does exist is to put a page in front of the redirect so that JavaScript has somewhere to run. That is not a workaround. That is switching the redirect off.&lt;/p&gt;

&lt;p&gt;Here is why nothing you already have can see the click, and what actually counts it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why every analytics tool you own is blind to this
&lt;/h2&gt;

&lt;p&gt;Not a gap in any particular product. A structural fact about what a redirect is.&lt;/p&gt;

&lt;p&gt;Every privacy friendly analytics tool in common use — Cloudflare Web Analytics, Plausible, Umami, Counterscale — works the same way: a small script on the page reports that the page was viewed. Cloudflare's own installation instruction is the clearest statement of the problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Add the JS snippet to any of your website's HTML pages before the ending body tag.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A 301 response has no HTML page. It has no body tag. There is nowhere to put the beacon and nothing to run it, so the visitor is counted at the destination, if the destination is yours, and not at all if it is not.&lt;/p&gt;

&lt;p&gt;A Cloudflare employee said as much in 2023, answering someone who wanted Web Analytics on a domain that only redirects: Web Analytics captures the metric from the client side, and when the source returns a redirect to a third party rather than a page, that cannot be tracked.&lt;/p&gt;

&lt;p&gt;Plausible has the same hole, reported in April 2022, answered with a roadmap promise, and still open.&lt;/p&gt;

&lt;h2&gt;
  
  
  The phase order, which is the real explanation
&lt;/h2&gt;

&lt;p&gt;Cloudflare publishes the order in which its request phases run, and reading it answers the question completely.&lt;/p&gt;

&lt;p&gt;Single Redirects run in &lt;code&gt;http_request_dynamic_redirect&lt;/code&gt;, the &lt;strong&gt;first&lt;/strong&gt; application layer phase. Bulk Redirects run in &lt;code&gt;http_request_redirect&lt;/code&gt;, fifteenth. Snippets run in &lt;code&gt;http_request_snippets&lt;/code&gt;, nineteenth. And the rules documentation is explicit about what happens on a match:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For terminating actions (Block, Redirect, or one of the challenge actions), rule evaluation will stop and the action will be executed immediately.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So a redirect that matches produces the response there and then. Everything later in the pipeline never runs, which includes the one place you could have written a log line. Even paying for Snippets does not help: they sit four phases after Bulk Redirects and never see a request that already left.&lt;/p&gt;

&lt;p&gt;This is the whole reason the problem has no configuration answer. &lt;strong&gt;The thing that counts the click has to be the thing that issues the redirect.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the built in answer costs
&lt;/h2&gt;

&lt;p&gt;Cloudflare does have a product that hands you raw request logs, and its availability table is short:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Logpush&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is the honest reason this article exists. On any plan below Enterprise, the log you want is something you have to write.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not KV, and this is worth being specific about
&lt;/h2&gt;

&lt;p&gt;The suggestion you will find in older threads is to write each click into Workers KV, one key per click or one counter key, and read it back with a list. Both shapes are wrong, for different documented reasons.&lt;/p&gt;

&lt;p&gt;The free plan allows &lt;strong&gt;1,000 writes per day&lt;/strong&gt;. A redirect that gets any real traffic exhausts that before lunch, and then silently stops recording.&lt;/p&gt;

&lt;p&gt;More fundamentally, KV limits you to &lt;strong&gt;one write per second to the same key&lt;/strong&gt;, on every plan. A single counter key is exactly the access pattern KV is built to refuse. Paying more does not change that number.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing that does work: a Worker and Analytics Engine
&lt;/h2&gt;

&lt;p&gt;Workers Analytics Engine is built for exactly this shape of data, and it is on the free plan:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Free&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data points written&lt;/td&gt;
&lt;td&gt;100,000 per day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read queries&lt;/td&gt;
&lt;td&gt;10,000 per day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retention&lt;/td&gt;
&lt;td&gt;three months&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Compared with KV's thousand writes, that is a hundred times the room, and the write path is designed for one event per request rather than a mutable counter.&lt;/p&gt;

&lt;p&gt;Bind a dataset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# wrangler.toml&lt;/span&gt;
&lt;span class="nn"&gt;[[analytics_engine_datasets]]&lt;/span&gt;
&lt;span class="py"&gt;binding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"CLICKS"&lt;/span&gt;
&lt;span class="py"&gt;dataset&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"redirect_clicks"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then let the Worker issue the redirect and record it on the way past:&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&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;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/offer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CLICKS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeDataPoint&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;indexes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;blobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gclid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;referer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt; &lt;span class="o"&gt;??&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="na"&gt;doubles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&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;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four details in those twenty lines matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not await the write.&lt;/strong&gt; Cloudflare's guidance is explicit that &lt;code&gt;writeDataPoint&lt;/code&gt; is non blocking and returns immediately, with the runtime persisting the data in the background. Awaiting it would add latency to a hop whose entire job is to be fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The index is the sampling key&lt;/strong&gt;, and the limits are one index per call, at most 96 bytes. Put something low cardinality there — the hostname, the campaign — not the full URL. Blobs are where the high cardinality detail goes: up to twenty of them, 16 KB in total, with 250 data points per invocation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the click ID here, at the only hop that is guaranteed to see it.&lt;/strong&gt; This is the same parameter that &lt;a href="https://301.sh/find-the-redirect-that-drops-your-gclid/" rel="noopener noreferrer"&gt;a misconfigured Bulk Redirect quietly deletes&lt;/a&gt;; if your own Worker is the redirect, you decide what survives instead of choosing between two halves of a query string.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Response.redirect(target, 301)&lt;/code&gt; is a real 301.&lt;/strong&gt; No interstitial, no meta refresh, no JavaScript. Search engines see a permanent redirect, and the visitor sees one hop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading it back
&lt;/h2&gt;

&lt;p&gt;Analytics Engine has a SQL API. One &lt;code&gt;curl&lt;/code&gt;, one token with Account Analytics Read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://api.cloudflare.com/client/v4/accounts/{account_id}/analytics_engine/sql"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$CF_API_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s2"&gt;"SELECT blob1 AS path,
                 SUM(_sample_interval) AS clicks
          FROM redirect_clicks
          WHERE timestamp &amp;gt;= NOW() - INTERVAL '7' DAY
          GROUP BY path
          ORDER BY clicks DESC"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SUM(_sample_interval)&lt;/code&gt; rather than &lt;code&gt;COUNT()&lt;/code&gt; is not a stylistic choice. At volume the data is downsampled, and each stored row then stands for several real ones:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;_sample_interval&lt;/code&gt; indicates what the sample rate is for this row (that is, how many rows of the original data are represented by this row)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Count the rows and you will under report, by a factor that changes with traffic. Sum the interval and the number is right at any volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this gives you, and what it does not
&lt;/h2&gt;

&lt;p&gt;It gives you a count of requests, with whatever dimensions you chose to write, on a hop that happens before the browser goes anywhere. Nothing to consent to, nothing for a blocker to block, no cookie, because the click is a request to your own hostname rather than a script loaded from someone else's.&lt;/p&gt;

&lt;p&gt;It is not attribution. Every request is counted, including bots and preview fetchers, and nothing here ties a click to a conversion that happens on a page you do not own. That is a harder problem and a different article.&lt;/p&gt;

&lt;p&gt;And it changes what your redirect is. A Redirect Rule is configuration: no deploy, no code, no one to maintain it. A Worker is software, with a rollback and a person who has to understand it later. For one link that trade is usually worth it. For &lt;a href="https://301.sh/every-way-to-redirect-on-cloudflare/" rel="noopener noreferrer"&gt;the full list of ways to redirect and what each one costs&lt;/a&gt;, the comparison is worth reading before you move a working rule into code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this stops being twenty lines
&lt;/h2&gt;

&lt;p&gt;One redirect, one Worker, one dataset: an afternoon, and the numbers are yours.&lt;/p&gt;

&lt;p&gt;The shape changes when the redirects are a portfolio. Every link that needs counting needs the Worker in front of it, every domain needs the route, and the dataset needs someone to query it on a schedule rather than when somebody wonders. At that point you are maintaining a small analytics product as a side effect of wanting to know how many people clicked. That is the job &lt;a href="https://301.st" rel="noopener noreferrer"&gt;301.st&lt;/a&gt; does by default. For a single link, the Worker above is the whole answer, and it is free.&lt;/p&gt;

</description>
      <category>tracking</category>
      <category>redirects</category>
      <category>cloudflare</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
