<?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: Gorilla Vibe</title>
    <description>The latest articles on DEV Community by Gorilla Vibe (@rokya_elbarbary_8e9bbb0da).</description>
    <link>https://dev.to/rokya_elbarbary_8e9bbb0da</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%2F4149480%2Fc3060700-6405-4702-a875-22a1ed2c3d4c.png</url>
      <title>DEV Community: Gorilla Vibe</title>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rokya_elbarbary_8e9bbb0da"/>
    <language>en</language>
    <item>
      <title>An Internal Link Audit with a Crawl Export and pandas</title>
      <dc:creator>Gorilla Vibe</dc:creator>
      <pubDate>Mon, 19 Oct 2026 10:59:17 +0000</pubDate>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da/an-internal-link-audit-with-a-crawl-export-and-pandas-329f</link>
      <guid>https://dev.to/rokya_elbarbary_8e9bbb0da/an-internal-link-audit-with-a-crawl-export-and-pandas-329f</guid>
      <description>&lt;p&gt;Internal links decide how authority and crawlers move through a site, and most teams only look at them when something is already broken. A crawl export plus a short pandas script is enough to answer three practical questions: which pages have almost no internal links, which links point at redirects or errors, and which anchor texts say nothing useful.&lt;/p&gt;

&lt;p&gt;This walkthrough uses an "all inlinks" export from a desktop crawler (Screaming Frog calls it &lt;em&gt;Bulk Export → Links → All Inlinks&lt;/em&gt;). The column names below match that export (&lt;code&gt;Source&lt;/code&gt;, &lt;code&gt;Destination&lt;/code&gt;, &lt;code&gt;Anchor&lt;/code&gt;, &lt;code&gt;Status Code&lt;/code&gt;); rename them if your tool differs. You also need a list of URLs that &lt;em&gt;should&lt;/em&gt; be indexable, for example from your XML sitemap.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;links&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all_inlinks.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;links&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;links&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;fillna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hyperlink&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hyperlink&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;   &lt;span class="c1"&gt;# ignore images, scripts, etc.
&lt;/span&gt;
&lt;span class="c1"&gt;# 1) Unique internal links pointing at each destination (ignore repeats from the same source)
&lt;/span&gt;&lt;span class="n"&gt;inlinks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop_duplicates&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Source&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;Destination&lt;/span&gt;&lt;span class="sh"&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;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Destination&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unique_inlinks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# 2) Compare with the pages that should be discoverable
&lt;/span&gt;&lt;span class="n"&gt;sitemap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sitemap_urls.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="o"&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;Destination&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;audit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sitemap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inlinks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Destination&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;how&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;left&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fillna&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unique_inlinks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;orphans&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unique_inlinks&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;weak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unique_inlinks&lt;/span&gt;&lt;span class="sh"&gt;"&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="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unique_inlinks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;In sitemap but no internal links:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;orphans&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;In sitemap with 1-2 internal links:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;weak&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Links that waste a hop or hit an error
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Status Code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isin&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="mi"&gt;302&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;307&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;308&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="mi"&gt;410&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;])]&lt;/span&gt;
&lt;span class="n"&gt;fix_first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Destination&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;Status Code&lt;/span&gt;&lt;span class="sh"&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;size&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;links_pointing_here&lt;/span&gt;&lt;span class="sh"&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;reset_index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;links_pointing_here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ascending&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fix_first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The destinations at the top of this list are the cheapest wins: updating one template or navigation block can remove hundreds of bad links at once. For redirects, change the internal link to the final URL rather than relying on the redirect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anchor text that carries no meaning
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;generic&lt;/span&gt; &lt;span class="o"&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;click here&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;read more&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;learn more&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;here&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;more&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;this page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;anchors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Anchor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;fillna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;share&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anchors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generic&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Generic anchors: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;share&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;top_generic_targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;anchors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generic&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
                       &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Destination&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                       &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ascending&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top_generic_targets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A high share of generic anchors is not an emergency, but for important pages a short descriptive anchor tells both readers and crawlers what the destination is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the result
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Orphans&lt;/strong&gt; need either a real place in the navigation or a decision to retire them. Do not add links just to hit a number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pages with one or two links&lt;/strong&gt; that matter commercially are candidates for contextual links from related content.&lt;/li&gt;
&lt;li&gt;Re-run the script after each release and keep the numbers. A trend tells you more than a single snapshot.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;p&gt;A crawl only sees links present in the rendered HTML it fetched. Links added by JavaScript after load, or behind logins, may be missing unless the crawler renders JavaScript. Internal link counts are also not rankings; they are one input among many.&lt;/p&gt;

&lt;p&gt;If you would like this kind of audit turned into a prioritised plan for your site, it is part of our &lt;a href="https://thegorillavibe.com/service/seo" rel="noopener noreferrer"&gt;SEO service&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Search Central: &lt;a href="https://developers.google.com/search/docs/crawling-indexing/links-crawlable" rel="noopener noreferrer"&gt;Link best practices for Google&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;pandas documentation: &lt;a href="https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.merge.html" rel="noopener noreferrer"&gt;&lt;code&gt;DataFrame.merge&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Screaming Frog: &lt;a href="https://www.screamingfrog.co.uk/seo-spider/user-guide/" rel="noopener noreferrer"&gt;SEO Spider user guide&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>seo</category>
      <category>python</category>
      <category>pandas</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Faceted Navigation and SEO: When to Index, Canonicalise or Block</title>
      <dc:creator>Gorilla Vibe</dc:creator>
      <pubDate>Mon, 19 Oct 2026 10:58:53 +0000</pubDate>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da/faceted-navigation-and-seo-when-to-index-canonicalise-or-block-202d</link>
      <guid>https://dev.to/rokya_elbarbary_8e9bbb0da/faceted-navigation-and-seo-when-to-index-canonicalise-or-block-202d</guid>
      <description>&lt;p&gt;Filters and sort options on a category page can multiply a few hundred products into millions of crawlable URLs. Most of those URLs are near-duplicates, and letting a crawler wander through all of them wastes requests and dilutes signals. The standard advice sounds simple ("block them", "canonicalise them"), but the three tools do different jobs, and using the wrong one gives a result you did not intend.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each tool actually does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;robots.txt&lt;/code&gt; disallow&lt;/strong&gt; stops compliant crawlers from &lt;em&gt;fetching&lt;/em&gt; a URL. It does not remove the URL from the index if other pages link to it, and Google cannot see a &lt;code&gt;noindex&lt;/code&gt; or a canonical on a page it is not allowed to fetch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;rel="canonical"&lt;/code&gt;&lt;/strong&gt; is a hint that a page is a duplicate of another URL. Google may follow it or may not, and the duplicate still has to be crawled for the hint to be read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;noindex&lt;/code&gt;&lt;/strong&gt; (meta robots or &lt;code&gt;X-Robots-Tag&lt;/code&gt;) keeps a crawlable page out of the index. The page still gets crawled, so it does not save crawl requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A decision table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Facet type&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Index?&lt;/th&gt;
&lt;th&gt;Treatment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Has real search demand and a unique page purpose&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/shoes/running/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Clean URL, unique title and copy, self-referencing canonical, in the sitemap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Combination with demand but fragile&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/shoes/running/red/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maybe&lt;/td&gt;
&lt;td&gt;Only if the page has distinct content and stock; otherwise canonical to the parent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sort order&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?sort=price_asc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Canonical to the unsorted URL; keep out of internal links&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pagination&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?page=2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes, as its own page&lt;/td&gt;
&lt;td&gt;Self-referencing canonical; do not canonicalise page 2 to page 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stackable filters (3+ at once)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?color=red&amp;amp;size=9&amp;amp;brand=x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Disallow in &lt;code&gt;robots.txt&lt;/code&gt; if crawling them is the problem; make the links non-crawlable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal search results&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/search?q=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;noindex&lt;/code&gt; and disallow, depending on how they are linked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session or tracking parameters&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;?sessionid=&lt;/code&gt;, &lt;code&gt;?utm_&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Do not generate them in internal links; canonical to the clean URL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Order of operations
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decide which facets deserve to exist as pages.&lt;/strong&gt; This is a product and SEO decision, not a technical one. A facet earns a URL only if people search for it and you can make the page useful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop generating crawlable links to everything else.&lt;/strong&gt; Filters implemented as buttons or form posts, not plain &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; links with parameters, are the cleanest fix because the URLs are never discovered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add canonicals&lt;/strong&gt; on the variants that will still exist as URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;robots.txt&lt;/code&gt; last&lt;/strong&gt;, and only for patterns that are already not worth crawling. Do not disallow a URL you also want de-indexed through &lt;code&gt;noindex&lt;/code&gt;, because the crawler will never see the tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure.&lt;/strong&gt; Compare crawled URLs in your logs and the pages in the Search Console indexing report before and after.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A quick way to see how big the problem is: count the distinct parameter combinations in a crawl or log export. If that number is many times larger than your real catalogue, the site is producing crawl space you did not design.&lt;/p&gt;

