<?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: rokya elbarbary</title>
    <description>The latest articles on DEV Community by rokya elbarbary (@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%2Fe799a3da-3ede-4956-80af-0d061dbc104b.png</url>
      <title>DEV Community: rokya elbarbary</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>Test Your Redirect Map Before and After a Site Migration With a Small Python Script</title>
      <dc:creator>rokya elbarbary</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>rokya elbarbary</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>rokya elbarbary</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>rokya elbarbary</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>
    <item>
      <title>A Bilingual Arabic/English (RTL + LTR) UX Checklist for Web Teams</title>
      <dc:creator>rokya elbarbary</dc:creator>
      <pubDate>Tue, 29 Sep 2026 13:49:40 +0000</pubDate>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da/a-bilingual-arabicenglish-rtl-ltr-ux-checklist-for-web-teams-4o17</link>
      <guid>https://dev.to/rokya_elbarbary_8e9bbb0da/a-bilingual-arabicenglish-rtl-ltr-ux-checklist-for-web-teams-4o17</guid>
      <description>&lt;p&gt;Sites that serve the Gulf and the wider Arabic-speaking market often need to work equally well in Arabic (right-to-left) and English (left-to-right). Mirroring the English layout is not enough: typography, forms, icons, numbers and search annotations all need attention. This checklist is meant for designers, developers and QA testers working on the same release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Document and layout
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Set &lt;code&gt;lang&lt;/code&gt; and &lt;code&gt;dir&lt;/code&gt; on the &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; element: &lt;code&gt;&amp;lt;html lang="ar" dir="rtl"&amp;gt;&lt;/code&gt; for Arabic pages, &lt;code&gt;&amp;lt;html lang="en" dir="ltr"&amp;gt;&lt;/code&gt; for English.&lt;/li&gt;
&lt;li&gt;[ ] Use &lt;strong&gt;CSS logical properties&lt;/strong&gt; (&lt;code&gt;margin-inline-start&lt;/code&gt;, &lt;code&gt;padding-inline-end&lt;/code&gt;, &lt;code&gt;inset-inline-start&lt;/code&gt;, &lt;code&gt;text-align: start&lt;/code&gt;) instead of &lt;code&gt;left&lt;/code&gt; and &lt;code&gt;right&lt;/code&gt;, so one stylesheet works in both directions.&lt;/li&gt;
&lt;li&gt;[ ] Flex and grid layouts follow the writing direction automatically; check any component that uses absolute positioning or transforms.&lt;/li&gt;
&lt;li&gt;[ ] Mixed-direction strings (an English brand name inside an Arabic sentence, a phone number, a URL) are wrapped in elements with &lt;code&gt;dir="auto"&lt;/code&gt; or use &lt;code&gt;&amp;lt;bdi&amp;gt;&lt;/code&gt; where the direction is unknown.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Typography
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Choose an Arabic typeface designed for screen use and test it at body sizes, not only in headlines.&lt;/li&gt;
&lt;li&gt;[ ] Arabic text usually needs a &lt;strong&gt;larger font size and line height&lt;/strong&gt; than Latin text to be equally readable; do not reuse English values blindly.&lt;/li&gt;
&lt;li&gt;[ ] Do not apply letter-spacing to Arabic - it breaks the connected script.&lt;/li&gt;
&lt;li&gt;[ ] Avoid all-caps styling rules that only make sense for Latin scripts.&lt;/li&gt;
&lt;li&gt;[ ] Check that text truncation (&lt;code&gt;text-overflow: ellipsis&lt;/code&gt;) works from the correct side.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Icons and imagery
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Mirror&lt;/strong&gt; directional icons: back/forward arrows, chevrons in breadcrumbs, progress indicators, "send" icons.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Do not mirror&lt;/strong&gt; icons that represent real-world objects or universal symbols: play buttons, clocks, checkmarks, logos, media controls.&lt;/li&gt;
&lt;li&gt;[ ] Carousels and sliders move in the reading direction; swipe gestures match.&lt;/li&gt;
&lt;li&gt;[ ] Photographs with text or directional composition may need separate crops, not a flipped image.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Forms
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Labels, help text and error messages align to the start edge.&lt;/li&gt;
&lt;li&gt;[ ] Inputs that hold Latin-only data (email, URL, some IDs) can be set to &lt;code&gt;dir="ltr"&lt;/code&gt; so characters do not appear reversed while typing.&lt;/li&gt;
&lt;li&gt;[ ] Phone inputs accept both Western Arabic (0-9) and Eastern Arabic (٠-٩) digits, or clearly state which is expected - and the backend normalises them.&lt;/li&gt;
&lt;li&gt;[ ] Validation messages are translated, not just the labels.&lt;/li&gt;
&lt;li&gt;[ ] Date pickers respect the locale's first day of the week and reading direction.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Numbers, dates and currency
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Decide deliberately whether Arabic pages display Western Arabic or Eastern Arabic numerals, and apply it consistently. Use &lt;code&gt;Intl.NumberFormat&lt;/code&gt; and &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; with the right locale rather than hand-built strings.&lt;/li&gt;
&lt;li&gt;[ ] Currency symbols and codes (AED, SAR, QAR, KWD, BHD, OMR) appear in the position readers expect for the locale.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Content and navigation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] The language switcher is visible on every page, labelled in the target language ("English" / "العربية"), and links to the &lt;strong&gt;equivalent page&lt;/strong&gt;, not the homepage.&lt;/li&gt;
&lt;li&gt;[ ] Translations are written or reviewed by a native speaker, and adapted for the market - not machine-translated and published.&lt;/li&gt;
&lt;li&gt;[ ] Navigation order reads correctly from the start edge.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Search and metadata
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Each language version has its own URL (for example &lt;code&gt;/ar/...&lt;/code&gt; and &lt;code&gt;/en/...&lt;/code&gt;), its own title and meta description, and a self-referencing canonical.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;hreflang&lt;/code&gt; annotations link each language version to the others and back to itself, and include &lt;code&gt;x-default&lt;/code&gt; if there is a language selector page.&lt;/li&gt;
&lt;li&gt;[ ] Structured data on Arabic pages is written in Arabic where it describes visible content.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  QA pass
&lt;/h2&gt;

