<?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: ricco020</title>
    <description>The latest articles on DEV Community by ricco020 (@ricco020).</description>
    <link>https://dev.to/ricco020</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%2F3960216%2Fbc01eb94-de65-41c2-9767-966a157976ae.jpeg</url>
      <title>DEV Community: ricco020</title>
      <link>https://dev.to/ricco020</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ricco020"/>
    <language>en</language>
    <item>
      <title>Leaving Vercel with a static site: the redirects you lose without noticing</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Wed, 07 Oct 2026 11:48:32 +0000</pubDate>
      <link>https://dev.to/ricco020/leaving-vercel-with-a-static-site-the-redirects-you-lose-without-noticing-2i4j</link>
      <guid>https://dev.to/ricco020/leaving-vercel-with-a-static-site-the-redirects-you-lose-without-noticing-2i4j</guid>
      <description>&lt;p&gt;This week I moved four static Astro sites off Vercel and onto a plain container host. The builds were the easy part: &lt;code&gt;npm run build&lt;/code&gt;, copy &lt;code&gt;dist/&lt;/code&gt; into a web server image, done. What almost went wrong is everything Vercel had been doing around the build without my thinking about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure that does not show up
&lt;/h2&gt;

&lt;p&gt;A static site on Vercel is not only a folder of HTML. &lt;code&gt;vercel.json&lt;/code&gt; holds redirects, headers, &lt;code&gt;cleanUrls&lt;/code&gt; and &lt;code&gt;trailingSlash&lt;/code&gt;, and the platform applies them at the edge. Copy the same &lt;code&gt;dist/&lt;/code&gt; behind a default file server and:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the home page answers 200,&lt;/li&gt;
&lt;li&gt;the build is green,&lt;/li&gt;
&lt;li&gt;the deployment is marked healthy,&lt;/li&gt;
&lt;li&gt;and every inherited 301, every &lt;code&gt;/go/...&lt;/code&gt; short link and every extensionless URL returns 404.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing reports it. No build step knows those rules existed. You find out weeks later in Search Console, or you never do. Between them the four sites carry 64 redirects, most inherited from older URL structures, plus header rules. Retyping them by hand is how one gets dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  What had to be rebuilt in the web server
&lt;/h2&gt;

&lt;p&gt;I went with Caddy, and ended up translating five things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Redirects, in file order.&lt;/strong&gt; Vercel evaluates them top to bottom, so the Caddyfile wraps them in a &lt;code&gt;route&lt;/code&gt; block to keep that order. Patterns like &lt;code&gt;/old/:slug&lt;/code&gt; and &lt;code&gt;/docs/:path*&lt;/code&gt; become &lt;code&gt;path_regexp&lt;/code&gt; matchers, and &lt;code&gt;:slug&lt;/code&gt; or &lt;code&gt;$1&lt;/code&gt; in the destination become capture group placeholders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host conditions.&lt;/strong&gt; &lt;code&gt;has: [{ "type": "host", "value": "www.example.com" }]&lt;/code&gt; becomes a &lt;code&gt;host&lt;/code&gt; matcher on the same rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headers.&lt;/strong&gt; The catch-all &lt;code&gt;/(.*)&lt;/code&gt; block becomes one global &lt;code&gt;header&lt;/code&gt; directive, path-specific ones get a named matcher.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cleanUrls&lt;/code&gt; and &lt;code&gt;trailingSlash: false&lt;/code&gt;.&lt;/strong&gt; A redirect from &lt;code&gt;/page.html&lt;/code&gt; to &lt;code&gt;/page&lt;/code&gt;, a redirect away from the trailing slash, then &lt;code&gt;try_files {path} {path}/index.html {path}.html&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language negotiation on &lt;code&gt;/&lt;/code&gt;.&lt;/strong&gt; One site had a small &lt;code&gt;middleware.ts&lt;/code&gt; sending visitors to &lt;code&gt;/fr&lt;/code&gt; or &lt;code&gt;/es&lt;/code&gt; from &lt;code&gt;Accept-Language&lt;/code&gt;. In Caddy that is a &lt;code&gt;header_regexp&lt;/code&gt; matcher and a 302, with &lt;code&gt;Vary: Accept-Language&lt;/code&gt; and &lt;code&gt;Cache-Control: no-store&lt;/code&gt; so no cache pins one visitor's language for everyone. Crawlers send no &lt;code&gt;Accept-Language&lt;/code&gt;, so they keep the default root.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One detail I would not have guessed: short links. A rule on &lt;code&gt;/go/docs&lt;/code&gt; does not match &lt;code&gt;/go/docs/&lt;/code&gt;, and links copied around the web come in both forms. The generated matcher lists both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refuse, do not skip
&lt;/h2&gt;

&lt;p&gt;The part I care most about is what the script does with a rule it cannot translate. &lt;code&gt;rewrites&lt;/code&gt;, &lt;code&gt;routes&lt;/code&gt;, &lt;code&gt;missing&lt;/code&gt;, and &lt;code&gt;has&lt;/code&gt; conditions other than &lt;code&gt;host&lt;/code&gt; stop it with an error naming the rule. A warning would scroll past, and a skipped redirect is exactly the silent 404 the whole exercise is meant to avoid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things that bit me anyway
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;npm ci&lt;/code&gt; failed in the container&lt;/strong&gt; on one site because the lockfile had drifted from &lt;code&gt;package.json&lt;/code&gt;. Vercel had been tolerating it. Decide whether you want to fix the lockfile or use &lt;code&gt;npm install&lt;/code&gt; in the build stage, but find out before the cutover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify on the served site, not the config.&lt;/strong&gt; After each deploy I requested a sample of old URLs against the container's public address and checked the status code and the &lt;code&gt;Location&lt;/code&gt; header. A Caddyfile that parses says nothing about whether your patterns mean what you think.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;It is one Python file, standard library only: it reads &lt;code&gt;vercel.json&lt;/code&gt; and writes a &lt;code&gt;Caddyfile&lt;/code&gt;, a two-stage &lt;code&gt;Dockerfile&lt;/code&gt; (Node build, then &lt;code&gt;caddy:2-alpine&lt;/code&gt;) and a &lt;code&gt;.dockerignore&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;python3 vercel-to-caddy.py &amp;lt;repo&amp;gt; &lt;span class="nt"&gt;--lang-root&lt;/span&gt; fr,es &lt;span class="nt"&gt;--out&lt;/span&gt; dist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source, a worked example and the list of what is and is not handled: &lt;a href="https://gitlab.com/ler.eric/vercel-to-caddy" rel="noopener noreferrer"&gt;gitlab.com/ler.eric/vercel-to-caddy&lt;/a&gt;. It only covers static output; anything that needs functions or server rendering is out of scope.&lt;/p&gt;

&lt;p&gt;I do this kind of migration and site work for clients at &lt;a href="https://z-ax.com" rel="noopener noreferrer"&gt;Z-AX&lt;/a&gt;. If you have a &lt;code&gt;vercel.json&lt;/code&gt; pattern the script refuses, open an issue with the rule: the refusal list is where it should grow next.&lt;/p&gt;