&lt;p&gt;Rebuilding filters and templates so the crawlable set matches the pages you actually want indexed is work our team handles under &lt;a href="https://thegorillavibe.com/service/web-platform-development" rel="noopener noreferrer"&gt;web platform development&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Search Central: &lt;a href="https://developers.google.com/search/docs/crawling-indexing/crawling-managing-faceted-navigation" rel="noopener noreferrer"&gt;Managing crawling of faceted navigation URLs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Search Central: &lt;a href="https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls" rel="noopener noreferrer"&gt;Consolidate duplicate URLs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Search Central: &lt;a href="https://developers.google.com/search/docs/crawling-indexing/robots/intro" rel="noopener noreferrer"&gt;Introduction to robots.txt&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>ecommerce</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Checking Googlebot in Your Server Logs with grep and awk</title>
      <dc:creator>Gorilla Vibe</dc:creator>
      <pubDate>Mon, 19 Oct 2026 10:58:22 +0000</pubDate>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da/checking-googlebot-in-your-server-logs-with-grep-and-awk-12i9</link>
      <guid>https://dev.to/rokya_elbarbary_8e9bbb0da/checking-googlebot-in-your-server-logs-with-grep-and-awk-12i9</guid>
      <description>&lt;p&gt;Server logs are the only place where you can see what Googlebot actually requested, how often, and what it got back. Crawl reports from a tool show what a crawler &lt;em&gt;can&lt;/em&gt; reach; logs show what Google &lt;em&gt;did&lt;/em&gt; reach. You do not need a log-analysis product for a first pass. A few &lt;code&gt;grep&lt;/code&gt; and &lt;code&gt;awk&lt;/code&gt; commands on a standard access log answer the questions that matter: is Googlebot wasting requests on junk URLs, and is it hitting errors?&lt;/p&gt;

&lt;p&gt;The commands below assume the common "combined" log format, where field 7 is the request path and field 9 is the status code. Check one line of your own log first, because CDNs and some servers reorder fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Only count real Googlebot traffic
&lt;/h2&gt;

&lt;p&gt;The user-agent string can be faked by anyone, so a &lt;code&gt;grep -i googlebot&lt;/code&gt; on its own over-counts. Google documents a two-step check: reverse DNS on the IP must resolve to a &lt;code&gt;googlebot.com&lt;/code&gt; or &lt;code&gt;google.com&lt;/code&gt; host, and the forward lookup of that hostname must return the same IP.&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;# take the IPs that claim to be Googlebot, then verify each unique one&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'googlebot'&lt;/span&gt; access.log | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; claimed_ips.txt

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;ip&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;host_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;host &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ip&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;'/pointer/ {print $5}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host_name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;.googlebot.com.|&lt;span class="k"&gt;*&lt;/span&gt;.google.com.&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nv"&gt;fwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;host &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host_name&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="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/has address/ {print $4}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$fwd&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ip&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="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ip&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;esac&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; claimed_ips.txt &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; verified_ips.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then keep only the lines that start with a verified IP:&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;grep&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; verified_ips.txt access.log &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; googlebot.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. What status codes is Googlebot seeing?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $9}'&lt;/span&gt; googlebot.log | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A healthy site is mostly &lt;code&gt;200&lt;/code&gt; with some &lt;code&gt;301&lt;/code&gt; and &lt;code&gt;304&lt;/code&gt;. A visible share of &lt;code&gt;404&lt;/code&gt;, &lt;code&gt;410&lt;/code&gt; or &lt;code&gt;5xx&lt;/code&gt; is the first thing to chase. A &lt;code&gt;5xx&lt;/code&gt; that appears only a fraction of the time is easy to miss in manual checks, so look at the proportion rather than whether it ever happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Where is the crawl budget going?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# the 20 most requested paths&lt;/span&gt;
&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $7}'&lt;/span&gt; googlebot.log | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;

&lt;span class="c"&gt;# how many requests carry a query string&lt;/span&gt;
&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $7}'&lt;/span&gt; googlebot.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'?'&lt;/span&gt;

&lt;span class="c"&gt;# the same split by top-level folder&lt;/span&gt;
&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $7}'&lt;/span&gt; googlebot.log | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;/ &lt;span class="nt"&gt;-f2&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If parameter URLs (&lt;code&gt;?sort=&lt;/code&gt;, &lt;code&gt;?color=&lt;/code&gt;, session or tracking parameters) make up a large share of requests while your important pages are crawled rarely, that is the signal to tighten internal links, canonicals and robots rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Which broken URLs are still being requested?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'$9 ~ /^(404|410|500|502|503)$/ {print $9, $7}'&lt;/span&gt; googlebot.log | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paths that keep appearing here are usually either old URLs without a redirect or internal links that still point at a dead page. Fix the link, or add a permanent &lt;code&gt;301&lt;/code&gt; to the closest live equivalent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Logs only cover requests that reached your origin. If a CDN answers from cache, those hits will be missing unless you export the CDN logs.&lt;/li&gt;
&lt;li&gt;A short sample misleads. Use at least a few weeks of data, and compare before and after a change rather than judging one day.&lt;/li&gt;
&lt;li&gt;Logs tell you what was requested, not what was indexed. Pair them with Search Console's page indexing report.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want help turning crawl and log findings into a prioritised fix list, that is part of our &lt;a href="https://thegorillavibe.com/service/seo" rel="noopener noreferrer"&gt;SEO service&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Search Central: &lt;a href="https://developers.google.com/search/docs/crawling-indexing/verifying-googlebot" rel="noopener noreferrer"&gt;Verifying Googlebot and other Google crawlers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Search Central: &lt;a href="https://developers.google.com/search/docs/crawling-indexing/large-site-managing-crawl-budget" rel="noopener noreferrer"&gt;Crawl budget management for large sites&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apache HTTP Server docs: &lt;a href="https://httpd.apache.org/docs/current/logs.html" rel="noopener noreferrer"&gt;Log files&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>bash</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Server-side lead events with the GA4 Measurement Protocol (and the pitfalls)</title>
      <dc:creator>Gorilla Vibe</dc:creator>
      <pubDate>Sun, 18 Oct 2026 13:09:05 +0000</pubDate>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da/server-side-lead-events-with-the-ga4-measurement-protocol-and-the-pitfalls-2cog</link>
      <guid>https://dev.to/rokya_elbarbary_8e9bbb0da/server-side-lead-events-with-the-ga4-measurement-protocol-and-the-pitfalls-2cog</guid>
      <description>&lt;p&gt;Browser tags miss leads. Ad blockers, consent banners, people closing the tab right after submitting, and thank-you pages that never load all mean that the &lt;code&gt;generate_lead&lt;/code&gt; event in GA4 is often lower than the number of rows in your CRM. If you optimise paid campaigns on that number, you are optimising on a sample with a bias you cannot see.&lt;/p&gt;