&lt;p&gt;Test every key template in both languages on a real mobile device, with long real-world strings (long names, long company names, mixed Arabic/English addresses). Most RTL bugs only appear with real content.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;W3C Internationalization: &lt;a href="https://www.w3.org/International/questions/qa-html-dir" rel="noopener noreferrer"&gt;Structural markup and right-to-left text in HTML&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MDN: &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_logical_properties_and_values" rel="noopener noreferrer"&gt;CSS logical properties and values&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Search Central: &lt;a href="https://developers.google.com/search/docs/specialty/international/localized-versions" rel="noopener noreferrer"&gt;Localized versions of your pages&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;For how research, wireframing and design systems come together in interface work, see the &lt;a href="https://thegorillavibe.com/ui-ux" rel="noopener noreferrer"&gt;UI/UX design page from Gorilla Vibe&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>a11y</category>
      <category>i18n</category>
    </item>
    <item>
      <title>Core Web Vitals Explained for the Marketers on Your Team</title>
      <dc:creator>rokya elbarbary</dc:creator>
      <pubDate>Tue, 29 Sep 2026 13:12:40 +0000</pubDate>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da/core-web-vitals-explained-for-the-marketers-on-your-team-3m3h</link>
      <guid>https://dev.to/rokya_elbarbary_8e9bbb0da/core-web-vitals-explained-for-the-marketers-on-your-team-3m3h</guid>
      <description>&lt;p&gt;Core Web Vitals are Google's three user-experience metrics for loading, responsiveness and visual stability. They matter for two reasons: they are part of the page experience signals Google Search considers, and - more importantly - they describe whether real visitors can actually use your pages.&lt;/p&gt;

&lt;p&gt;This guide explains each metric in plain language, how to measure it, and where fixes usually come from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three metrics
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Measures&lt;/th&gt;
&lt;th&gt;"Good" threshold (Google)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;LCP&lt;/strong&gt; - Largest Contentful Paint&lt;/td&gt;
&lt;td&gt;How long until the main content (usually a hero image or headline) is visible&lt;/td&gt;
&lt;td&gt;2.5 seconds or less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;INP&lt;/strong&gt; - Interaction to Next Paint&lt;/td&gt;
&lt;td&gt;How quickly the page responds visually after a tap, click or key press&lt;/td&gt;
&lt;td&gt;200 milliseconds or less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;CLS&lt;/strong&gt; - Cumulative Layout Shift&lt;/td&gt;
&lt;td&gt;How much content unexpectedly jumps around while loading&lt;/td&gt;
&lt;td&gt;0.1 or less&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Google assesses each metric at the &lt;strong&gt;75th percentile&lt;/strong&gt; of real page loads, separately for mobile and desktop. INP replaced First Input Delay (FID) as a Core Web Vital in March 2024.&lt;/p&gt;