</description>
      <category>caddy</category>
      <category>vercel</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>80,000 Search Console impressions, zero clicks: three checks before you call it an opportunity</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Wed, 07 Oct 2026 08:20:18 +0000</pubDate>
      <link>https://dev.to/ricco020/80000-search-console-impressions-zero-clicks-three-checks-before-you-call-it-an-opportunity-3gdl</link>
      <guid>https://dev.to/ricco020/80000-search-console-impressions-zero-clicks-three-checks-before-you-call-it-an-opportunity-3gdl</guid>
      <description>&lt;p&gt;One of my sites went from about 850 Search Console impressions a day to 16,000 in a week. Before rewriting anything to "capture the opportunity", I spent ten minutes on the numbers. I am glad I did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;Over 28 days the site showed 133,505 impressions. One page carried 88,640 of them: an article about &lt;a href="https://www.anonymflow.com/en/blog/real-price-of-vpns-with-commitment" rel="noopener noreferrer"&gt;what NordVPN really costs over 24 months&lt;/a&gt;. One query carried most of that page:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;Impressions&lt;/th&gt;
&lt;th&gt;Clicks&lt;/th&gt;
&lt;th&gt;Avg. position&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;nordvpn cost&lt;/td&gt;
&lt;td&gt;80,721&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;9.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nord vpn preis&lt;/td&gt;
&lt;td&gt;6,729&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2.6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zero clicks at position 9 on eighty thousand impressions is unusual. Zero clicks at position 2.6 on nearly seven thousand is not something a human audience produces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three checks that took the "opportunity" apart
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Compare with the site's own pages at the same rank.&lt;/strong&gt; On the same property, in the same period, a French article at position 7.3 got 18 clicks from 985 impressions, and a Spanish one at 6.7 got 9 from 986. So the site does get clicks at these positions. This one query does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Look at who is supposedly searching.&lt;/strong&gt; The property's top countries by impressions were Austria (18,895, 0 clicks), Denmark, the United States, Norway and the Netherlands. For an English page about a price in euros, Austria and Norway leading with no clicks at all is not a reader profile. 83% of impressions were on desktop, while clicks were split almost evenly between desktop and mobile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Look at the shape over time.&lt;/strong&gt; Daily impressions sat around 800, jumped to 4,953 on 24 September, stayed between 9,000 and 16,700 for six days, then dropped to 2,579 on 1 October. Demand for a product price does not behave like that. A scheduled job does.&lt;/p&gt;

&lt;p&gt;I cannot prove what generated those impressions. Rank trackers and scrapers querying Google at scale are the obvious candidates, and Search Console counts an impression whenever the result is rendered in a results page, whoever asked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed
&lt;/h2&gt;

&lt;p&gt;Nothing on the page. Its title already answers the query, and rewriting a title to chase readers who are not there risks losing the ones who are.&lt;/p&gt;

&lt;p&gt;What I changed is the rule I work by: &lt;strong&gt;an impressions number is not a demand number until the clicks at that position look like the rest of the site.&lt;/strong&gt; The check is cheap:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take the page's query with the most impressions.&lt;/li&gt;
&lt;li&gt;Find two other pages on the same property at a similar average position.&lt;/li&gt;
&lt;li&gt;Compare click-through rates. An order of magnitude apart, with zero clicks, means the impressions are not people.&lt;/li&gt;
&lt;li&gt;Split by country and device. If the split looks nothing like the pages that do get clicks, stop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The dashboard total had told me this site was sitting on its biggest opening of the year. Excluding that single query, its impressions for the period were about 52,800, with the same 180 clicks. That is the number I plan with now.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>analytics</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Three of the eight sites that support /.well-known/change-password do not support it</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Sun, 23 Aug 2026 22:31:06 +0000</pubDate>
      <link>https://dev.to/ricco020/three-of-the-eight-sites-that-support-well-knownchange-password-do-not-support-it-251p</link>
      <guid>https://dev.to/ricco020/three-of-the-eight-sites-that-support-well-knownchange-password-do-not-support-it-251p</guid>
      <description>&lt;p&gt;There is a small W3C standard called &lt;a href="https://w3c.github.io/webappsec-change-password-url/" rel="noopener noreferrer"&gt;A Well-Known URL for Changing Passwords&lt;/a&gt;. A site serves &lt;code&gt;/.well-known/change-password&lt;/code&gt;, redirects it to its real change-password page, and password managers can send a user straight there instead of making them hunt through account settings.&lt;/p&gt;

&lt;p&gt;I checked 24 well known sites for it tonight. The interesting part is not how many support it. It is that &lt;strong&gt;three of the eight apparent supporters do not support it at all&lt;/strong&gt;, and the specification predicted exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap is written into the spec
&lt;/h2&gt;

&lt;p&gt;A naive check is "does &lt;code&gt;/.well-known/change-password&lt;/code&gt; return 200". That check is wrong, because plenty of servers return 200 for &lt;em&gt;everything&lt;/em&gt;. A SPA with a catch-all route will happily serve its shell for any path you invent.&lt;/p&gt;

&lt;p&gt;So the spec includes a countermeasure. Section 5 tells clients to also request a URL that should never exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/.well-known/resource-that-should-not-exist-whatever-forty-two
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns 200 too, the server is not answering your question. It is just saying 200 at everything. The support signal is void.&lt;/p&gt;