&lt;p&gt;A small, honest fix is to send the lead event &lt;strong&gt;from your server&lt;/strong&gt;, at the moment the form submission is actually stored, using the GA4 Measurement Protocol. This post shows a minimal version and the pitfalls that make people give up on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A GA4 web data stream, its &lt;strong&gt;Measurement ID&lt;/strong&gt; (&lt;code&gt;G-XXXXXXX&lt;/code&gt;) and a &lt;strong&gt;Measurement Protocol API secret&lt;/strong&gt; (Admin → Data streams → your stream → Measurement Protocol API secrets).&lt;/li&gt;
&lt;li&gt;The visitor's GA &lt;strong&gt;client ID&lt;/strong&gt;, captured in the browser and sent along with the form. Without it, GA4 cannot join the server event to the session that produced it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Capture the client ID in a hidden field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"ga_client_id"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"ga_client_id"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;gtag&lt;/span&gt;&lt;span class="p"&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="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;G-XXXXXXX&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;client_id&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="nx"&gt;id&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ga_client_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Send the event when the lead is saved
&lt;/h2&gt;

&lt;p&gt;Node.js 18+ (built-in &lt;code&gt;fetch&lt;/code&gt;), called right after your form handler writes the lead to the database:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MP_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.google-analytics.com/mp/collect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendLeadEvent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leadId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;formName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&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;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;clientId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;skipped&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no client id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// never invent one&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;MP_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?measurement_id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&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;GA4_MEASUREMENT_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
              &lt;span class="s2"&gt;`&amp;amp;api_secret=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&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;GA4_API_SECRET&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;generate_lead&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;form_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;lead_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;leadId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// lets you de-duplicate and reconcile with the CRM&lt;/span&gt;
        &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;engagement_time_msec&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="c1"&gt;// makes the event count toward active users&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;res&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;fetch&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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="k"&gt;return&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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;Keep the API secret in an environment variable on the server, never in front-end code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate before you trust it
&lt;/h2&gt;

&lt;p&gt;The production endpoint returns &lt;code&gt;204&lt;/code&gt; even for malformed payloads, so a "successful" request proves nothing. During development, post the same body to the &lt;strong&gt;debug endpoint&lt;/strong&gt;, which returns a list of validation messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://www.google-analytics.com/debug/mp/collect?measurement_id=...&amp;amp;api_secret=...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An empty &lt;code&gt;validationMessages&lt;/code&gt; array is what you want. After that, confirm in GA4 DebugView, and then in the standard reports a day later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Double counting.&lt;/strong&gt; If the browser tag also fires &lt;code&gt;generate_lead&lt;/code&gt;, you will count most leads twice. Pick one source for the event. A clean split: the browser keeps &lt;code&gt;form_start&lt;/code&gt; and &lt;code&gt;form_submit&lt;/code&gt;, and only the server sends &lt;code&gt;generate_lead&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consent.&lt;/strong&gt; Server-side does not mean consent-free. If a visitor declined analytics, do not send the event. Store the consent state with the lead and check it before calling &lt;code&gt;sendLeadEvent&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timestamps.&lt;/strong&gt; If you queue events and send them later, set &lt;code&gt;timestamp_micros&lt;/code&gt;. GA4 accepts events up to 72 hours in the past.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spam.&lt;/strong&gt; Send the event after your spam filter, not before. Otherwise the bot traffic you just blocked becomes "conversions".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The payoff
&lt;/h2&gt;

&lt;p&gt;Once the event comes from the same place as the CRM row, the two numbers finally reconcile, apart from consent-declined leads, which you can count separately. That is the number worth importing into Google Ads or feeding to bidding. It is a small piece of the measurement work we do in &lt;a href="https://thegorillavibe.com/service/performance-marketing" rel="noopener noreferrer"&gt;performance marketing&lt;/a&gt;: fix what is counted before spending more on what is counted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google for Developers: &lt;a href="https://developers.google.com/analytics/devguides/collection/protocol/ga4" rel="noopener noreferrer"&gt;Measurement Protocol (GA4)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google for Developers: &lt;a href="https://developers.google.com/analytics/devguides/collection/protocol/ga4/validating-events" rel="noopener noreferrer"&gt;Validating events&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Analytics Help: &lt;a href="https://support.google.com/analytics/answer/9267735" rel="noopener noreferrer"&gt;Recommended events: generate_lead&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>analytics</category>
      <category>javascript</category>
      <category>node</category>
      <category>marketing</category>
    </item>
    <item>
      <title>hreflang for English and Arabic sites: the five mistakes we see most</title>
      <dc:creator>Gorilla Vibe</dc:creator>
      <pubDate>Sun, 18 Oct 2026 13:08:28 +0000</pubDate>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da/hreflang-for-english-and-arabic-sites-the-five-mistakes-we-see-most-1m60</link>
      <guid>https://dev.to/rokya_elbarbary_8e9bbb0da/hreflang-for-english-and-arabic-sites-the-five-mistakes-we-see-most-1m60</guid>
      <description>&lt;p&gt;Bilingual English–Arabic sites are the norm across the GCC, and &lt;code&gt;hreflang&lt;/code&gt; is where most of them quietly go wrong. The tags are simple, but the rules for how they must agree with each other are strict, and search engines ignore a whole cluster if one piece is broken. These are the five mistakes we find most often in audits, with the fix for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Missing return links
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;hreflang&lt;/code&gt; only works when both pages point at each other. If &lt;code&gt;/en/pricing&lt;/code&gt; lists &lt;code&gt;/ar/pricing&lt;/code&gt; as its Arabic alternate, then &lt;code&gt;/ar/pricing&lt;/code&gt; must list &lt;code&gt;/en/pricing&lt;/code&gt; back. One-way annotations are ignored.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- on https://example.com/en/pricing AND on https://example.com/ar/pricing --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/en/pricing"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"ar"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/ar/pricing"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"x-default"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/en/pricing"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The safest pattern is to output the same complete block on every page in the cluster, including a self-reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Invalid or over-specific codes
&lt;/h2&gt;

&lt;p&gt;The value is a language code (ISO 639-1), optionally followed by a region code (ISO 3166-1 Alpha-2): &lt;code&gt;ar&lt;/code&gt;, &lt;code&gt;ar-AE&lt;/code&gt;, &lt;code&gt;en-SA&lt;/code&gt;. We regularly see &lt;code&gt;ar-UAE&lt;/code&gt;, &lt;code&gt;en-UK&lt;/code&gt; and &lt;code&gt;ae&lt;/code&gt;, which are all invalid. (&lt;code&gt;UK&lt;/code&gt; is not the ISO code for the United Kingdom, and &lt;code&gt;ae&lt;/code&gt; is a region, not a language.)&lt;/p&gt;

&lt;p&gt;Only add a region when the content really differs by country, for example in prices, delivery or legal text. A single Arabic version for the whole GCC should just be &lt;code&gt;ar&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Pointing at URLs that redirect, 404 or are noindex
&lt;/h2&gt;

&lt;p&gt;Every URL in the cluster must return &lt;code&gt;200&lt;/code&gt;, be indexable and be its own canonical. Annotations that point at a redirect, a &lt;code&gt;404&lt;/code&gt;, a &lt;code&gt;noindex&lt;/code&gt; page or a page whose canonical points somewhere else break the set. This happens most after a URL restructure, when the language switcher is updated but the &lt;code&gt;hreflang&lt;/code&gt; generator still emits old paths.&lt;/p&gt;

&lt;p&gt;A quick check: collect every &lt;code&gt;href&lt;/code&gt; from your &lt;code&gt;hreflang&lt;/code&gt; tags, request each one, and fail if any response is not a plain &lt;code&gt;200&lt;/code&gt; without &lt;code&gt;noindex&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Canonical tags that fight hreflang
&lt;/h2&gt;

&lt;p&gt;A common CMS default canonicalises the Arabic page to the English one ("the English page is the original"). That tells search engines the Arabic page is a duplicate, and they never show it. Each language version needs a &lt;strong&gt;self-referencing canonical&lt;/strong&gt;; &lt;code&gt;hreflang&lt;/code&gt; then connects the versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Translating the slug but not the internal links
&lt;/h2&gt;

&lt;p&gt;If the Arabic page lives at &lt;code&gt;/ar/الأسعار&lt;/code&gt; but internal links and the sitemap still point at &lt;code&gt;/ar/pricing&lt;/code&gt; (which redirects), crawlers keep landing on redirects. Either keep Latin slugs in both languages or make sure every internal link, sitemap entry and &lt;code&gt;hreflang&lt;/code&gt; tag uses the final Arabic URL, percent-encoded consistently.&lt;/p&gt;

&lt;h2&gt;
  
  
  A sitemap alternative
&lt;/h2&gt;

&lt;p&gt;For large sites, maintaining &lt;code&gt;hreflang&lt;/code&gt; in the XML sitemap is easier to audit than in page heads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;url&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;loc&amp;gt;&lt;/span&gt;https://example.com/en/pricing&lt;span class="nt"&gt;&amp;lt;/loc&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;xhtml:link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/en/pricing"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;xhtml:link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"ar"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/ar/pricing"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;xhtml:link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"x-default"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/en/pricing"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/url&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use one method, not both. Two sources that disagree is mistake number one again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every page outputs the full cluster, including itself&lt;/li&gt;
&lt;li&gt;[ ] Codes are valid (&lt;code&gt;ar&lt;/code&gt;, &lt;code&gt;en&lt;/code&gt;, optional &lt;code&gt;-AE&lt;/code&gt;/&lt;code&gt;-SA&lt;/code&gt;), plus &lt;code&gt;x-default&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Every alternate URL returns &lt;code&gt;200&lt;/code&gt;, is indexable and is self-canonical&lt;/li&gt;
&lt;li&gt;[ ] Language switcher, sitemap and &lt;code&gt;hreflang&lt;/code&gt; use the same final URLs&lt;/li&gt;
&lt;li&gt;[ ] Re-check after every URL migration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Getting this right is part of the technical groundwork we do for bilingual brands; see our &lt;a href="https://thegorillavibe.com/service/seo" rel="noopener noreferrer"&gt;SEO service&lt;/a&gt; for how we approach it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Search Central: &lt;a href="https://developers.google.com/search/docs/specialty/international/localized-versions" rel="noopener noreferrer"&gt;Tell Google about localized versions of your page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Search Central: &lt;a href="https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls" rel="noopener noreferrer"&gt;Consolidate duplicate URLs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;W3C: &lt;a href="https://www.w3.org/International/articles/language-tags/" rel="noopener noreferrer"&gt;Language tags in HTML and XML&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>i18n</category>
      <category>html</category>
    </item>
    <item>
      <title>Catching flaky 404s before Google does: a 40-line URL stability check</title>
      <dc:creator>Gorilla Vibe</dc:creator>
      <pubDate>Sun, 18 Oct 2026 13:07:34 +0000</pubDate>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da/catching-flaky-404s-before-google-does-a-40-line-url-stability-check-5gc7</link>
      <guid>https://dev.to/rokya_elbarbary_8e9bbb0da/catching-flaky-404s-before-google-does-a-40-line-url-stability-check-5gc7</guid>
      <description>&lt;p&gt;A page that returns &lt;code&gt;200&lt;/code&gt; when you open it in your browser can still be failing for search engines. We hit this on a Next.js site: the service pages loaded fine on every manual check, yet Bing reported them as &lt;code&gt;404&lt;/code&gt; and Google slowly dropped them from the index. The cause was an intermittent failure in the data layer. Roughly three requests out of four came back as &lt;code&gt;404&lt;/code&gt; with a &lt;code&gt;noindex&lt;/code&gt; tag, and the fourth one, the one a human happened to see, was fine.&lt;/p&gt;

&lt;p&gt;One request per URL will never catch that. You need to ask the same question many times and look at the distribution of answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check: request every URL N times
&lt;/h2&gt;

&lt;p&gt;The script below is plain Python 3 with no dependencies. It reads URLs from a file, requests each one &lt;code&gt;N&lt;/code&gt; times with a short pause, and prints how often each status code appeared. It also flags a &lt;code&gt;noindex&lt;/code&gt; in the response, because an error page that carries &lt;code&gt;noindex&lt;/code&gt; is what actually gets a page dropped.&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;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;url_stability.py - request each URL N times and report status-code distribution.
usage: python3 url_stability.py urls.txt [N]&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;

&lt;span class="n"&gt;UA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mozilla/5.0 (stability-check)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&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;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cache-Control&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;no-cache&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200_000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&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;ignore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&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;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;noindex&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;body&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200_000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&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;ignore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;noindex&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;body&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&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;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&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;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;u&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="k"&gt;if&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;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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;2&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
    &lt;span class="n"&gt;unstable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;codes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;noindex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ni&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&lt;/span&gt;&lt;span class="sh"&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;?&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;url&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time_ns&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;codes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;noindex&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;ni&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;codes&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="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BAD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;unstable&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ok  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;codes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  noindex=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;noindex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;unstable&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A run against a healthy site prints one &lt;code&gt;OK 20/20&lt;/code&gt; line per URL. On the site we were debugging it printed lines like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BAD 5/20 ok  {404: 15, 200: 5}  noindex=15  https://example.com/service/seo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single line explained weeks of confusing reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Details that matter
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bust caches on purpose.&lt;/strong&gt; The &lt;code&gt;_=&lt;/code&gt; query parameter forces a fresh response from CDNs and ISR caches. Run the check a second time without it to see what cached visitors and crawlers get. The two results can differ, and the difference tells you which layer is broken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twenty requests is a floor.&lt;/strong&gt; Twenty requests per URL is enough to see a 25% failure rate reliably. For rarer failures, run it on a schedule and keep the output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit codes make it CI-friendly.&lt;/strong&gt; The script exits &lt;code&gt;1&lt;/code&gt; if any URL is unstable, so you can run it after every deploy and fail the pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the bots too.&lt;/strong&gt; Change the &lt;code&gt;User-Agent&lt;/code&gt; to a crawler string to confirm that bots are not treated differently. If only one user agent fails, look at the middleware, the WAF and the bot rules first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Typical causes once you find it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A CMS or API call fails and the page calls &lt;code&gt;notFound()&lt;/code&gt;.&lt;/strong&gt; In Next.js App Router code, &lt;code&gt;if (!data) notFound()&lt;/code&gt; turns every upstream timeout into a real &lt;code&gt;404&lt;/code&gt; with &lt;code&gt;noindex&lt;/code&gt;. Fail loudly instead: return a &lt;code&gt;5xx&lt;/code&gt; (search engines retry those) or serve the last good copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two app versions behind a load balancer.&lt;/strong&gt; One instance knows the new routes and the other does not, so the response depends on which instance answers. Compare response headers, such as the build ID and server timing, between good and bad responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge caching an error.&lt;/strong&gt; A single bad response cached for an hour looks like an outage. Make sure error responses are sent with &lt;code&gt;Cache-Control: no-store&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  After the fix
&lt;/h2&gt;

&lt;p&gt;Re-run the check with &lt;code&gt;N=50&lt;/code&gt; and keep the log. Then resubmit the affected URLs in Search Console and Bing Webmaster Tools, and watch the coverage reports over the following week. For redesigns, add a permanent &lt;code&gt;301&lt;/code&gt; from every old path to its new one. A missing redirect looks exactly like this problem from the crawler's side.&lt;/p&gt;

&lt;p&gt;If you want a second pair of eyes on a platform or migration like this, our team covers it under &lt;a href="https://thegorillavibe.com/service/web-platform-development" rel="noopener noreferrer"&gt;web platform development&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Search Central: &lt;a href="https://developers.google.com/search/docs/crawling-indexing/http-network-errors" rel="noopener noreferrer"&gt;HTTP status codes and how they affect Google Search&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Next.js docs: &lt;a href="https://nextjs.org/docs/app/api-reference/functions/not-found" rel="noopener noreferrer"&gt;&lt;code&gt;notFound&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bing Webmaster Guidelines: &lt;a href="https://www.bing.com/webmasters/help/webmaster-guidelines-30fba23a" rel="noopener noreferrer"&gt;Bing Webmaster Tools help&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>python</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Consultant, Agency or In-House? A Decision Checklist for Small Teams</title>
      <dc:creator>Gorilla Vibe</dc:creator>
      <pubDate>Mon, 12 Oct 2026 12:05:18 +0000</pubDate>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da/consultant-agency-or-in-house-a-decision-checklist-for-small-teams-3nnl</link>
      <guid>https://dev.to/rokya_elbarbary_8e9bbb0da/consultant-agency-or-in-house-a-decision-checklist-for-small-teams-3nnl</guid>
      <description>&lt;p&gt;Small teams hit the same fork sooner or later: do we hire someone, bring in a consultant for a few months, or sign with an agency? Each can be the right answer. They fail when the choice is made on price alone, so here is a checklist you can run in about twenty minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, name the real problem
&lt;/h2&gt;

&lt;p&gt;Write one sentence that starts with "We need…". If it says "more leads" or "better branding", keep going until it describes something a person could actually do, such as "a landing page that converts mobile visitors from paid search" or "a tracking setup we trust".&lt;/p&gt;

&lt;h2&gt;
  
  
  When in-house is the right call
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The work is continuous and needs deep knowledge of your product.&lt;/li&gt;
&lt;li&gt;You can describe the role in a way that attracts senior people, and you can pay for them.&lt;/li&gt;
&lt;li&gt;You have someone who can manage and review the output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In-house is slow to start but compounds. It is a poor fit if the need is a three-month project.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a consultant fits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You have a team but lack one specific skill: a measurement plan, a pricing review, a go-to-market sequence.&lt;/li&gt;
&lt;li&gt;You want a decision, a plan or an audit, not months of execution.&lt;/li&gt;
&lt;li&gt;The deliverable can be reviewed on a date.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ask for a written scope, a named person and a handover document. A good consultant makes themselves less necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  When an agency fits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You need several skills at once (strategy, design, web, ads) and cannot hire all of them.&lt;/li&gt;
&lt;li&gt;Work is ongoing but the volume goes up and down.&lt;/li&gt;
&lt;li&gt;You want predictable monthly capacity rather than recruiting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ask who actually does the work, how many hours a month are included, and how reporting is done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five questions that settle most cases
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Is this a project with an end date, or an ongoing function?&lt;/li&gt;
&lt;li&gt;Do we already have someone who can brief and review the work?&lt;/li&gt;
&lt;li&gt;How many different skills does it need?&lt;/li&gt;
&lt;li&gt;How fast do we need to start?&lt;/li&gt;
&lt;li&gt;What does it cost us if it goes wrong?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If most answers say "ongoing, with a reviewer and one skill", hire. If they say "a decision on a date", use a consultant. If they say "several skills, flexible volume, start soon", an agency is usually cheaper than building the same team.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on switching later
&lt;/h2&gt;

&lt;p&gt;Whatever you choose, keep your accounts, tracking and files in your own name so you can change your mind. That single habit removes most of the risk from any of the three options.&lt;/p&gt;

&lt;p&gt;If you are weighing a consultant for a specific decision, the &lt;a href="https://thegorillavibe.com/consulting-advisory" rel="noopener noreferrer"&gt;consulting and advisory team at Gorilla Vibe&lt;/a&gt; works on exactly that kind of scoped, time-boxed engagement.&lt;/p&gt;

</description>
      <category>css</category>
      <category>design</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Design Tokens: Stop Your Brand Colours Drifting Between Figma and CSS</title>
      <dc:creator>Gorilla Vibe</dc:creator>
      <pubDate>Wed, 30 Sep 2026 07:50:15 +0000</pubDate>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da/design-tokens-stop-your-brand-colours-drifting-between-figma-and-css-16g</link>
      <guid>https://dev.to/rokya_elbarbary_8e9bbb0da/design-tokens-stop-your-brand-colours-drifting-between-figma-and-css-16g</guid>
      <description>&lt;p&gt;A brand guideline PDF says the primary colour is orange. Six months later the website has four slightly different oranges, the email templates have a fifth, and nobody can say which one is right.&lt;/p&gt;

&lt;p&gt;Design tokens fix this by turning brand decisions into named, versioned data that both designers and developers consume. This post shows a small, practical setup you can adopt without a big design-system project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a design token is
&lt;/h2&gt;

&lt;p&gt;A token is a name paired with a design decision:&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;"color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"brand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"orange"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"$value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#F26D25"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&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;"color"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"yellow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"$value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#FDBE37"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&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;"color"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;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;The &lt;code&gt;$value&lt;/code&gt; / &lt;code&gt;$type&lt;/code&gt; shape follows the format being standardised by the W3C Design Tokens Community Group. Tools such as Style Dictionary can read token files and generate CSS, iOS, Android or JavaScript outputs from the same source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use three tiers, not one
&lt;/h2&gt;

&lt;p&gt;Putting raw hex values straight into components recreates the original problem. A three-tier structure keeps changes cheap:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Example name&lt;/th&gt;
&lt;th&gt;Holds&lt;/th&gt;
&lt;th&gt;Who changes it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primitive&lt;/td&gt;
&lt;td&gt;&lt;code&gt;color.brand.orange&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The raw brand palette&lt;/td&gt;
&lt;td&gt;Brand owner, rarely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic&lt;/td&gt;
&lt;td&gt;&lt;code&gt;color.action.primary&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The &lt;em&gt;role&lt;/em&gt; a colour plays&lt;/td&gt;
&lt;td&gt;Design system owner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component&lt;/td&gt;
&lt;td&gt;&lt;code&gt;button.primary.background&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A specific component decision&lt;/td&gt;
&lt;td&gt;Component owner&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Semantic tokens point at primitives, and components point at semantic tokens. When the brand refreshes its orange, you change one primitive. When marketing wants a different call-to-action colour for a campaign, you change one semantic token, and no component code moves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Output as CSS custom properties
&lt;/h2&gt;

&lt;p&gt;The simplest delivery format for the web is CSS custom properties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* primitives */&lt;/span&gt;
  &lt;span class="py"&gt;--color-brand-orange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#F26D25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-brand-yellow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#FDBE37&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-neutral-950&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#141414&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-neutral-50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f5f5f5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c"&gt;/* semantic */&lt;/span&gt;
  &lt;span class="py"&gt;--color-action-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-brand-orange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-neutral-50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-neutral-950&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefers-color-scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;--color-surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-neutral-950&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--color-text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-neutral-50&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="nc"&gt;.button-primary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-action-primary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-neutral-950&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;Dark mode becomes a matter of remapping semantic tokens, not rewriting components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tokens are not only colours
&lt;/h2&gt;

&lt;p&gt;Most brand drift comes from the things that are not colours:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Typography:&lt;/strong&gt; font families, a type scale, line heights, and weights&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spacing:&lt;/strong&gt; a small scale (for example 4, 8, 12, 16, 24, 32, 48) instead of arbitrary pixel values&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Radius and elevation:&lt;/strong&gt; corner radii and shadows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Motion:&lt;/strong&gt; durations and easing curves&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your brand works in more than one script, for example Arabic and English, give each script its own typography tokens. Arabic type usually needs different sizes and line heights to read as comfortably as Latin text at the same visual weight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checks that keep tokens honest
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Contrast checks in CI.&lt;/strong&gt; Generate every text/background semantic pair and fail the build if any combination drops below the WCAG contrast ratio you have committed to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lint for raw values.&lt;/strong&gt; A stylelint rule that rejects hex colours outside the token files stops drift at code review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version the token package.&lt;/strong&gt; Treat token changes like an API: a changelog, semantic versioning, and a visual diff before release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One source of truth.&lt;/strong&gt; Designers and developers must edit the same token file (or a tool that syncs to it), never parallel copies.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where to start this week
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Export the brand palette and type scale into a single JSON token file.&lt;/li&gt;
&lt;li&gt;Add semantic names for the five or six roles you actually use: primary action, surface, text, muted text, border, danger.&lt;/li&gt;
&lt;li&gt;Generate CSS custom properties and replace hard-coded values in one high-traffic template.&lt;/li&gt;
&lt;li&gt;Add the contrast check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is enough to stop the "five oranges" problem, and it scales into a full design system later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;MDN: &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties" rel="noopener noreferrer"&gt;Using CSS custom properties&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MDN: &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme" rel="noopener noreferrer"&gt;prefers-color-scheme&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/design-tokens/community-group" rel="noopener noreferrer"&gt;Design Tokens Community Group (W3C) repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://styledictionary.com/" rel="noopener noreferrer"&gt;Style Dictionary&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Written by the team at Gorilla Vibe. The colours in the examples are our own brand palette. For how brand identity work turns into systems like this, see &lt;a href="https://thegorillavibe.com/identity-branding" rel="noopener noreferrer"&gt;how we approach brand identity&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>figma</category>
      <category>webdev</category>
      <category>design</category>
    </item>
    <item>
      <title>Test Your Redirect Map Before and After a Site Migration With a Small Python Script</title>
      <dc:creator>Gorilla Vibe</dc:creator>
      <pubDate>Tue, 29 Sep 2026 14:23:49 +0000</pubDate>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da/test-your-redirect-map-before-and-after-a-site-migration-with-a-small-python-script-44m9</link>
      <guid>https://dev.to/rokya_elbarbary_8e9bbb0da/test-your-redirect-map-before-and-after-a-site-migration-with-a-small-python-script-44m9</guid>
      <description>&lt;p&gt;Most site migrations do not fail at launch. They fail quietly over the following weeks, as old URLs that used to earn traffic and links start returning 404s, land on the homepage, or bounce through three redirects before reaching the right page.&lt;/p&gt;

&lt;p&gt;The fix is boring and reliable: keep a redirect map, and test it automatically before and after launch. This post gives you a small standard-library Python script that does exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the script checks
&lt;/h2&gt;

&lt;p&gt;For every row in a CSV of &lt;code&gt;old_url,expected_url&lt;/code&gt; it follows redirects &lt;strong&gt;one hop at a time&lt;/strong&gt; and reports:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Verdict&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;&lt;code&gt;OK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One permanent redirect (301/308), or none, ending on the expected URL with HTTP 200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WRONG_TARGET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The chain ends on a 200 page, but not the one you mapped (often the homepage)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CHAIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;More than one redirect before the final page - fix the map so old URLs go straight to the destination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TEMPORARY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The only redirect is a 302/303/307 - fine for campaigns, usually wrong for a migration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LOOP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A redirect loop or more than 10 hops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ERROR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A 4xx/5xx or a network error at the end of the chain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Following hops manually matters: if you let your HTTP client follow redirects automatically, you only see the final URL and miss chains and temporary redirects.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Check a redirect map after a site migration.

Input: a CSV with two columns, old_url,expected_url (header row required).
Output: one line per row: OK / WRONG_TARGET / CHAIN / LOOP / ERROR, plus the hops taken.
Standard library only.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.error&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urljoin&lt;/span&gt;

&lt;span class="n"&gt;MAX_HOPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NoRedirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPRedirectHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;redirect_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newurl&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# do not follow; we record each hop ourselves
&lt;/span&gt;

&lt;span class="n"&gt;OPENER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build_opener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NoRedirect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hops_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return a list of (status, url) pairs, following redirects one hop at a time.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;hops&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&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="k"&gt;while&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;hops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MAX_HOPS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;hops&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LOOP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hops&lt;/span&gt;
        &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HEAD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&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;User-Agent&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;redirect-check/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;OPENER&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;hops&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="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hops&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;hops&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="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;301&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="mi"&gt;303&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;307&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;308&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urljoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hops&lt;/span&gt;
        &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URLError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;hops&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="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="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hops&lt;/span&gt;
    &lt;span class="n"&gt;hops&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TOO_MANY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hops&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hops&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;last_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hops&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;redirects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hops&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&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="mi"&gt;302&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;303&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;307&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;308&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;last_status&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LOOP&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;TOO_MANY&lt;/span&gt;&lt;span class="sh"&gt;"&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;LOOP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;last_status&lt;/span&gt; &lt;span class="o"&gt;==&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;or&lt;/span&gt; &lt;span class="n"&gt;last_status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&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;ERROR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;last_url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&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="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&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;WRONG_TARGET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&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;redirects&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;1&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;CHAIN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;redirects&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;redirects&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;redirects&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;308&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;TEMPORARY&lt;/span&gt;&lt;span class="sh"&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;OK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&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;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;hops&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hops_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hops&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="n"&gt;trail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;trail&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; problem(s) found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;redirects.csv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;old&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;expected&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;url&lt;/span&gt;
&lt;span class="nv"&gt;http:&lt;/span&gt;&lt;span class="c1"&gt;//example.com/seo,https://example.com/seo&lt;/span&gt;
&lt;span class="nv"&gt;http:&lt;/span&gt;&lt;span class="c1"&gt;//example.com/services/seo,https://example.com/seo&lt;/span&gt;
&lt;span class="nv"&gt;https:&lt;/span&gt;&lt;span class="c1"&gt;//example.com/blog/old-post,https://example.com/blog/new-post&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python redirect_check.py redirects.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sample output (illustrative):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OK            301 http://example.com/seo -&amp;gt; 200 https://example.com/seo
CHAIN         301 http://example.com/services/seo -&amp;gt; 301 https://example.com/services/seo -&amp;gt; 200 https://example.com/seo
ERROR         404 https://example.com/blog/old-post

2 problem(s) found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script exits with code 1 when anything is wrong, so you can drop it into CI and fail a deployment that breaks the redirect map.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the redirect map comes from
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Export every URL that currently gets organic traffic or has backlinks (Search Console, analytics landing pages, your crawler of choice, backlink exports).&lt;/li&gt;
&lt;li&gt;Map each one to the &lt;strong&gt;closest equivalent&lt;/strong&gt; new page. Only fall back to a parent category when there is genuinely no equivalent; mass-redirecting everything to the homepage is treated like a soft 404.&lt;/li&gt;
&lt;li&gt;Remove pages you are intentionally retiring from the map and let them return 404 or 410.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Before and after launch
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before:&lt;/strong&gt; run it against staging (swap the hostnames in the CSV) and fix every non-&lt;code&gt;OK&lt;/code&gt; line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Launch day:&lt;/strong&gt; run it against production immediately after DNS or routing changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weeks 1-4:&lt;/strong&gt; run it on a schedule and watch Search Console for 404 spikes and "Page with redirect" reports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update internal links&lt;/strong&gt; so they point straight at the new URLs instead of relying on redirects.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It uses &lt;code&gt;HEAD&lt;/code&gt; requests; a few servers answer &lt;code&gt;HEAD&lt;/code&gt; differently from &lt;code&gt;GET&lt;/code&gt;. If you see odd &lt;code&gt;405&lt;/code&gt; errors, change &lt;code&gt;method="HEAD"&lt;/code&gt; to &lt;code&gt;"GET"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It does not execute JavaScript, so client-side redirects are not detected - which is also a good reason not to rely on them for migrations.&lt;/li&gt;
&lt;li&gt;Keep request volume polite on large maps (add a small delay or run it in batches).&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Written by the team at &lt;a href="https://thegorillavibe.com/" rel="noopener noreferrer"&gt;Gorilla Vibe&lt;/a&gt;, a GCC-focused marketing-as-a-service agency. The script is standard library only; copy it, adapt it, and use it freely.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>seo</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>A Content Brief Template for Writers, Editors and SEOs</title>
      <dc:creator>Gorilla Vibe</dc:creator>
      <pubDate>Tue, 29 Sep 2026 14:12:51 +0000</pubDate>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da/a-content-brief-template-for-writers-editors-and-seos-4pk8</link>
      <guid>https://dev.to/rokya_elbarbary_8e9bbb0da/a-content-brief-template-for-writers-editors-and-seos-4pk8</guid>
      <description>&lt;p&gt;A content brief is the contract between the person who knows &lt;em&gt;why&lt;/em&gt; a piece should exist and the person who writes it. Without one, writers guess at intent, editors rewrite for tone, and the finished article ranks for nothing and converts nobody.&lt;/p&gt;

&lt;p&gt;The template below works for blog articles, guides and landing page copy. Fill in every section; if a section cannot be filled, the piece is probably not ready to commission.&lt;/p&gt;




&lt;h2&gt;
  
  
  Content brief
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Working title: "&lt;/strong&gt; [title]"&lt;br&gt;
&lt;strong&gt;Owner / approver:&lt;/strong&gt; [name]&lt;br&gt;
&lt;strong&gt;Writer:&lt;/strong&gt; [name]&lt;br&gt;
&lt;strong&gt;Due date:&lt;/strong&gt; [YYYY-MM-DD]&lt;br&gt;
&lt;strong&gt;Target URL:&lt;/strong&gt; [final URL or planned slug]&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Purpose
&lt;/h3&gt;

&lt;p&gt;One sentence: what should this piece achieve for the business?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Example: Help founders understand what a marketing audit covers, so that qualified prospects book an audit call.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. Reader
&lt;/h3&gt;

&lt;p&gt;Describe the reader by &lt;strong&gt;situation&lt;/strong&gt;, not just job title.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Example: A founder of a 20-person company whose ad spend has grown but whose lead volume has not, who suspects money is being wasted but does not know where.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What do they already know? What do they need to believe or understand by the end?&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Search intent
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary query or topic&lt;/td&gt;
&lt;td&gt;[e.g. "digital marketing audit"]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intent type&lt;/td&gt;
&lt;td&gt;Informational / Commercial investigation / Transactional / Navigational&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What currently ranks?&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What is missing from those results?&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the results are dominated by a format you are not planning to write (for example, tools or product pages), reconsider the format.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Outline
&lt;/h3&gt;

&lt;p&gt;Provide H2s (and H3s where useful) in order. Mark sections that need input from a subject-matter expert.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;[H2]&lt;/li&gt;
&lt;li&gt;[H2]&lt;/li&gt;
&lt;li&gt;[H2]&lt;/li&gt;
&lt;li&gt;FAQ (only questions real customers ask)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  5. Evidence and sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Primary sources the writer must use (official documentation, first-party data, interviews).&lt;/li&gt;
&lt;li&gt;Claims that &lt;strong&gt;must not&lt;/strong&gt; be made without proof (percentages, guarantees, client names).&lt;/li&gt;
&lt;li&gt;Statistics: include only if the original source can be linked. Otherwise leave them out.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. Internal links
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Anchor idea&lt;/th&gt;
&lt;th&gt;Destination&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;[natural phrase]&lt;/td&gt;
&lt;td&gt;[related guide]&lt;/td&gt;
&lt;td&gt;Supports a concept the reader needs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[natural phrase]&lt;/td&gt;
&lt;td&gt;[relevant service page]&lt;/td&gt;
&lt;td&gt;Next step for readers who want help&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Keep links useful: one or two relevant service links in a helpful guide are fine; a link in every paragraph is not.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Format and style
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Target length: set by what the topic needs, not a word count target.&lt;/li&gt;
&lt;li&gt;Tone: [e.g. direct, practical, no jargon without explanation]&lt;/li&gt;
&lt;li&gt;Language/market: [e.g. English for GCC readers; an Arabic version to follow]&lt;/li&gt;
&lt;li&gt;Visuals needed: [diagrams, screenshots, tables]&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  8. Call to action
&lt;/h3&gt;

&lt;p&gt;What should the reader do next, and where on the page?&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Definition of done
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Answers the primary question within the first few paragraphs&lt;/li&gt;
&lt;li&gt;[ ] Every claim is sourced or removed&lt;/li&gt;
&lt;li&gt;[ ] Title, meta description and H1 written and unique&lt;/li&gt;
&lt;li&gt;[ ] Internal links added and checked&lt;/li&gt;
&lt;li&gt;[ ] Reviewed by the subject-matter expert&lt;/li&gt;
&lt;li&gt;[ ] Proofread by a second person&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Using the brief after publishing
&lt;/h2&gt;

&lt;p&gt;Keep the brief with the published URL. Three months later, compare the page's actual search queries and conversions against the purpose and intent you wrote down - that comparison tells you whether to update, consolidate or leave the page alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Google Search Central: &lt;a href="https://developers.google.com/search/docs/fundamentals/creating-helpful-content" rel="noopener noreferrer"&gt;Creating helpful, reliable, people-first content&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Briefs are one part of a wider content system. For how content strategy and distribution fit together, see &lt;a href="https://thegorillavibe.com/content-social-media" rel="noopener noreferrer"&gt;the content and social media overview from Gorilla Vibe&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>writing</category>
      <category>seo</category>
      <category>marketing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>A Webhook Runbook for Marketing Automation That Never Loses a Lead</title>
      <dc:creator>Gorilla Vibe</dc:creator>
      <pubDate>Tue, 29 Sep 2026 14:05:24 +0000</pubDate>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da/a-webhook-runbook-for-marketing-automation-that-never-loses-a-lead-4052</link>
      <guid>https://dev.to/rokya_elbarbary_8e9bbb0da/a-webhook-runbook-for-marketing-automation-that-never-loses-a-lead-4052</guid>
      <description>&lt;p&gt;A typical lead journey now crosses several systems: a website form, an automation tool, a CRM, a messaging channel and sometimes a spreadsheet. Webhooks glue them together, and when a webhook silently fails, a real enquiry disappears. This runbook describes how to design those integrations so failures are rare, visible and recoverable.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Map the flow before building it
&lt;/h2&gt;

&lt;p&gt;Write the journey as a table first:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Source system&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Destination&lt;/th&gt;
&lt;th&gt;Payload owner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Website form&lt;/td&gt;
&lt;td&gt;Form submitted (server confirmed)&lt;/td&gt;
&lt;td&gt;Automation tool webhook&lt;/td&gt;
&lt;td&gt;Web team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Automation tool&lt;/td&gt;
&lt;td&gt;New lead&lt;/td&gt;
&lt;td&gt;CRM - create or update contact&lt;/td&gt;
&lt;td&gt;Marketing ops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;CRM&lt;/td&gt;
&lt;td&gt;Lead assigned&lt;/td&gt;
&lt;td&gt;Sales notification channel&lt;/td&gt;
&lt;td&gt;Sales ops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;CRM&lt;/td&gt;
&lt;td&gt;Lead qualified&lt;/td&gt;
&lt;td&gt;Ad platform offline conversion upload&lt;/td&gt;
&lt;td&gt;Marketing ops&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every row needs an owner who is responsible when that step breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Design the payload
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Include a &lt;strong&gt;unique event ID&lt;/strong&gt; generated at the source (for example a UUID per form submission). Every downstream step should carry it.&lt;/li&gt;
&lt;li&gt;Include a &lt;strong&gt;timestamp in UTC&lt;/strong&gt; and the &lt;strong&gt;source&lt;/strong&gt; (form ID, page URL).&lt;/li&gt;
&lt;li&gt;Send only the fields the destination needs. Personal data you do not transmit cannot leak.&lt;/li&gt;
&lt;li&gt;Version the payload (&lt;code&gt;"schema_version": 2&lt;/code&gt;) so you can change it without breaking consumers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Make every receiver idempotent
&lt;/h2&gt;

&lt;p&gt;Webhook senders retry. Networks duplicate requests. If the same payload arrives twice, the receiver must not create two contacts or send two welcome messages.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store processed event IDs and ignore repeats.&lt;/li&gt;
&lt;li&gt;Prefer &lt;strong&gt;upsert&lt;/strong&gt; (create or update by email or external ID) over blind create.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Handle failure deliberately
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Behaviour&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Destination returns 5xx or times out&lt;/td&gt;
&lt;td&gt;Retry with exponential backoff (for example after 1, 5, 15 and 60 minutes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Destination returns 4xx (bad request)&lt;/td&gt;
&lt;td&gt;Do not retry blindly - send to a dead-letter queue and alert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Destination rate-limits (429)&lt;/td&gt;
&lt;td&gt;Respect &lt;code&gt;Retry-After&lt;/code&gt;; slow down&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All retries exhausted&lt;/td&gt;
&lt;td&gt;Dead-letter queue + alert to the step owner&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A &lt;strong&gt;dead-letter queue&lt;/strong&gt; can be as simple as a table or sheet that stores failed payloads with the error message, so they can be replayed after the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Secure the endpoints
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Verify a &lt;strong&gt;signature&lt;/strong&gt; (HMAC of the payload with a shared secret) on incoming webhooks when the sender supports it.&lt;/li&gt;
&lt;li&gt;Keep secrets in the automation tool's credential store or environment variables - never inside a shared document or a workflow description.&lt;/li&gt;
&lt;li&gt;Reject payloads older than a few minutes to limit replay attacks, if timestamps are signed.&lt;/li&gt;
&lt;li&gt;Use HTTPS only.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Monitor what matters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Volume check:&lt;/strong&gt; alert if form submissions arrive on the website but no leads are created in the CRM for a defined period during business hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error rate:&lt;/strong&gt; alert on any dead-letter entry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconciliation:&lt;/strong&gt; once a week, compare form submission counts against CRM lead counts for the same period. Differences reveal silent failures.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Change management
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Test changes in a sandbox or with a test form before touching the production flow.&lt;/li&gt;
&lt;li&gt;Keep a changelog of workflow edits: date, who, what, why.&lt;/li&gt;
&lt;li&gt;After any change, submit a real test lead end-to-end and confirm it arrives everywhere it should.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Incident checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Identify the first failing step using the event ID.&lt;/li&gt;
&lt;li&gt;Pause downstream steps that could make things worse (for example automated messages).&lt;/li&gt;
&lt;li&gt;Fix the cause.&lt;/li&gt;
&lt;li&gt;Replay dead-letter payloads in order.&lt;/li&gt;
&lt;li&gt;Confirm with the reconciliation check and write a short incident note.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Automation should remove manual work without hiding failures. For an audit-first view of automation, see &lt;a href="https://thegorillavibe.com/ai-automation" rel="noopener noreferrer"&gt;Gorilla Vibe's approach to AI marketing automation&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>automation</category>
      <category>webhooks</category>
      <category>devops</category>
      <category>marketing</category>
    </item>
    <item>
      <title>A CRO Experiment Brief Template That Stops False Wins</title>
      <dc:creator>Gorilla Vibe</dc:creator>
      <pubDate>Tue, 29 Sep 2026 13:58:06 +0000</pubDate>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da/a-cro-experiment-brief-template-that-stops-false-wins-p3n</link>
      <guid>https://dev.to/rokya_elbarbary_8e9bbb0da/a-cro-experiment-brief-template-that-stops-false-wins-p3n</guid>
      <description>&lt;p&gt;Most failed A/B tests fail before they launch: the hypothesis was vague, the metric was chosen afterwards, or the test was stopped the moment the dashboard turned green. A one-page brief, completed and agreed before anything is built, prevents all three.&lt;/p&gt;

&lt;p&gt;Copy the template below into your ticket system or experiment log.&lt;/p&gt;




&lt;h2&gt;
  
  
  Experiment brief
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ID:&lt;/strong&gt; &lt;code&gt;EXP-&amp;lt;number&amp;gt;&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Owner:&lt;/strong&gt; [name]&lt;br&gt;
&lt;strong&gt;Status:&lt;/strong&gt; Draft / Approved / Running / Concluded&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Observation
&lt;/h3&gt;

&lt;p&gt;What did you see, and where did you see it? Link the evidence.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Example: Session recordings and form analytics show that a large share of mobile visitors who open the quote form abandon it at the "company size" field.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Evidence sources can include analytics funnels, heatmaps, session recordings, on-site surveys, support tickets and usability tests. An opinion in a meeting is not an observation.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Hypothesis
&lt;/h3&gt;

&lt;p&gt;Use this structure:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Because&lt;/strong&gt; we observed [observation],&lt;br&gt;
&lt;strong&gt;we believe&lt;/strong&gt; that [change]&lt;br&gt;
&lt;strong&gt;for&lt;/strong&gt; [audience / segment]&lt;br&gt;
&lt;strong&gt;will cause&lt;/strong&gt; [expected behaviour change].&lt;br&gt;
&lt;strong&gt;We will know this when&lt;/strong&gt; [primary metric] changes in [direction].&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. Metrics
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary (one only)&lt;/td&gt;
&lt;td&gt;e.g. quote form completion rate&lt;/td&gt;
&lt;td&gt;Completed quote forms / sessions that viewed the form&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guardrail&lt;/td&gt;
&lt;td&gt;e.g. lead quality&lt;/td&gt;
&lt;td&gt;Share of leads accepted by sales&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guardrail&lt;/td&gt;
&lt;td&gt;e.g. page performance&lt;/td&gt;
&lt;td&gt;No regression in LCP or INP on the tested template&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secondary (diagnostic)&lt;/td&gt;
&lt;td&gt;e.g. field-level drop-off&lt;/td&gt;
&lt;td&gt;Not used to call the result&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Decide the primary metric &lt;strong&gt;before&lt;/strong&gt; launch. Reporting whichever metric happened to move afterwards is how false wins get shipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Design
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Variants:&lt;/strong&gt; Control vs. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audience and targeting:&lt;/strong&gt; device, market, language, traffic source&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic split:&lt;/strong&gt; usually 50/50&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unit of randomisation:&lt;/strong&gt; user (preferred) or session&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Sample size and duration
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Estimate the required sample with a calculator, using your current baseline conversion rate and the &lt;strong&gt;smallest effect worth detecting&lt;/strong&gt; for the business.&lt;/li&gt;
&lt;li&gt;Run for &lt;strong&gt;whole weeks&lt;/strong&gt; (at least one, usually two or more full business cycles) to avoid day-of-week bias.&lt;/li&gt;
&lt;li&gt;If the traffic needed exceeds what the page receives in a reasonable time, do not run the test. Use qualitative research or make the change based on evidence instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. Stopping rule
&lt;/h3&gt;

&lt;p&gt;Write it down: "We will stop when each variant reaches [n] visitors &lt;strong&gt;and&lt;/strong&gt; at least [x] full weeks have passed." Do not stop early because a result looks significant on day three - repeated peeking inflates false positives unless your tool uses a method designed for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. QA checklist before launch
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Variant renders correctly on mobile and desktop, in every language the page supports&lt;/li&gt;
&lt;li&gt;[ ] No flicker of the original content before the variant loads&lt;/li&gt;
&lt;li&gt;[ ] Analytics events fire identically in both variants&lt;/li&gt;
&lt;li&gt;[ ] The test does not break forms, checkout or consent banners&lt;/li&gt;
&lt;li&gt;[ ] Exclusion rules work (internal traffic, bots)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  8. Result and decision
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Entry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Outcome&lt;/td&gt;
&lt;td&gt;Win / Loss / Inconclusive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary metric result&lt;/td&gt;
&lt;td&gt;[effect and confidence interval]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guardrails&lt;/td&gt;
&lt;td&gt;[any regressions]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decision&lt;/td&gt;
&lt;td&gt;Ship / Iterate / Discard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What we learned&lt;/td&gt;
&lt;td&gt;[one or two sentences, regardless of outcome]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Keeping a learning log
&lt;/h2&gt;

&lt;p&gt;Store every concluded brief - including losses and inconclusive tests - in one searchable place. Over time the log becomes the most valuable CRO asset you own: it stops the team re-running the same idea and shows which kinds of changes actually move your audience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;For a broader view of how a structured, funnel-wide conversion programme is run, see &lt;a href="https://thegorillavibe.com/cro" rel="noopener noreferrer"&gt;how Gorilla Vibe runs conversion programmes&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>testing</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>analytics</category>
    </item>
  </channel>
</rss>