&lt;h2&gt;
  
  
  Field data vs lab data
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Field data&lt;/strong&gt; comes from real Chrome users (the Chrome User Experience Report, shown in Search Console and PageSpeed Insights). This is what counts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lab data&lt;/strong&gt; comes from a simulated test (Lighthouse). It is repeatable and good for debugging, but it cannot measure INP directly and it does not reflect your real users' devices and networks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A page can score well in Lighthouse and still fail in the field, usually on mid-range Android phones on mobile networks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where LCP problems usually come from
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Slow server response (TTFB).&lt;/strong&gt; Uncached HTML, slow APIs during server rendering, or no CDN close to your users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The LCP image is discovered late.&lt;/strong&gt; For example, a hero image set as a CSS background or injected by JavaScript. Put it in the HTML as an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; and consider &lt;code&gt;fetchpriority="high"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The LCP image is lazy-loaded.&lt;/strong&gt; Never apply &lt;code&gt;loading="lazy"&lt;/code&gt; to the above-the-fold hero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oversized images.&lt;/strong&gt; Serve responsive sizes (&lt;code&gt;srcset&lt;/code&gt;) and modern formats.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render-blocking CSS and fonts.&lt;/strong&gt; Inline critical CSS; use &lt;code&gt;font-display: swap&lt;/code&gt; or &lt;code&gt;optional&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where INP problems usually come from
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Long JavaScript tasks&lt;/strong&gt; on the main thread - large bundles, heavy hydration, third-party scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag manager bloat.&lt;/strong&gt; Every chat widget, heatmap and pixel adds main-thread work. Audit tags quarterly and remove what nobody uses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expensive event handlers&lt;/strong&gt; that do layout-heavy work synchronously. Yield to the main thread and defer non-urgent work.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where CLS problems usually come from
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Images and videos &lt;strong&gt;without width and height&lt;/strong&gt; (or an aspect-ratio).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Late-loading banners&lt;/strong&gt;, cookie bars or promos that push content down instead of overlaying it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web fonts&lt;/strong&gt; that swap and change text size; match fallback font metrics.&lt;/li&gt;
&lt;li&gt;Ads or embeds inserted without reserved space.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A workable process for marketing teams
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;strong&gt;Core Web Vitals report&lt;/strong&gt; in Search Console and group failing URLs by template (home, service page, blog post, landing page).&lt;/li&gt;
&lt;li&gt;Fix templates, not individual pages. One template fix can move hundreds of URLs.&lt;/li&gt;
&lt;li&gt;Use PageSpeed Insights on one representative URL per template to find the specific cause.&lt;/li&gt;
&lt;li&gt;Ship the fix, then wait: field data is a rolling 28-day window, so improvements take several weeks to show.&lt;/li&gt;
&lt;li&gt;Add a performance budget (bundle size, image weight, number of third-party tags) to your release checklist so the problem does not return.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;web.dev: &lt;a href="https://web.dev/articles/vitals" rel="noopener noreferrer"&gt;Web Vitals&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;web.dev: &lt;a href="https://web.dev/articles/optimize-lcp" rel="noopener noreferrer"&gt;Optimize LCP&lt;/a&gt;, &lt;a href="https://web.dev/articles/optimize-inp" rel="noopener noreferrer"&gt;Optimize INP&lt;/a&gt;, &lt;a href="https://web.dev/articles/optimize-cls" rel="noopener noreferrer"&gt;Optimize CLS&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Search Central: &lt;a href="https://developers.google.com/search/docs/appearance/core-web-vitals" rel="noopener noreferrer"&gt;Understanding Core Web Vitals and Google search results&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Performance is easiest to get right when it is designed in from the start. The &lt;a href="https://thegorillavibe.com/web-platform-development" rel="noopener noreferrer"&gt;Gorilla Vibe&lt;/a&gt; web platform development page describes building sites around lead generation and speed.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webperf</category>
      <category>webdev</category>
      <category>performance</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Core Web Vitals Explained for the Marketers on Your Team</title>
      <dc:creator>rokya elbarbary</dc:creator>
      <pubDate>Tue, 29 Sep 2026 12:48:34 +0000</pubDate>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da/core-web-vitals-explained-for-the-marketers-on-your-team-3p62</link>
      <guid>https://dev.to/rokya_elbarbary_8e9bbb0da/core-web-vitals-explained-for-the-marketers-on-your-team-3p62</guid>
      <description>&lt;p&gt;Core Web Vitals are Google's three user-experience metrics for loading, responsiveness and visual stability. They matter for two reasons: they are part of the page experience signals Google Search considers, and - more importantly - they describe whether real visitors can actually use your pages.&lt;/p&gt;

&lt;p&gt;This guide explains each metric in plain language, how to measure it, and where fixes usually come from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three metrics
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Measures&lt;/th&gt;
&lt;th&gt;"Good" threshold (Google)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;LCP&lt;/strong&gt; - Largest Contentful Paint&lt;/td&gt;
&lt;td&gt;How long until the main content (usually a hero image or headline) is visible&lt;/td&gt;
&lt;td&gt;2.5 seconds or less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;INP&lt;/strong&gt; - Interaction to Next Paint&lt;/td&gt;
&lt;td&gt;How quickly the page responds visually after a tap, click or key press&lt;/td&gt;
&lt;td&gt;200 milliseconds or less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;CLS&lt;/strong&gt; - Cumulative Layout Shift&lt;/td&gt;
&lt;td&gt;How much content unexpectedly jumps around while loading&lt;/td&gt;
&lt;td&gt;0.1 or less&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Google assesses each metric at the &lt;strong&gt;75th percentile&lt;/strong&gt; of real page loads, separately for mobile and desktop. INP replaced First Input Delay (FID) as a Core Web Vital in March 2024.&lt;/p&gt;