&lt;p&gt;That is a rare thing to find in a standard: the authors anticipated the false positive and shipped the control with the feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;Two requests per host, one for the standard path and one for the control path, real HTTP, browser User-Agent, 24 hosts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;supports it for real   5 / 24     change-password 200, control 404
false positive         3 / 24     change-password 200, control 200
no support            16 / 24     404 (13), 403 (2), 400 (1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real support:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;github.com        200 / 404
reddit.com        200 / 404
wordpress.com     200 / 404
apple.com         200 / 404
1password.com     200 / 404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;False positives, which a 200-only check would have counted as wins:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;twitter.com       200 / 200
netflix.com       200 / 200
linkedin.com      200 / 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the control request I would have reported 8 supporters instead of 5. That is a 60 % overcount, from a check that never threw an error and looked green all the way down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I did not expect
&lt;/h2&gt;

&lt;p&gt;I included six password managers, since this standard exists mainly for their benefit. They are the consumers of the signal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1password.com     supports it
bitwarden.com     404
dashlane.com      404
proton.me         404
lastpass.com      404
nordpass.com      404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One out of six serves the standard that password managers rely on. I am not going to pretend that is a scandal, these are marketing domains and the account systems often live elsewhere. But if you are building anything that touches &lt;a href="https://www.pwdfortress.com/en/blog/are-passkeys-safe" rel="noopener noreferrer"&gt;password and passkey hygiene&lt;/a&gt;, it is a useful reminder that ecosystem support is thinner than the existence of a spec suggests.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to add it
&lt;/h2&gt;

&lt;p&gt;It is genuinely a two line change on most stacks. Redirect &lt;code&gt;/.well-known/change-password&lt;/code&gt; to your existing change-password page with a 302, and make sure your 404 handler actually returns 404 for unknown paths under &lt;code&gt;/.well-known/&lt;/code&gt;. That second half is the part people get wrong, and it is the half that makes the first half detectable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general lesson
&lt;/h2&gt;

&lt;p&gt;I have spent a lot of this week finding checks that returned a confident answer to a question slightly different from the one I meant to ask. This is the same shape, with one difference: here the spec authors saw it coming and gave you the control request for free.&lt;/p&gt;

&lt;p&gt;Most of the time nobody hands you the control. You have to think of it yourself, which usually means asking "what would this check return if the thing I am testing were completely absent?" If the answer is "the same thing", the check is decorative.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>standards</category>
      <category>testing</category>
    </item>
    <item>
      <title>Google said it had never heard of my URL. The URL was fine, my probe was not.</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Sun, 23 Aug 2026 22:23:06 +0000</pubDate>
      <link>https://dev.to/ricco020/google-said-it-had-never-heard-of-my-url-the-url-was-fine-my-probe-was-not-53gg</link>
      <guid>https://dev.to/ricco020/google-said-it-had-never-heard-of-my-url-the-url-was-fine-my-probe-was-not-53gg</guid>
      <description>&lt;p&gt;Last night I ran Google's URL Inspection API against a site I run and got this back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;coverageState &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Google&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ne&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reconnait&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pas&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cette&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;URL'&lt;/span&gt;
&lt;span class="na"&gt;lastCrawl     &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;JAMAIS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Google has never heard of this URL." On a page that had been in the sitemap for weeks.&lt;/p&gt;

&lt;p&gt;I was about to open a very large investigation. The page was fine. My probe was not. Here are the three ways that API lied to me in one sitting, and the one number that finally told me something true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 1: www and non-www are different URLs to this API
&lt;/h2&gt;

&lt;p&gt;I passed &lt;code&gt;https://save-my-disk.com/fr/blog/carte-sd-non-detectee&lt;/code&gt;. The sitemap declares &lt;code&gt;https://www.save-my-disk.com/...&lt;/code&gt;. Same site to a human. Two different URLs to the API.&lt;/p&gt;

&lt;p&gt;The result is not an error. It is not a 404. It is a confident, well-formed answer to a question I did not mean to ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;without www &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Google&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ne&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reconnait&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pas&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cette&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;URL'&lt;/span&gt;   &lt;span class="s"&gt;lastCrawl JAMAIS&lt;/span&gt;
&lt;span class="na"&gt;with www    &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Envoyee&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;et&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;indexee'&lt;/span&gt;                  &lt;span class="s"&gt;lastCrawl 2026-07-18&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same page. Indexed the whole time.&lt;/p&gt;

&lt;p&gt;The rule I now follow: &lt;strong&gt;never compose the URL you inspect. Read it out of the sitemap you actually serve.&lt;/strong&gt; I fetch &lt;code&gt;sitemap.xml&lt;/code&gt;, grep the slug, and inspect that exact string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 2: the &lt;code&gt;indexed&lt;/code&gt; field on sitemaps is zero for everyone
&lt;/h2&gt;

&lt;p&gt;The Sitemaps endpoint returns a &lt;code&gt;contents&lt;/code&gt; block that looks like it answers the only question you care about:&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"submitted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3258&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"indexed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&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;That &lt;code&gt;indexed: 0&lt;/code&gt; is not a measurement. I have a site doing 1677 impressions a month reporting &lt;code&gt;indexed: 0&lt;/code&gt;. Every property I own reports &lt;code&gt;indexed: 0&lt;/code&gt;. If you build a dashboard on that field you will build an alarm that fires forever.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;errors&lt;/code&gt; and &lt;code&gt;warnings&lt;/code&gt; from the same block. Those are real. Ignore &lt;code&gt;indexed&lt;/code&gt; and inspect URLs individually if you want indexing state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 3: two states that read alike and mean opposite things
&lt;/h2&gt;

&lt;p&gt;This is the one that matters, and it is the reason the whole exercise was worth it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Discovered - currently not indexed"&lt;/strong&gt; means Googlebot knows the URL exists and &lt;strong&gt;has never fetched it&lt;/strong&gt;. &lt;code&gt;lastCrawl&lt;/code&gt; is empty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Crawled - currently not indexed"&lt;/strong&gt; means Googlebot &lt;strong&gt;fetched the page, read it, and declined to index it&lt;/strong&gt;. &lt;code&gt;lastCrawl&lt;/code&gt; has a date.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They sit next to each other in the same report. They call for opposite work. The first is a crawl budget and discovery problem: more internal links, sitemap pings, authority. The second is a quality or duplication judgment: the page was seen and found wanting, and adding more pages like it makes things worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number
&lt;/h2&gt;

&lt;p&gt;I sampled 14 article URLs at random from a 590-page corpus on &lt;a href="https://www.save-my-disk.com/en/blog/sd-card-not-showing-up" rel="noopener noreferrer"&gt;a data recovery site I run&lt;/a&gt;, and inspected each one. I validated the probe on a known-healthy control page first, which came back "Submitted and indexed" as expected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 4/14   Submitted and indexed                      29 %
 4/14   Google does not recognise this URL     |
 4/14   Discovered - currently not indexed     |   8/14 with lastCrawl = never
 2/14   Crawled - currently not indexed            fetched, then declined
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight of fourteen pages had never been fetched at all. Only two had been read and rejected.&lt;/p&gt;

&lt;p&gt;I had spent the previous hour preparing to write another article for that site. The measurement says that would have produced a 657th URL that Googlebot has never visited. The bottleneck is not the writing. It is that two thirds of what already exists has never been looked at.&lt;/p&gt;

&lt;p&gt;That reframes the work entirely: internal linking, sitemap hygiene and off-site authority, instead of publishing volume. Not a conclusion I wanted, which is usually a sign it is the real one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell past me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Read the URL out of the artifact you serve. Do not build it from a folder name or a slug you remember.&lt;/li&gt;
&lt;li&gt;Run any new probe against a page you already know is healthy, &lt;strong&gt;before&lt;/strong&gt; you trust it on the page you are worried about.&lt;/li&gt;
&lt;li&gt;When a check returns zero on everything it looks at, suspect the check first.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The API is not wrong in any of these cases. It answers precisely the question you asked. The gap between that question and the one you meant is where the hour goes.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>googlecloud</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Eight checks lied to me in one day, and none of them threw an error</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Fri, 21 Aug 2026 15:11:19 +0000</pubDate>
      <link>https://dev.to/ricco020/eight-checks-lied-to-me-in-one-day-and-none-of-them-threw-an-error-18m1</link>
      <guid>https://dev.to/ricco020/eight-checks-lied-to-me-in-one-day-and-none-of-them-threw-an-error-18m1</guid>
      <description>&lt;p&gt;In one working day I wrote or relied on eight checks that were wrong. &lt;strong&gt;Not one of them threw an&lt;br&gt;
error.&lt;/strong&gt; Every single one returned a plausible answer, which is exactly why each cost me time.&lt;/p&gt;

&lt;p&gt;Here they are, with what they actually returned.&lt;/p&gt;
&lt;h2&gt;
  
  
  The four ways a check can lie
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. It never runs
&lt;/h3&gt;

&lt;p&gt;I wrote assertions that validate prices against the strings shown to users, then deliberately&lt;br&gt;
corrupted one to confirm the build would fail.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sabotaged price string
BUILD EXIT=0     &amp;lt;- the guard saw nothing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The assertions run on import. &lt;strong&gt;Nothing imported the module yet.&lt;/strong&gt; After wiring it into a page that&lt;br&gt;
is actually built, the same sabotage gave &lt;code&gt;EXIT=1&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A check nothing calls protects nothing, and it is &lt;strong&gt;worse&lt;/strong&gt; than no check, because you believe you&lt;br&gt;
are covered.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  2. It caches a stale verdict
&lt;/h3&gt;

&lt;p&gt;A crawler wrote &lt;code&gt;sitemap_absent: true&lt;/code&gt; on the first pass. I fixed the fetch, the data came back&lt;br&gt;
correctly, and the report still printed &lt;strong&gt;"SITEMAP NOT SERVED"&lt;/strong&gt; — because the flag was never&lt;br&gt;
cleared.&lt;/p&gt;

&lt;p&gt;The JSON on disk said 445 URLs. The screen said absent. &lt;strong&gt;I only caught it by opening the artefact&lt;br&gt;
instead of reading the summary.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  3. It asks for the wrong shape
&lt;/h3&gt;

&lt;p&gt;Counting rendered questions on a page returned &lt;strong&gt;0&lt;/strong&gt;. I nearly reported that a paid feature shipped&lt;br&gt;
nothing.&lt;/p&gt;

&lt;p&gt;My selectors looked for DOM classes. The items were &lt;strong&gt;props of a client component&lt;/strong&gt;, serialised into&lt;br&gt;
the framework payload as &lt;code&gt;{"q": …}&lt;/code&gt;. The page was 16,908 bytes in both measurements. &lt;strong&gt;The content&lt;br&gt;
was always there; only my way of counting was wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The fix was not a better guess. It was reading the type definition and counting what the code&lt;br&gt;
actually emits.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. It includes itself in its own sample
&lt;/h3&gt;

&lt;p&gt;Two on the same day, and this is my favourite.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pgrep &lt;span class="nt"&gt;-af&lt;/span&gt; &lt;span class="s2"&gt;"next start"&lt;/span&gt;
&lt;span class="c"&gt;# returns only the bash -c wrapper that contains the pattern&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old server was still holding the port under a different process name. I found it with&lt;br&gt;
&lt;code&gt;ss -ltnp&lt;/code&gt; — &lt;strong&gt;searching for who holds the port always works; searching for a name you assume it has&lt;br&gt;
does not.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then, checking whether a new slug collided with existing ones, I globbed the directory — including&lt;br&gt;
the file I had just written. It saw itself and answered &lt;strong&gt;"already taken."&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A probe that includes itself in its own sample always confirms what it is looking for.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  The two failure modes, and why calibration is the whole job
&lt;/h2&gt;

&lt;p&gt;A check can fail in exactly two directions, and I hit both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;too weak   counting accented characters       -&amp;gt; a text with a few accents passes while being wrong
too strong requiring a fixed list of words    -&amp;gt; cries on correct text that had no reason to use them
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The German page was correct. My check demanded words the article never needed, and shouted anyway.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A false alarm that repeats gets ignored, and an ignored check protects nothing.&lt;/strong&gt; That is the&lt;br&gt;
quietest way to lose a safeguard: it stays in the code, it goes green in the reader's head, and it&lt;br&gt;
stops saying anything.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The calibrated version turned out to be: &lt;strong&gt;a non-zero count of language-specific characters AND a&lt;br&gt;
few witness words present — not all of them.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do now, and it costs three minutes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Test the guard by making it fail on purpose.&lt;/strong&gt; Not once written, but every time it is written.&lt;br&gt;
Sabotage the input, confirm it screams, restore. Two of my checks passed that test only on the second&lt;br&gt;
attempt, and I would never have known otherwise.&lt;/p&gt;

&lt;p&gt;Two other habits that came out of the same day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read the artefact, not the summary.&lt;/strong&gt; The JSON, the manifest, the served HTML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prove identity, not just presence.&lt;/strong&gt; "The server responds" is not "my version responds." A
deployment I thought I was testing had never started; the old process answered &lt;code&gt;200&lt;/code&gt; all along.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the same shape as a thing I ended up writing about elsewhere: a VPN kill switch&lt;br&gt;
toggle being on tells you what the app intends, not what the operating system will do when the tunnel&lt;br&gt;
dies at an awkward moment. I wrote up the four tests that actually settle it&lt;br&gt;
&lt;a href="https://www.vpnsmith.com/en/blog/vpn-kill-switch-test-2026" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eight wrong checks, zero errors raised.&lt;/strong&gt; A check that crashes gets fixed. A check that answers&lt;br&gt;
slightly the wrong question gets believed.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>debugging</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>Two traps in the Bing Webmaster API that silently invert your site comparison</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Fri, 21 Aug 2026 05:05:00 +0000</pubDate>
      <link>https://dev.to/ricco020/two-traps-in-the-bing-webmaster-api-that-silently-invert-your-site-comparison-57om</link>
      <guid>https://dev.to/ricco020/two-traps-in-the-bing-webmaster-api-that-silently-invert-your-site-comparison-57om</guid>
      <description>&lt;p&gt;I spent a night comparing two sites in the Bing Webmaster API and reached three different&lt;br&gt;
conclusions, each one demolishing the last. All three came from the same two traps. Neither raises&lt;br&gt;
an error. Neither shows up in the JSON. Both silently invert the comparison.&lt;/p&gt;

&lt;p&gt;Figures below are anonymised, because the ratios are the point, not whose sites they are.&lt;/p&gt;
&lt;h2&gt;
  
  
  Trap 1: &lt;code&gt;GetCrawlStats&lt;/code&gt; history length is per site, and it is not the same
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;site A   76 data points   2026-06-05 .. 2026-08-19
site B    7 data points   2026-08-13 .. 2026-08-19
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I wrote the obvious thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CrawledPages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For site A that sums 30 days. &lt;strong&gt;For site B it sums 7&lt;/strong&gt;, because there are only 7. No error, no&lt;br&gt;
warning, just a number that is roughly a quarter of what I thought I was comparing.&lt;/p&gt;

&lt;p&gt;My conclusion was "site B is crawled nine times less". Normalised per day, &lt;strong&gt;site B is crawled&lt;br&gt;
more&lt;/strong&gt;: 168 pages/day against 121.&lt;/p&gt;

&lt;p&gt;The cause is that Bing does not backfill. A property verified last week has a week of history, not&lt;br&gt;
a week of low activity. &lt;strong&gt;A short history looks exactly like a quiet site if you only read the&lt;br&gt;
sum.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# what it should have been
&lt;/span&gt;&lt;span class="n"&gt;per_day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CrawledPages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;window&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dates&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dates&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# and print it, always
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Trap 2: &lt;code&gt;GetPageStats&lt;/code&gt; covers a different date range per site, and never tells you
&lt;/h2&gt;

&lt;p&gt;This one is worse, because there is no date parameter to get wrong. You call it, you get rows, each&lt;br&gt;
row has a &lt;code&gt;Date&lt;/code&gt;. I never looked at those dates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;site A   GetPageStats covers  2026-06-05 .. 2026-06-19    (15 days, in JUNE)
site B   GetPageStats covers  2026-08-14 .. 2026-08-14    (ONE day, in AUGUST)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had been comparing &lt;strong&gt;June against August, fifteen days against one&lt;/strong&gt;, and calling it a finding&lt;br&gt;
about the sites. The whole analysis, and the follow-up work it justified, rested on that.&lt;/p&gt;
&lt;h2&gt;
  
  
  The check that caught it, and why it was written first
&lt;/h2&gt;

&lt;p&gt;Before running any of this I had written down one rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If &lt;code&gt;GetPageStats&lt;/code&gt; total impressions differ from &lt;code&gt;GetRankAndTrafficStats&lt;/code&gt; over the same window, the&lt;br&gt;
endpoint is sampling and the page counts are worthless. Stop there.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The measurement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;site A   PageStats 12 impressions   RankAndTraffic 10   -&amp;gt;  120 % coverage
site B   PageStats 714 impressions  RankAndTraffic 325  -&amp;gt;  220 % coverage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two endpoints, same property, same window, and they do not agree with each other. So the check&lt;br&gt;
fired and I stopped, instead of building one more conclusion on top.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Writing that rule down before measuring is the only reason I caught it.&lt;/strong&gt; I had also written a&lt;br&gt;
"reserve" in the earlier analysis, hedging that the average was fragile on a small sample. That&lt;br&gt;
hedge was aimed at the right object for the wrong reason: the problem was never the average, it was&lt;br&gt;
the window. &lt;strong&gt;A smart-sounding caveat can cover a defect without naming it, and reassure you into&lt;br&gt;
keeping the wrong number.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell anyone using this API
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Print the date range and the row count of every response&lt;/strong&gt; before you compute anything. Not in
debug mode. Always.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Never sum across sites without dividing by the number of points.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-check two endpoints on the same window.&lt;/strong&gt; If they disagree, neither is a total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the invalidation condition before you measure&lt;/strong&gt;, not after you like the result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is specific to Bing. GSC anonymises rare queries, so summing by query dimension covered&lt;br&gt;
only 19 % of impressions on one of my properties, and comparing two sites that way &lt;strong&gt;reversed their&lt;br&gt;
ranking&lt;/strong&gt;. Same shape of error: an API answered exactly what I asked, and I had asked something&lt;br&gt;
slightly different from what I meant.&lt;/p&gt;

&lt;p&gt;The sites in the table are mine; one of them is &lt;a href="https://www.vpnsmith.com/en/blog/cheapest-vps-wireguard-vpn-2026" rel="noopener noreferrer"&gt;VPNSmith&lt;/a&gt;,&lt;br&gt;
where the traffic data came from. I am not going to publish their raw numbers, which is why the&lt;br&gt;
ratios above are anonymised.&lt;/p&gt;

&lt;p&gt;If you have a comparison in a dashboard right now that spans two properties, check that both sides&lt;br&gt;
cover the same dates. Mine did not, for three hours, and it was confident the whole time.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>api</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Two thirds of VPN vendors ship no Content-Security-Policy. Including, sort of, us.</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Thu, 20 Aug 2026 21:38:33 +0000</pubDate>
      <link>https://dev.to/ricco020/two-thirds-of-vpn-vendors-ship-no-content-security-policy-including-sort-of-us-i8</link>
      <guid>https://dev.to/ricco020/two-thirds-of-vpn-vendors-ship-no-content-security-policy-including-sort-of-us-i8</guid>
      <description>&lt;p&gt;I scanned the public homepages of thirty commercial VPN vendors for the six HTTP response headers&lt;br&gt;
that browsers actually act on. One GET each, 2026-08-20, 29 of 30 hosts answering.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mean score 3.66 / 6

Strict-Transport-Security   26/29   90%
X-Content-Type-Options      25/29   86%
X-Frame-Options             23/29   79%
Referrer-Policy             14/29   48%
Content-Security-Policy     10/29   34%   &amp;lt;-- the interesting one
Permissions-Policy           8/29   28%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Two out of three companies selling security do not ship a Content-Security-Policy.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why that particular header
&lt;/h2&gt;

&lt;p&gt;HSTS is one line in a config file and every hosting guide mentions it. That is why it is at 90%.&lt;/p&gt;

&lt;p&gt;CSP is the one that costs real work, because writing a useful one means auditing every script the&lt;br&gt;
marketing department has added over the years. It is the header that separates "we followed a&lt;br&gt;
checklist" from "someone owns this".&lt;/p&gt;

&lt;p&gt;Full scores (6/6): cryptostorm.is, mullvad.net, nordvpn.com, torguard.net, windscribe.com.&lt;br&gt;
One vendor scored 0/6.&lt;/p&gt;
&lt;h2&gt;
  
  
  The part that makes this post worth writing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;We are in the 34%, and our CSP is close to worthless.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;script-src&lt;/span&gt; &lt;span class="s"&gt;'self'&lt;/span&gt; &lt;span class="s"&gt;'unsafe-inline'&lt;/span&gt; &lt;span class="s"&gt;'unsafe-eval'&lt;/span&gt; &lt;span class="s"&gt;https:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inline scripts allowed, &lt;code&gt;eval&lt;/code&gt; allowed, script from any HTTPS host allowed. Against the injection&lt;br&gt;
attacks a CSP exists to stop, that policy blocks very nearly nothing. We ship the header. We do not&lt;br&gt;
ship the defence.&lt;/p&gt;

&lt;p&gt;Which is the actual finding, and the reason it is printed on the dataset itself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Presence is not correctness.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ten vendors have a CSP. I did not check how many of those ten have a CSP worth having, because that&lt;br&gt;
means reading each policy line by line. That work is not done, so the claim is not made.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, stated up front
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One observation, one moment.&lt;/strong&gt; Headers change, sometimes weekly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One vantage point.&lt;/strong&gt; A CDN can inject or strip headers per edge node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Homepage only.&lt;/strong&gt; Checkout and account pages matter more and are not in this scan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presence, not correctness.&lt;/strong&gt; As above. It is the main way to misread the whole table.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;The vendor list, the six header names and the scoring rule are published as a dataset under&lt;br&gt;
&lt;a href="https://zenodo.org/records/22032840" rel="noopener noreferrer"&gt;DOI 10.5281/zenodo.22032840&lt;/a&gt;, so you can disagree with my&lt;br&gt;
reading without taking my word for the numbers. Any HTTP client reproduces it in a few minutes, and&lt;br&gt;
differences from this snapshot are expected and informative in themselves.&lt;/p&gt;

&lt;p&gt;The longer write-up, including why a header scan tells you nothing at all about whether a VPN&lt;br&gt;
service is trustworthy, is on &lt;a href="https://www.vpnsmith.com/en/blog/is-nordvpn-safe-header-scan" rel="noopener noreferrer"&gt;VPNSmith&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you run any public site: check your own &lt;code&gt;script-src&lt;/code&gt; before you feel good about having a CSP. I&lt;br&gt;
did, and that is how this post got its most useful paragraph.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>privacy</category>
      <category>devops</category>
    </item>
    <item>
      <title>GitHub says these three repos have no licence. They all do.</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Thu, 20 Aug 2026 01:52:17 +0000</pubDate>
      <link>https://dev.to/ricco020/github-says-these-three-repos-have-no-licence-they-all-do-1nl8</link>
      <guid>https://dev.to/ricco020/github-says-these-three-repos-have-no-licence-they-all-do-1nl8</guid>
      <description>&lt;p&gt;GitHub puts a licence label on every repository. It reads it from the &lt;code&gt;LICENSE&lt;/code&gt; file and&lt;br&gt;
normalises it to an SPDX identifier — &lt;code&gt;MIT&lt;/code&gt;, &lt;code&gt;BSD-3-Clause&lt;/code&gt;, &lt;code&gt;Apache-2.0&lt;/code&gt;. When it cannot,&lt;br&gt;
the API returns &lt;code&gt;NOASSERTION&lt;/code&gt; and the web UI quietly says &lt;strong&gt;"Other"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I checked twelve popular proxy, VPN and mesh-networking repositories through the GitHub API&lt;br&gt;
on 20 August 2026. Three came back &lt;code&gt;NOASSERTION&lt;/code&gt;. Here is what each of the three actually&lt;br&gt;
ships — because "Other" is not one situation, it is three very different ones.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repository&lt;/th&gt;
&lt;th&gt;SPDX from the API&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SagerNet/sing-box&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;NOASSERTION&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;37,141&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XTLS/Xray-core&lt;/td&gt;
&lt;td&gt;MPL-2.0&lt;/td&gt;
&lt;td&gt;41,121&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;juanfont/headscale&lt;/td&gt;
&lt;td&gt;BSD-3-Clause&lt;/td&gt;
&lt;td&gt;43,009&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tailscale/tailscale&lt;/td&gt;
&lt;td&gt;BSD-3-Clause&lt;/td&gt;
&lt;td&gt;35,368&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v2fly/v2ray-core&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;34,491&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wireguard/wireguard-go&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;4,347&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;apernet/hysteria&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;22,355&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EAimTY/tuic&lt;/td&gt;
&lt;td&gt;GPL-3.0&lt;/td&gt;
&lt;td&gt;3,261&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cloudflare/cloudflared&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;15,304&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;netbirdio/netbird&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;NOASSERTION&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;28,520&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zerotier/ZeroTierOne&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;NOASSERTION&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;17,029&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;slackhq/nebula&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;17,630&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three out of twelve. Now the interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. sing-box — GPLv3, plus a clause of its own
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;LICENSE&lt;/code&gt; file opens with the standard GPL version 3 or later grant. Then it adds:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In addition, no derivative work may use the name or imply association with this&lt;br&gt;
application without prior consent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence is why the label cannot be &lt;code&gt;GPL-3.0&lt;/code&gt;: the text is no longer the unmodified&lt;br&gt;
licence. The grant is still copyleft, with a naming restriction bolted on.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. netbird — BSD-3-Clause, except where it is AGPLv3
&lt;/h2&gt;

&lt;p&gt;Its &lt;code&gt;LICENSE&lt;/code&gt; file says it plainly in the first two lines:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This BSD-3-Clause license applies to all parts of the repository except for the&lt;br&gt;
directories [...] Those directories are licensed under the GNU Affero General Public&lt;br&gt;
License version 3.0.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A permissive licence for most of the tree, and the strongest network copyleft there is for&lt;br&gt;
part of it. One SPDX identifier cannot express that, and picking the friendlier of the two&lt;br&gt;
is exactly the mistake the label would encourage.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. ZeroTier — MPL, and a &lt;code&gt;nonfree/&lt;/code&gt; directory
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;LICENSE.txt&lt;/code&gt; is three lines, and the second one is the one that matters:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;See nonfree/LICENSE.md for all non-free ("source available") portions of this repository.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Source-available is not open source. A repository can be mostly MPL and still contain a&lt;br&gt;
directory you may read but not freely reuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to take from this
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;NOASSERTION&lt;/code&gt; does not mean "no licence" and it does not mean "unclear". It means &lt;strong&gt;GitHub&lt;br&gt;
declined to summarise this one in a single word&lt;/strong&gt;, and in all three cases it was right to&lt;br&gt;
decline. An extra clause, a per-directory split, and a non-free carve-out are all things&lt;br&gt;
that matter the moment you ship something built on top.&lt;/p&gt;

&lt;p&gt;The practical rule is boring and holds up: &lt;strong&gt;if you are redistributing, open the &lt;code&gt;LICENSE&lt;/code&gt;&lt;br&gt;
file.&lt;/strong&gt; The badge is a convenience, not a statement of terms. Two minutes of reading, and&lt;br&gt;
the three repositories above stop being surprises.&lt;/p&gt;

&lt;p&gt;I went down this road while comparing two of these cores in detail — the licence turned out&lt;br&gt;
to be the sharpest difference between them, well ahead of the protocol lists everyone else&lt;br&gt;
compares:&lt;br&gt;
&lt;a href="https://www.vpnsmith.com/en/blog/sing-box-vs-xray-2026" rel="noopener noreferrer"&gt;sing-box vs Xray: the differences that actually decide it&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All figures read from the GitHub REST API on 20 August 2026; star counts move, the licence&lt;br&gt;
files are the stable part.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>go</category>
      <category>licensing</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your sysctl says one thing, the kernel says another</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Wed, 19 Aug 2026 05:06:12 +0000</pubDate>
      <link>https://dev.to/ricco020/your-sysctl-says-one-thing-the-kernel-says-another-4gha</link>
      <guid>https://dev.to/ricco020/your-sysctl-says-one-thing-the-kernel-says-another-4gha</guid>
      <description>&lt;p&gt;You set &lt;code&gt;net.ipv4.ip_forward=1&lt;/code&gt; in &lt;code&gt;/etc/sysctl.conf&lt;/code&gt;. You rebooted. The value is still &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Nothing is broken, and you did not make a typo. On a host running UFW, &lt;strong&gt;there is a second sysctl file, and it wins&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two files, one kernel
&lt;/h2&gt;

&lt;p&gt;UFW ships &lt;code&gt;/etc/ufw/sysctl.conf&lt;/code&gt; and loads it when the firewall comes up. It takes precedence over &lt;code&gt;/etc/sysctl.conf&lt;/code&gt;. Anything you set in the familiar file that UFW also sets is quietly overwritten on the next &lt;code&gt;ufw enable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Quietly is the operative word. There is no warning, no log line, no diff. The file you edited still contains exactly what you typed — which is why this costs hours rather than minutes: you keep re-reading a correct file.&lt;/p&gt;

&lt;p&gt;The symptom that most often brings people here: a WireGuard tunnel that comes up, a client that receives its address, and no traffic going anywhere. &lt;code&gt;ip_forward&lt;/code&gt; fell back to &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the effective value, not the file
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sysctl net.ipv4.ip_forward          &lt;span class="c"&gt;# what the kernel applies&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/sys/net/ipv4/ip_forward   &lt;span class="c"&gt;# same thing, one file&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that disagrees with what you wrote, look in &lt;code&gt;/etc/ufw/sysctl.conf&lt;/code&gt;. Note the notation there uses slashes:&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;net&lt;/span&gt;/&lt;span class="n"&gt;ipv4&lt;/span&gt;/&lt;span class="n"&gt;ip_forward&lt;/span&gt;=&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both &lt;code&gt;net.ipv4.ip_forward&lt;/code&gt; and &lt;code&gt;net/ipv4/ip_forward&lt;/code&gt; name the same knob. Grepping for only one of them is a good way to conclude the setting is absent when it is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small script to stop guessing
&lt;/h2&gt;

&lt;p&gt;I wrote a read-only checker that compares &lt;strong&gt;effective&lt;/strong&gt; sysctl values against hardening recommendations and, more importantly, flags the ones that are &lt;em&gt;declared somewhere and not applied&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; ECART  kernel.kptr_restrict            effectif=0      attendu=2
 ECART  fs.protected_hardlinks          effectif=0      attendu=1
         declare a 1 dans /etc/sysctl.conf, mais le noyau applique 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second line is the whole point. No dependencies, Python 3 only, changes nothing:&lt;br&gt;
&lt;a href="https://gitlab.com/ler.eric/sysctl-hardening-check" rel="noopener noreferrer"&gt;https://gitlab.com/ler.eric/sysctl-hardening-check&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  While we are here: what hardening does not cover
&lt;/h2&gt;

&lt;p&gt;Tightening sysctl narrows what a compromised process reaches &lt;em&gt;on that machine&lt;/em&gt;. It says nothing about the paths that never touch the machine — a password reused on a service that leaked, a session left open on a device you no longer carry, the one account that receives every recovery link.&lt;/p&gt;

&lt;p&gt;That distinction is worth keeping in view: &lt;a href="https://secure-os.org/articles/linux-hardening" rel="noopener noreferrer"&gt;Linux hardening, what it covers and what it does not&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One line to keep: &lt;strong&gt;the file you edited is not necessarily the file that wins.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>devops</category>
      <category>sysadmin</category>
    </item>
    <item>
      <title>UFW and WireGuard: the tunnel is up and nothing goes through</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Wed, 19 Aug 2026 03:37:35 +0000</pubDate>
      <link>https://dev.to/ricco020/ufw-and-wireguard-the-tunnel-is-up-and-nothing-goes-through-4lpo</link>
      <guid>https://dev.to/ricco020/ufw-and-wireguard-the-tunnel-is-up-and-nothing-goes-through-4lpo</guid>
      <description>&lt;p&gt;The tunnel comes up. &lt;code&gt;wg show&lt;/code&gt; prints a recent handshake. The client has its address inside the tunnel. And not a single byte reaches the internet.&lt;/p&gt;

&lt;p&gt;Almost every guide answers this with "open UDP 51820 in the firewall". You already did that — it is why the handshake works at all. The problem is somewhere else, and UFW makes the distinction easy to miss:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Entering a machine and traversing it are two different permissions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ufw allow 51820/udp&lt;/code&gt; lets packets &lt;em&gt;arrive&lt;/em&gt; at the server. Your clients' traffic does not stop there — it goes &lt;em&gt;through&lt;/em&gt; the box and out the public interface. That path lives in the FORWARD chain, which UFW denies by default and which no &lt;code&gt;allow&lt;/code&gt; rule touches.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four things to check, in order
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. IP forwarding — and the file that overwrites the other file
&lt;/h3&gt;

&lt;p&gt;This is the one that costs hours, because the setting &lt;em&gt;looks&lt;/em&gt; done.&lt;/p&gt;

&lt;p&gt;UFW loads &lt;strong&gt;its own sysctl file&lt;/strong&gt; at startup, and it takes precedence over the system one. A value you carefully set in &lt;code&gt;/etc/sysctl.conf&lt;/code&gt; can be silently overwritten on the next &lt;code&gt;ufw enable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The right place is &lt;code&gt;/etc/ufw/sysctl.conf&lt;/code&gt;:&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;net&lt;/span&gt;/&lt;span class="n"&gt;ipv4&lt;/span&gt;/&lt;span class="n"&gt;ip_forward&lt;/span&gt;=&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;net&lt;/span&gt;/&lt;span class="n"&gt;ipv6&lt;/span&gt;/&lt;span class="n"&gt;conf&lt;/span&gt;/&lt;span class="n"&gt;default&lt;/span&gt;/&lt;span class="n"&gt;forwarding&lt;/span&gt;=&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;net&lt;/span&gt;/&lt;span class="n"&gt;ipv6&lt;/span&gt;/&lt;span class="n"&gt;conf&lt;/span&gt;/&lt;span class="n"&gt;all&lt;/span&gt;/&lt;span class="n"&gt;forwarding&lt;/span&gt;=&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check the &lt;strong&gt;effective&lt;/strong&gt; value, not the file you just edited:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sysctl net.ipv4.ip_forward
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Forwarding, which is not the same as ingress
&lt;/h3&gt;

&lt;p&gt;Targeted, and the one to prefer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw route allow &lt;span class="k"&gt;in &lt;/span&gt;on wg0 out on eth0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or globally, in &lt;code&gt;/etc/default/ufw&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="nv"&gt;DEFAULT_FORWARD_POLICY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ACCEPT"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second opens forwarding for &lt;em&gt;every&lt;/em&gt; interface. It is a good ten-second diagnostic and a poor permanent configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. NAT, which UFW never adds on its own
&lt;/h3&gt;

&lt;p&gt;Without it, packets leave carrying their tunnel address, which nothing on the internet knows how to answer. In &lt;code&gt;/etc/ufw/before.rules&lt;/code&gt;, &lt;strong&gt;at the very top&lt;/strong&gt;, before the &lt;code&gt;*filter&lt;/code&gt; line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
COMMIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two classic mistakes here: putting this block &lt;em&gt;after&lt;/em&gt; &lt;code&gt;*filter&lt;/code&gt; (it is then ignored), and copying &lt;code&gt;eth0&lt;/code&gt; without checking.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The real name of the egress interface
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;eth0&lt;/code&gt; is an article convention, not a fact. Depending on the host it will be &lt;code&gt;ens3&lt;/code&gt;, &lt;code&gt;enp1s0&lt;/code&gt;, something else. The command that answers without guessing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip route get 1.1.1.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The test that separates the two families of causes
&lt;/h2&gt;

&lt;p&gt;From a connected client, try to reach the &lt;strong&gt;server's address inside the tunnel&lt;/strong&gt; (often &lt;code&gt;10.8.0.1&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you observe&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;tunnel address answers, nothing external does&lt;/td&gt;
&lt;td&gt;tunnel is fine — forwarding or NAT missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tunnel address does not answer either&lt;/td&gt;
&lt;td&gt;upstream: keys, &lt;code&gt;AllowedIPs&lt;/code&gt;, client routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;everything answers but names do not resolve&lt;/td&gt;
&lt;td&gt;not UFW at all — it is the DNS pushed to the client&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That third row is worth knowing. "I have no internet" very often means "no name resolves". Pinging an IP directly tells you in one command.&lt;/p&gt;

&lt;h2&gt;
  
  
  After applying
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;before.rules&lt;/code&gt; is only re-read on reload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw disable &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable
sudo &lt;/span&gt;ufw status verbose
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-t&lt;/span&gt; nat &lt;span class="nt"&gt;-L&lt;/span&gt; POSTROUTING &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One last thing that produces duplicate configurations: if your &lt;code&gt;PostUp&lt;/code&gt;/&lt;code&gt;PostDown&lt;/code&gt; rules in the WireGuard config already add &lt;code&gt;MASQUERADE&lt;/code&gt;, you do not need the &lt;code&gt;*nat&lt;/code&gt; block above. Both together rarely break anything, but they make the &lt;em&gt;next&lt;/em&gt; outage much harder to read — and there will be a next one.&lt;/p&gt;




&lt;p&gt;If you are still choosing a port or wondering whether 443/UDP is worth it, I wrote a longer piece on &lt;a href="https://www.vpnsmith.com/en/blog/wireguard-port" rel="noopener noreferrer"&gt;WireGuard ports, changing them and opening them properly&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One line to keep: &lt;strong&gt;an open port means packets can enter, not that they can traverse.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>wireguard</category>
      <category>linux</category>
      <category>networking</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your drive says SMART PASSED and is already failing: the 5 attributes that actually predict it</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Sat, 15 Aug 2026 15:52:24 +0000</pubDate>
      <link>https://dev.to/ricco020/your-drive-says-smart-passed-and-is-already-failing-the-5-attributes-that-actually-predict-it-6g</link>
      <guid>https://dev.to/ricco020/your-drive-says-smart-passed-and-is-already-failing-the-5-attributes-that-actually-predict-it-6g</guid>
      <description>&lt;p&gt;A drive that SMART calls &lt;strong&gt;PASSED&lt;/strong&gt; can already be losing your data. The overall&lt;br&gt;
self-assessment is a manufacturer threshold, not a statement about your files — and it stays&lt;br&gt;
green well past the point where sectors have started going unreadable.&lt;/p&gt;

&lt;p&gt;Here is the case that matters, from a real report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SMART overall verdict : PASSED

Reallocated_Sector_Ct  = 24   sectors already remapped
Current_Pending_Sector = 8    sectors unreadable RIGHT NOW
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That drive passes. It is also actively failing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five attributes worth reading
&lt;/h2&gt;

&lt;p&gt;Forget the table of forty values. Five of them describe the two families that precede&lt;br&gt;
unreadable data — reallocation and pending sectors:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;attribute&lt;/th&gt;
&lt;th&gt;meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Reallocated_Sector_Ct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sectors already remapped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;197&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Current_Pending_Sector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;unreadable now, waiting to be remapped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;198&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Offline_Uncorrectable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the drive could not read them offline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;187&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Reported_Uncorrect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;errors it could not correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;188&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Command_Timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;commands that timed out&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A non-zero &lt;strong&gt;197&lt;/strong&gt; on a drive reporting PASSED is the exact situation to act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read it in one command
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;smartctl &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-j&lt;/span&gt; /dev/sda | python3 smart_triage.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script is MIT, has no dependencies and no network access — it reads stdin and prints a&lt;br&gt;
verdict, with exit codes &lt;code&gt;0&lt;/code&gt; healthy, &lt;code&gt;1&lt;/code&gt; monitor, &lt;code&gt;2&lt;/code&gt; back up now, &lt;code&gt;3&lt;/code&gt; stop and image:&lt;br&gt;
&lt;a href="https://gitlab.com/ler.eric/smart-triage" rel="noopener noreferrer"&gt;https://gitlab.com/ler.eric/smart-triage&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The order of operations, which decides whether you keep the data
&lt;/h2&gt;

&lt;p&gt;If the verdict is &lt;strong&gt;stop and image&lt;/strong&gt;, the sequence is not negotiable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stop using the drive.&lt;/strong&gt; Every hour of use turns more pending sectors into lost ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image the whole device to a file&lt;/strong&gt; with &lt;code&gt;ddrescue&lt;/code&gt; — not a file copy, a block-level image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Work on the copy.&lt;/strong&gt; Never on the original.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The mistake that destroys recoverable data is running a filesystem repair tool first. &lt;code&gt;fsck&lt;/code&gt;&lt;br&gt;
and &lt;code&gt;chkdsk&lt;/code&gt; &lt;strong&gt;write&lt;/strong&gt; to the disk. Writing to a drive with pending sectors is how a&lt;br&gt;
recoverable image becomes an unrecoverable one — and it is irreversible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SMART does not tell you
&lt;/h2&gt;

&lt;p&gt;SMART predicts a meaningful share of failures, not all of them. Controller failures, firmware&lt;br&gt;
faults and head crashes can arrive with every attribute still green. A clean report is a reason&lt;br&gt;
to keep monitoring, never a reason to skip backups.&lt;/p&gt;

&lt;p&gt;If you want the detail of what is realistically recoverable per failure mode — mechanical,&lt;br&gt;
electronic, logical, firmware — this breakdown covers it:&lt;br&gt;
&lt;a href="https://www.save-my-disk.com/en/blog/hard-drive-failure" rel="noopener noreferrer"&gt;https://www.save-my-disk.com/en/blog/hard-drive-failure&lt;/a&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>sysadmin</category>
      <category>hardware</category>
      <category>devops</category>
    </item>
    <item>
      <title>A failed deployment is invisible from the web</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Fri, 14 Aug 2026 06:27:43 +0000</pubDate>
      <link>https://dev.to/ricco020/a-failed-deployment-is-invisible-from-the-web-3kc9</link>
      <guid>https://dev.to/ricco020/a-failed-deployment-is-invisible-from-the-web-3kc9</guid>
      <description>&lt;p&gt;Last night a deployment of mine failed for 21 hours and every monitoring check stayed green. The site answered &lt;code&gt;200&lt;/code&gt; the whole time. Here is why that happens, and the two-line check that catches it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode
&lt;/h2&gt;

&lt;p&gt;When a build fails on Vercel (and on Railway, and on most platforms with atomic deploys), the platform does the sensible thing: it keeps serving the &lt;strong&gt;previous&lt;/strong&gt; successful build. Your users see a working site. Your uptime monitor sees &lt;code&gt;200 OK&lt;/code&gt;. Your health check passes.&lt;/p&gt;

&lt;p&gt;What nobody sees is that &lt;strong&gt;the commit you pushed is not the code being served&lt;/strong&gt;. Every article, fix and config change since the last green build is sitting in git, live nowhere.&lt;/p&gt;

&lt;p&gt;That is a good default — far better than serving a broken build. But it means "the site is up" and "my work is deployed" are two different questions, and most monitoring only answers the first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check that actually answers it
&lt;/h2&gt;

&lt;p&gt;Compare the timestamp of your last commit with the timestamp of the last &lt;strong&gt;successful&lt;/strong&gt; deployment. If the commit is newer by more than a build's duration, something is wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;%ct                 &lt;span class="c"&gt;# last commit, unix time&lt;/span&gt;
npx vercel &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;--prod&lt;/span&gt;                    &lt;span class="c"&gt;# last deployments + their state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the age of the newest &lt;code&gt;Ready&lt;/code&gt; deployment is older than your last commit, you are serving stale code. In my case the newest &lt;code&gt;Ready&lt;/code&gt; was 21 hours old, and two &lt;code&gt;Error&lt;/code&gt; deployments sat above it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap that cost me an hour
&lt;/h2&gt;

&lt;p&gt;Here is the part worth the read. Outside a TTY, &lt;code&gt;vercel ls&lt;/code&gt; &lt;strong&gt;splits its output across two streams&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;stdout&lt;/strong&gt; gets the deployment URLs, one per line — no status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;stderr&lt;/strong&gt; gets the formatted table, including the &lt;code&gt;● Ready&lt;/code&gt; / &lt;code&gt;● Error&lt;/code&gt; column&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So this looks correct and silently returns nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vercel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--prod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# always empty
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your script reports zero errors on a project that is entirely broken. The fix is to read both streams:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&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 general lesson is worth more than the specific fix: &lt;strong&gt;an empty result and a clean result are not the same thing&lt;/strong&gt;. If a check can return "nothing found" both when everything is fine and when the parsing broke, it will eventually lie to you — and it will lie in the reassuring direction. Make the two cases distinguishable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NOT VERIFIED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;     &lt;span class="c1"&gt;# not "0 errors"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What actually broke the build
&lt;/h2&gt;

&lt;p&gt;For the curious: a TypeScript error on a component prop. One call site out of 21 was missing a required &lt;code&gt;id&lt;/code&gt;. Nothing exotic — but because the previous build kept serving, nothing surfaced it either.&lt;/p&gt;

&lt;p&gt;I found the second occurrence only after the first fix failed, because I had listed the call sites with &lt;code&gt;grep ... | head -8&lt;/code&gt; on a file with 21 of them. &lt;strong&gt;A &lt;code&gt;head&lt;/code&gt; on a search meant to be exhaustive turns "I saw nothing" into "there is nothing".&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things to take away
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Uptime monitoring does not detect failed deployments. Compare commit time to last successful deploy time.&lt;/li&gt;
&lt;li&gt;Check the &lt;strong&gt;state&lt;/strong&gt; of recent deployments, not just the age of the last good one.&lt;/li&gt;
&lt;li&gt;When a check returns nothing, make sure it can tell you &lt;em&gt;why&lt;/em&gt; — no data and no problem must not look identical.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you are wiring up this kind of monitoring, the same reasoning applies to the telemetry you collect about it — &lt;a href="https://coldwa.st/e/blog/2026-08-14-what-is-opentelemetry.html" rel="noopener noreferrer"&gt;what OpenTelemetry actually is, and what it deliberately does not give you&lt;/a&gt; covers the difference between producing telemetry and having observability, which is the trap one layer up.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>monitoring</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