&lt;h2&gt;
  
  
  Field data vs lab data
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Field data&lt;/strong&gt; comes from real Chrome users (the Chrome User Experience Report, shown in Search Console and PageSpeed Insights). This is what counts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lab data&lt;/strong&gt; comes from a simulated test (Lighthouse). It is repeatable and good for debugging, but it cannot measure INP directly and it does not reflect your real users' devices and networks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A page can score well in Lighthouse and still fail in the field, usually on mid-range Android phones on mobile networks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where LCP problems usually come from
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Slow server response (TTFB).&lt;/strong&gt; Uncached HTML, slow APIs during server rendering, or no CDN close to your users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The LCP image is discovered late.&lt;/strong&gt; For example, a hero image set as a CSS background or injected by JavaScript. Put it in the HTML as an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; and consider &lt;code&gt;fetchpriority="high"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The LCP image is lazy-loaded.&lt;/strong&gt; Never apply &lt;code&gt;loading="lazy"&lt;/code&gt; to the above-the-fold hero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oversized images.&lt;/strong&gt; Serve responsive sizes (&lt;code&gt;srcset&lt;/code&gt;) and modern formats.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render-blocking CSS and fonts.&lt;/strong&gt; Inline critical CSS; use &lt;code&gt;font-display: swap&lt;/code&gt; or &lt;code&gt;optional&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where INP problems usually come from
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Long JavaScript tasks&lt;/strong&gt; on the main thread - large bundles, heavy hydration, third-party scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag manager bloat.&lt;/strong&gt; Every chat widget, heatmap and pixel adds main-thread work. Audit tags quarterly and remove what nobody uses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expensive event handlers&lt;/strong&gt; that do layout-heavy work synchronously. Yield to the main thread and defer non-urgent work.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where CLS problems usually come from
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Images and videos &lt;strong&gt;without width and height&lt;/strong&gt; (or an aspect-ratio).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Late-loading banners&lt;/strong&gt;, cookie bars or promos that push content down instead of overlaying it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web fonts&lt;/strong&gt; that swap and change text size; match fallback font metrics.&lt;/li&gt;
&lt;li&gt;Ads or embeds inserted without reserved space.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A workable process for marketing teams
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;strong&gt;Core Web Vitals report&lt;/strong&gt; in Search Console and group failing URLs by template (home, service page, blog post, landing page).&lt;/li&gt;
&lt;li&gt;Fix templates, not individual pages. One template fix can move hundreds of URLs.&lt;/li&gt;
&lt;li&gt;Use PageSpeed Insights on one representative URL per template to find the specific cause.&lt;/li&gt;
&lt;li&gt;Ship the fix, then wait: field data is a rolling 28-day window, so improvements take several weeks to show.&lt;/li&gt;
&lt;li&gt;Add a performance budget (bundle size, image weight, number of third-party tags) to your release checklist so the problem does not return.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;web.dev: &lt;a href="https://web.dev/articles/vitals" rel="noopener noreferrer"&gt;Web Vitals&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;web.dev: &lt;a href="https://web.dev/articles/optimize-lcp" rel="noopener noreferrer"&gt;Optimize LCP&lt;/a&gt;, &lt;a href="https://web.dev/articles/optimize-inp" rel="noopener noreferrer"&gt;Optimize INP&lt;/a&gt;, &lt;a href="https://web.dev/articles/optimize-cls" rel="noopener noreferrer"&gt;Optimize CLS&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Search Central: &lt;a href="https://developers.google.com/search/docs/appearance/core-web-vitals" rel="noopener noreferrer"&gt;Understanding Core Web Vitals and Google search results&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Performance is easiest to get right when it is designed in from the start. The &lt;a href="https://thegorillavibe.com/web-platform-development" rel="noopener noreferrer"&gt;Gorilla Vibe&lt;/a&gt; web platform development page describes building sites around lead generation and speed.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webperf</category>
      <category>webdev</category>
      <category>performance</category>
      <category>marketing</category>
    </item>
    <item>
      <title>UTM Governance: Keep Your Campaign Data Out of "Unassigned"</title>
      <dc:creator>rokya elbarbary</dc:creator>
      <pubDate>Tue, 29 Sep 2026 12:45:35 +0000</pubDate>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da/utm-governance-keep-your-campaign-data-out-of-unassigned-3m8g</link>
      <guid>https://dev.to/rokya_elbarbary_8e9bbb0da/utm-governance-keep-your-campaign-data-out-of-unassigned-3m8g</guid>
      <description>&lt;p&gt;UTM parameters are the cheapest attribution system you will ever own, and one of the easiest to break. One person tags a newsletter &lt;code&gt;utm_medium=Email&lt;/code&gt;, another uses &lt;code&gt;e-mail&lt;/code&gt;, a third forgets tags entirely - and a month later the channel report is fiction.&lt;/p&gt;

&lt;p&gt;This note sets out a small set of rules that keeps campaign data clean in Google Analytics 4 and in any tool that reads the same parameters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parameters
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Required?&lt;/th&gt;
&lt;th&gt;Describes&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;utm_source&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Who sent the traffic&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;google&lt;/code&gt;, &lt;code&gt;meta&lt;/code&gt;, &lt;code&gt;linkedin&lt;/code&gt;, &lt;code&gt;newsletter&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;utm_medium&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;The type of channel&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cpc&lt;/code&gt;, &lt;code&gt;paid_social&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;referral&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;utm_campaign&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;The campaign or initiative&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026q4_audit_offer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;utm_content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;The specific creative or link&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;carousel_v2&lt;/code&gt;, &lt;code&gt;footer_link&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;utm_term&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;Keyword (mainly paid search)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;marketing_subscription&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;utm_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;A campaign ID that matches your ad platform or cost data&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cmp_1042&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;GA4 also recognises &lt;code&gt;utm_source_platform&lt;/code&gt;, &lt;code&gt;utm_creative_format&lt;/code&gt; and &lt;code&gt;utm_marketing_tactic&lt;/code&gt; if you need them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 1: lowercase everything
&lt;/h2&gt;

&lt;p&gt;GA4 treats &lt;code&gt;Email&lt;/code&gt; and &lt;code&gt;email&lt;/code&gt; as different values. Agree that every UTM value is lowercase, with underscores instead of spaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 2: &lt;code&gt;utm_medium&lt;/code&gt; must match GA4's channel definitions
&lt;/h2&gt;

&lt;p&gt;GA4's default channel grouping assigns sessions to channels such as Paid Search, Paid Social, Email and Organic Social largely by looking at the medium and the source. If your medium values do not match the patterns Google documents, sessions fall into &lt;strong&gt;Unassigned&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A safe, simple vocabulary:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Channel you intend&lt;/th&gt;
&lt;th&gt;&lt;code&gt;utm_medium&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Paid search&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cpc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Auto-tagging (gclid) handles Google Ads; use UTMs for other search engines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paid social&lt;/td&gt;
&lt;td&gt;&lt;code&gt;paid_social&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pair with a recognised social &lt;code&gt;utm_source&lt;/code&gt; such as &lt;code&gt;facebook&lt;/code&gt;, &lt;code&gt;instagram&lt;/code&gt;, &lt;code&gt;linkedin&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Organic social&lt;/td&gt;
&lt;td&gt;&lt;code&gt;social&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;For unpaid posts you publish yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email&lt;/td&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Newsletters and automated sequences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Display&lt;/td&gt;
&lt;td&gt;&lt;code&gt;display&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Programmatic and banner placements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Affiliates / partners&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;affiliate&lt;/code&gt; or &lt;code&gt;referral&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Pick one and keep it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Check the current channel definitions in Google's documentation before finalising your list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 3: a campaign naming pattern
&lt;/h2&gt;

&lt;p&gt;A pattern makes campaigns sortable and self-explanatory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;yyyy&amp;gt;&amp;lt;q|mm&amp;gt;_&amp;lt;objective&amp;gt;_&amp;lt;offer-or-theme&amp;gt;[_&amp;lt;market&amp;gt;]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Examples: &lt;code&gt;2026q4_leads_audit_offer_uae&lt;/code&gt;, &lt;code&gt;202611_awareness_brand_film&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 4: never tag internal links
&lt;/h2&gt;

&lt;p&gt;UTMs on links between pages of your own site start a new session attribution and overwrite the original source. Use them only on links that bring people &lt;strong&gt;into&lt;/strong&gt; the site from somewhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 5: one builder, one log
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Generate every tagged URL from a shared builder (a spreadsheet with data validation is enough).&lt;/li&gt;
&lt;li&gt;Keep a log: date, owner, channel, final URL, and where it was used.&lt;/li&gt;
&lt;li&gt;Validation dropdowns for &lt;code&gt;utm_source&lt;/code&gt; and &lt;code&gt;utm_medium&lt;/code&gt; remove most typos before they happen.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  QA before launch
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open the tagged URL in a private window and confirm the landing page loads without redirects that strip query strings.&lt;/li&gt;
&lt;li&gt;Check GA4 &lt;strong&gt;Realtime&lt;/strong&gt; for the expected source / medium.&lt;/li&gt;
&lt;li&gt;Confirm consent tools and redirects preserve parameters.&lt;/li&gt;
&lt;li&gt;For paid platforms, check whether the platform's own tracking template or auto-tagging already adds parameters, so you do not double-tag.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Monthly clean-up
&lt;/h2&gt;

&lt;p&gt;Review Traffic acquisition by &lt;strong&gt;session source / medium&lt;/strong&gt;. Anything in "Unassigned" or with unexpected capitalisation points to a link that needs fixing at the source, not a report filter.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Google Analytics Help: &lt;a href="https://support.google.com/analytics/answer/10917952" rel="noopener noreferrer"&gt;Collect campaign data with custom URLs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Analytics Help: &lt;a href="https://support.google.com/analytics/answer/9756891" rel="noopener noreferrer"&gt;Default channel group&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Clean attribution is the foundation of media optimisation. For how that foundation supports paid media, see &lt;a href="https://thegorillavibe.com/performance-marketing" rel="noopener noreferrer"&gt;Gorilla Vibe's performance marketing overview&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>analytics</category>
      <category>marketing</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Structured Data Templates (JSON-LD) You Can Copy Into Your Templates</title>
      <dc:creator>rokya elbarbary</dc:creator>
      <pubDate>Tue, 29 Sep 2026 12:43:09 +0000</pubDate>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da/structured-data-templates-json-ld-you-can-copy-into-your-templates-1a4a</link>
      <guid>https://dev.to/rokya_elbarbary_8e9bbb0da/structured-data-templates-json-ld-you-can-copy-into-your-templates-1a4a</guid>
      <description>&lt;p&gt;Structured data tells search engines explicitly what a page is about: which organisation publishes it, who wrote an article, where a page sits in the site hierarchy. It does not guarantee a rich result, and Google has narrowed several rich result types over time, so treat markup as a way to describe your content accurately rather than as a ranking trick.&lt;/p&gt;

&lt;p&gt;Google recommends &lt;strong&gt;JSON-LD&lt;/strong&gt;, placed in a &lt;code&gt;&amp;lt;script type="application/ld+json"&amp;gt;&lt;/code&gt; block. The templates below use only properties documented by Google or schema.org. Replace every value in angle brackets, and delete any property you cannot fill truthfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  Organization (site-wide, usually on the homepage)
&lt;/h2&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;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&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;"Organization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;Legal or trading name&amp;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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;your-domain&amp;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;"logo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;your-domain&amp;gt;/&amp;lt;path-to-logo&amp;gt;.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sameAs"&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="s2"&gt;"https://www.linkedin.com/company/&amp;lt;handle&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"https://www.instagram.com/&amp;lt;handle&amp;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;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;logo&lt;/code&gt; should be a crawlable image URL. Google documents its logo requirements in the Organization structured data guidelines.&lt;/li&gt;
&lt;li&gt;Only list profiles in &lt;code&gt;sameAs&lt;/code&gt; that you actually control.&lt;/li&gt;
&lt;li&gt;Only add &lt;code&gt;address&lt;/code&gt; or &lt;code&gt;telephone&lt;/code&gt; if they are real and match what is shown on the page. Placeholder contact details in markup are worse than none.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Article (blog posts and guides)
&lt;/h2&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;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&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;"Article"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"headline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;Title as shown on the page&amp;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;"datePublished"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;YYYY-MM-DD&amp;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;"dateModified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;YYYY-MM-DD&amp;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;"author"&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;"@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;"Person"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;Author name&amp;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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;your-domain&amp;gt;/&amp;lt;author-page&amp;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;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;your-domain&amp;gt;/&amp;lt;hero-image&amp;gt;.jpg"&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;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dateModified&lt;/code&gt; should change only when the content meaningfully changes.&lt;/li&gt;
&lt;li&gt;If the author is the organisation rather than a person, use &lt;code&gt;"@type": "Organization"&lt;/code&gt; for the author.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  BreadcrumbList
&lt;/h2&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;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&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;"BreadcrumbList"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"itemListElement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@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;"ListItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Home"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;your-domain&amp;gt;/"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@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;"ListItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Services"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;your-domain&amp;gt;/services"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@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;"ListItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;Current page&amp;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 last item may omit &lt;code&gt;item&lt;/code&gt;. The trail should match the breadcrumb that users see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Service (describing what you offer on a service page)
&lt;/h2&gt;

&lt;p&gt;Google does not currently show a dedicated rich result for &lt;code&gt;Service&lt;/code&gt;, but the type is valid schema.org vocabulary and helps describe a page unambiguously.&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;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&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;"Service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;Service name&amp;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;"serviceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;e.g. Search engine optimisation&amp;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;"provider"&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;"@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;"Organization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;Your organisation&amp;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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;your-domain&amp;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;"areaServed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;Country or region&amp;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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;your-domain&amp;gt;/&amp;lt;service-page&amp;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;h2&gt;
  
  
  Types to handle with care
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FAQPage&lt;/strong&gt;: since 2023 Google shows FAQ rich results only for a limited set of well-known, authoritative government and health sites. Marking up a genuine FAQ is still valid, but do not expect a visual result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HowTo&lt;/strong&gt;: HowTo rich results have been deprecated in Google Search.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review / AggregateRating&lt;/strong&gt;: self-serving reviews of your own business are not eligible for review snippets. Never mark up testimonials you cannot substantiate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Validation workflow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Validate syntax with the &lt;a href="https://validator.schema.org/" rel="noopener noreferrer"&gt;Schema Markup Validator&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Check eligibility with Google's &lt;a href="https://search.google.com/test/rich-results" rel="noopener noreferrer"&gt;Rich Results Test&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;After deployment, watch the enhancement reports in Search Console for errors that appear at scale.&lt;/li&gt;
&lt;li&gt;Keep markup in the template, generated from the same data as the visible page, so the two cannot drift apart.&lt;/li&gt;
&lt;/ol&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/appearance/structured-data/intro-structured-data" rel="noopener noreferrer"&gt;Introduction to structured data&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Search Central: &lt;a href="https://developers.google.com/search/docs/appearance/structured-data/sd-policies" rel="noopener noreferrer"&gt;Structured data general guidelines&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Structured data is one small part of search visibility. For how it fits alongside content, authority and AI-search optimisation, see Gorilla Vibe's &lt;a href="https://thegorillavibe.com/seo" rel="noopener noreferrer"&gt;SEO and authority growth&lt;/a&gt; overview.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>html</category>
      <category>json</category>
    </item>
    <item>
      <title>A Technical SEO Crawl and Index Checklist for Developers</title>
      <dc:creator>rokya elbarbary</dc:creator>
      <pubDate>Tue, 29 Sep 2026 12:35:08 +0000</pubDate>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da/a-technical-seo-crawl-and-index-checklist-for-developers-4bpm</link>
      <guid>https://dev.to/rokya_elbarbary_8e9bbb0da/a-technical-seo-crawl-and-index-checklist-for-developers-4bpm</guid>
      <description>&lt;p&gt;Search visibility starts with three separate questions, and a page can fail any one of them independently:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Can a search engine discover and fetch the URL?&lt;/strong&gt; (crawling)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can it see the content once fetched?&lt;/strong&gt; (rendering)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does it choose to keep the page in its index, and under which URL?&lt;/strong&gt; (indexing and canonicalisation)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Work through the checklist in that order. There is little point tuning titles on a page that is blocked in &lt;code&gt;robots.txt&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Crawling
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;How to verify&lt;/th&gt;
&lt;th&gt;What good looks like&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;robots.txt&lt;/code&gt; is reachable&lt;/td&gt;
&lt;td&gt;Request &lt;code&gt;/robots.txt&lt;/code&gt; directly&lt;/td&gt;
&lt;td&gt;Returns HTTP 200 (or 404 if you intentionally have none); never a 5xx, which can make Google pause crawling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Important paths are not disallowed&lt;/td&gt;
&lt;td&gt;Read every &lt;code&gt;Disallow&lt;/code&gt; rule; test key URLs in Search Console's robots.txt report&lt;/td&gt;
&lt;td&gt;Templates, CSS and JS needed for rendering are crawlable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XML sitemap exists and is referenced&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Sitemap:&lt;/code&gt; line in &lt;code&gt;robots.txt&lt;/code&gt;; submitted in Search Console&lt;/td&gt;
&lt;td&gt;Lists only canonical, indexable URLs that return 200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal links use real anchors&lt;/td&gt;
&lt;td&gt;Inspect navigation and pagination&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;a href="..."&amp;gt;&lt;/code&gt; elements, not click handlers on &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No redirect chains on internal links&lt;/td&gt;
&lt;td&gt;Crawl the site with a crawler of your choice&lt;/td&gt;
&lt;td&gt;Internal links point straight at the final URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server responds consistently&lt;/td&gt;
&lt;td&gt;Check server logs for Googlebot requests&lt;/td&gt;
&lt;td&gt;Stable 200 responses; no bursts of 429 or 5xx&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  2. Rendering
&lt;/h2&gt;

&lt;p&gt;Modern sites often build content in the browser. Google can render JavaScript, but rendering happens after the initial fetch and depends on every resource loading successfully.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compare &lt;strong&gt;view-source&lt;/strong&gt; with the &lt;strong&gt;rendered DOM&lt;/strong&gt; (URL Inspection tool, "View crawled page"). Primary content, headings and internal links should exist in the rendered HTML.&lt;/li&gt;
&lt;li&gt;Do not block the JavaScript or CSS files your templates depend on.&lt;/li&gt;
&lt;li&gt;Avoid content that only appears after a user interaction (clicking a tab, scrolling an infinite list) if you need it indexed. Put it in the HTML or render it server-side.&lt;/li&gt;
&lt;li&gt;Lazy-loaded images should use native &lt;code&gt;loading="lazy"&lt;/code&gt; or a method that exposes the image URL in the rendered DOM.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Indexing and canonicalisation
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;What to look for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;meta robots&lt;/code&gt; and &lt;code&gt;X-Robots-Tag&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No accidental &lt;code&gt;noindex&lt;/code&gt; left over from staging; check both the HTML and the HTTP headers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canonical tags&lt;/td&gt;
&lt;td&gt;One &lt;code&gt;rel="canonical"&lt;/code&gt; per page, absolute URL, pointing at a 200, indexable URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate variants&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;http&lt;/code&gt; vs &lt;code&gt;https&lt;/code&gt;, &lt;code&gt;www&lt;/code&gt; vs non-&lt;code&gt;www&lt;/code&gt;, trailing slash, uppercase, tracking parameters - each should resolve to one canonical version via redirect or canonical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Status codes&lt;/td&gt;
&lt;td&gt;Removed pages return 404 or 410; moved pages return 301 to the closest equivalent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Soft 404s&lt;/td&gt;
&lt;td&gt;Thin "no results" or empty category pages returning 200 - either add content or return 404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language versions&lt;/td&gt;
&lt;td&gt;If you publish Arabic and English versions, use &lt;code&gt;hreflang&lt;/code&gt; annotations that are reciprocal and point at canonical URLs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  4. Page-level basics
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A unique, descriptive &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and meta description on every indexable template. Placeholder descriptions left over from a CMS theme are a common and easily fixed problem.&lt;/li&gt;
&lt;li&gt;Exactly one visible H1 that describes the page. Duplicate or placeholder headings (for example a leftover "Heading" block) confuse users and are worth removing.&lt;/li&gt;
&lt;li&gt;Descriptive internal anchor text rather than "click here".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. After launch
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Submit the sitemap and inspect a sample of key URLs in Search Console.&lt;/li&gt;
&lt;li&gt;Watch the &lt;strong&gt;Pages&lt;/strong&gt; (indexing) report for spikes in "Crawled - currently not indexed", "Duplicate without user-selected canonical" or "Blocked by robots.txt".&lt;/li&gt;
&lt;li&gt;Re-run the crawl after every major release. Technical SEO regressions usually arrive with deployments, not with content.&lt;/li&gt;
&lt;/ol&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/how-search-works" rel="noopener noreferrer"&gt;How Google Search works&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Search Central: &lt;a href="https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics" rel="noopener noreferrer"&gt;JavaScript SEO basics&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;For a longer discussion of why this work is ongoing infrastructure rather than a one-off fix, see &lt;a href="https://thegorillavibe.com/technical-seo" rel="noopener noreferrer"&gt;Technical SEO in 2026: Building the Infrastructure for Organic Revenue&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>checklist</category>
    </item>
    <item>
      <title>A GA4 Event Naming Convention Your Whole Team Can Follow</title>
      <dc:creator>rokya elbarbary</dc:creator>
      <pubDate>Tue, 29 Sep 2026 12:31:49 +0000</pubDate>
      <link>https://dev.to/rokya_elbarbary_8e9bbb0da/a-ga4-event-naming-convention-your-whole-team-can-follow-2h68</link>
      <guid>https://dev.to/rokya_elbarbary_8e9bbb0da/a-ga4-event-naming-convention-your-whole-team-can-follow-2h68</guid>
      <description>&lt;p&gt;Google Analytics 4 lets anyone who can edit a tag create a new event name. That flexibility is useful for about six months, until the property contains &lt;code&gt;Form_Submit&lt;/code&gt;, &lt;code&gt;form_submit&lt;/code&gt;, &lt;code&gt;formSubmit&lt;/code&gt; and &lt;code&gt;lead_form_sent&lt;/code&gt; - all meaning the same thing and none of them comparable over time.&lt;/p&gt;

&lt;p&gt;A written naming convention, agreed before tracking is implemented, prevents that. This note is a starting point you can adapt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules that come from GA4 itself
&lt;/h2&gt;

&lt;p&gt;These are platform constraints documented by Google, not preferences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event names are &lt;strong&gt;case sensitive&lt;/strong&gt;. &lt;code&gt;sign_up&lt;/code&gt; and &lt;code&gt;Sign_Up&lt;/code&gt; are two different events.&lt;/li&gt;
&lt;li&gt;Event names must &lt;strong&gt;start with a letter&lt;/strong&gt; and use only letters, numbers and underscores.&lt;/li&gt;
&lt;li&gt;Event names are limited to &lt;strong&gt;40 characters&lt;/strong&gt;; parameter names to &lt;strong&gt;40 characters&lt;/strong&gt;; parameter values to &lt;strong&gt;100 characters&lt;/strong&gt; in a standard property.&lt;/li&gt;
&lt;li&gt;Some names and prefixes are &lt;strong&gt;reserved&lt;/strong&gt; (for example names starting with &lt;code&gt;google_&lt;/code&gt;, &lt;code&gt;ga_&lt;/code&gt; or &lt;code&gt;firebase_&lt;/code&gt;, and automatically collected event names). Do not reuse them for custom events.&lt;/li&gt;
&lt;li&gt;An event can carry &lt;strong&gt;up to 25 parameters&lt;/strong&gt;. Parameters only become available in standard reports once registered as &lt;strong&gt;custom dimensions or metrics&lt;/strong&gt;, and a standard property has a limited number of each - so register deliberately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check the current limits in Google's documentation before a large implementation; they occasionally change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our conventions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Example&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;
&lt;code&gt;snake_case&lt;/code&gt;, all lowercase&lt;/td&gt;
&lt;td&gt;&lt;code&gt;generate_lead&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Matches Google's recommended events; avoids case duplicates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use Google's &lt;strong&gt;recommended event&lt;/strong&gt; when one exists&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sign_up&lt;/code&gt;, &lt;code&gt;login&lt;/code&gt;, &lt;code&gt;generate_lead&lt;/code&gt;, &lt;code&gt;purchase&lt;/code&gt;, &lt;code&gt;view_item&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Unlocks built-in reports and keeps you compatible with future features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom events: &lt;code&gt;object_action&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;quote_request&lt;/code&gt;, &lt;code&gt;calculator_complete&lt;/code&gt;, &lt;code&gt;video_progress&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Readable in reports, sorts related events together&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Put detail in &lt;strong&gt;parameters&lt;/strong&gt;, not in the event name&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;generate_lead&lt;/code&gt; with &lt;code&gt;form_id=contact_footer&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Avoids hundreds of near-identical event names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No personal data in names or parameters&lt;/td&gt;
&lt;td&gt;Never send email addresses, phone numbers or names&lt;/td&gt;
&lt;td&gt;Google's policies prohibit sending PII to Analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parameter values are stable identifiers&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;form_id=pricing_modal&lt;/code&gt; not &lt;code&gt;form_id=Pricing Modal (new!)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Values survive design changes and translations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  A minimal event dictionary
&lt;/h2&gt;

&lt;p&gt;Keep a single table - in a spreadsheet or in your repository - that is the source of truth for every event. For example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event name&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Parameters&lt;/th&gt;
&lt;th&gt;Key event?&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;generate_lead&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Successful form submission (after server confirms)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;form_id&lt;/code&gt;, &lt;code&gt;form_location&lt;/code&gt;, &lt;code&gt;lead_type&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Marketing ops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;book_call_click&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Click on any "book a call" button&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;button_location&lt;/code&gt;, &lt;code&gt;page_type&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Marketing ops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;calculator_complete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;User reaches the result step of a calculator&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;calculator_id&lt;/code&gt;, &lt;code&gt;result_band&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;file_download&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enhanced measurement (automatic)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;file_name&lt;/code&gt;, &lt;code&gt;file_extension&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A dictionary like this also makes QA possible: the tester checks the property against the table instead of against somebody's memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Write the event dictionary before touching Google Tag Manager or the codebase.&lt;/li&gt;
&lt;li&gt;Fire lead events on &lt;strong&gt;confirmed success&lt;/strong&gt; (a thank-you state or a server response), not on button click, or you will count failed submissions.&lt;/li&gt;
&lt;li&gt;Use GTM's preview mode and GA4 &lt;strong&gt;DebugView&lt;/strong&gt; to confirm names and parameter values.&lt;/li&gt;
&lt;li&gt;Register only the parameters you will actually report on as custom dimensions.&lt;/li&gt;
&lt;li&gt;Mark the few events that represent real business outcomes as &lt;strong&gt;key events&lt;/strong&gt;; do not mark every click.&lt;/li&gt;
&lt;li&gt;Review the dictionary each quarter and remove events nobody uses.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Common problems in audits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Duplicate events caused by a tag firing both from GTM and from hard-coded &lt;code&gt;gtag.js&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enhanced measurement form interactions double counting a custom lead event.&lt;/li&gt;
&lt;li&gt;Parameters sent but never registered, so the data exists only in BigQuery exports.&lt;/li&gt;
&lt;li&gt;Cross-domain journeys (for example a booking tool on another domain) breaking sessions and attributing leads to "referral".&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Google Analytics Help: &lt;a href="https://support.google.com/analytics/answer/9267735" rel="noopener noreferrer"&gt;Recommended events&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Analytics Help: &lt;a href="https://support.google.com/analytics/answer/13316687" rel="noopener noreferrer"&gt;Event naming rules&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Tracking reviews like this are often the first part of a wider marketing audit. Gorilla Vibe describes that format on its &lt;a href="https://thegorillavibe.com/audits-sprints-special-ops" rel="noopener noreferrer"&gt;audits, sprints and special ops&lt;/a&gt; page.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>analytics</category>
      <category>webdev</category>
      <category>marketing</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
