<?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: Yuto Tanaka</title>
    <description>The latest articles on DEV Community by Yuto Tanaka (@yutotanaka).</description>
    <link>https://dev.to/yutotanaka</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%2F4151440%2F28512d68-7c50-4412-baf3-4d4dbcb39fc7.jpg</url>
      <title>DEV Community: Yuto Tanaka</title>
      <link>https://dev.to/yutotanaka</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yutotanaka"/>
    <language>en</language>
    <item>
      <title>Client Branded Links Stopped Working After a DNS Change: What to Check</title>
      <dc:creator>Yuto Tanaka</dc:creator>
      <pubDate>Sun, 01 Nov 2026 17:28:00 +0000</pubDate>
      <link>https://dev.to/yutotanaka/client-branded-links-stopped-working-after-a-dns-change-what-to-check-2oje</link>
      <guid>https://dev.to/yutotanaka/client-branded-links-stopped-working-after-a-dns-change-what-to-check-2oje</guid>
      <description>&lt;p&gt;A branded short link can look like a DNS problem when it is actually a TLS problem, and vice versa. After a nameserver change, a CDN migration, or a zone edit, &lt;code&gt;links.client.com&lt;/code&gt; may fail to resolve, resolve to the wrong target, return a certificate error, or redirect to a default page. The fastest way to narrow it down is to check the layers in order: authoritative DNS, TLS, HTTP redirects, and the link service's domain settings.&lt;/p&gt;

&lt;p&gt;This checklist assumes the client controls &lt;code&gt;client.com&lt;/code&gt; and uses a subdomain such as &lt;code&gt;links.client.com&lt;/code&gt; for branded links. The same sequence works for an apex domain, but the DNS constraints are different, so that case gets its own note.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the authoritative DNS answer
&lt;/h2&gt;

&lt;p&gt;Do not begin in the browser. Browser caches, OS caches, and public resolver caches can all hide the current state. Query DNS directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short links.client.com
dig +short links.client.com CNAME
dig +short links.client.com A
dig +short links.client.com AAAA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you want to see depends on how the link service is set up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;CNAME&lt;/code&gt; pointing to the provider target, usually with a trailing dot such as &lt;code&gt;links.provider.example.&lt;/code&gt;. The trailing dot is normal in DNS output.&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;A&lt;/code&gt; or &lt;code&gt;AAAA&lt;/code&gt; record if the provider uses CNAME flattening, a proxy, or a direct IP target.&lt;/li&gt;
&lt;li&gt;No conflicting records at the same name. A CNAME cannot coexist with an &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;AAAA&lt;/code&gt;, &lt;code&gt;TXT&lt;/code&gt;, or other data at the same owner name.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If &lt;code&gt;dig&lt;/code&gt; returns a CNAME but no final address, follow the target. The provider target may be missing, misspelled, or itself broken. If it returns an old IP address from before the migration, the zone change may not have been applied where you think it was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the delegation, not just the record
&lt;/h2&gt;

&lt;p&gt;A common failure is updating records at the old DNS host while the domain is delegated to different nameservers. Look at the delegation and the path resolution takes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig NS client.com +short
dig +trace links.client.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;dig NS&lt;/code&gt; shows the nameservers currently advertised for the zone. If those are not the ones you edited, the records you changed are irrelevant. Update the registrar or the active DNS provider instead.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dig +trace&lt;/code&gt; shows where resolution diverges: at the root, the TLD, the zone's nameservers, or the final target. It is especially useful when one resolver gives the right answer and another gives an old one. If you see &lt;code&gt;SERVFAIL&lt;/code&gt; and DNSSEC is enabled, check that the DS record at the registrar matches the new DNS provider. A DNSSEC mismatch can make the whole zone fail even when the records themselves look correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle apex domains differently
&lt;/h2&gt;

&lt;p&gt;If the branded host is the apex, such as &lt;code&gt;client.com&lt;/code&gt; rather than &lt;code&gt;links.client.com&lt;/code&gt;, a normal CNAME is not valid alongside the NS and SOA records every zone needs. Some DNS providers offer ALIAS, ANAME, or CNAME flattening at the apex; others require A and AAAA records, or a redirect at the HTTP layer.&lt;/p&gt;

&lt;p&gt;Do not assume the provider's flattening behavior is visible in the dashboard. Check what public resolvers actually return:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short client.com CNAME
dig +short client.com A
dig +short client.com AAAA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the answer changes depending on the resolver, or if the provider returns a mixture of records, treat that as a configuration problem rather than a propagation delay. Also check that a TXT or verification record was not removed when the CNAME was added.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check TLS before blaming the redirect
&lt;/h2&gt;

&lt;p&gt;DNS can be correct and the link can still fail at HTTPS. A new CDN or reverse proxy may answer for the hostname but not have a certificate that covers it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-Iv&lt;/span&gt; https://links.client.com/abc 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'1,40p'&lt;/span&gt;
openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; links.client.com:443 &lt;span class="nt"&gt;-servername&lt;/span&gt; links.client.com &lt;span class="nt"&gt;-showcerts&lt;/span&gt; &amp;lt;/dev/null 2&amp;gt;/dev/null | openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-subject&lt;/span&gt; &lt;span class="nt"&gt;-issuer&lt;/span&gt; &lt;span class="nt"&gt;-dates&lt;/span&gt; &lt;span class="nt"&gt;-ext&lt;/span&gt; subjectAltName
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The certificate is valid for &lt;code&gt;links.client.com&lt;/code&gt;, not only for the provider's default hostname.&lt;/li&gt;
&lt;li&gt;The certificate chain is complete.&lt;/li&gt;
&lt;li&gt;The server negotiates TLS with the correct SNI name. &lt;code&gt;openssl s_client&lt;/code&gt; with &lt;code&gt;-servername&lt;/code&gt; tests that directly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the DNS target changed to a CDN, the CDN usually needs the custom hostname added and validated before it will serve the right certificate. If the site uses HSTS, a broken HTTPS response will not fall back to HTTP; the browser will fail hard. Test with &lt;code&gt;curl&lt;/code&gt; first so you see the actual error instead of a browser-generated message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Follow the full redirect chain
&lt;/h2&gt;

&lt;p&gt;A branded short link is usually a redirector. DNS gets the client to the redirector; the redirector then sends the client to the destination. Both halves can break.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code} %{url_effective}'&lt;/span&gt; https://links.client.com/abc
curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code} %{url_effective}'&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; https://links.client.com/abc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first command shows the first response. The second follows redirects and prints the final URL. Check for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;301&lt;/code&gt;, &lt;code&gt;302&lt;/code&gt;, &lt;code&gt;307&lt;/code&gt;, or &lt;code&gt;308&lt;/code&gt; to the expected destination.&lt;/li&gt;
&lt;li&gt;A redirect loop between HTTP and HTTPS, &lt;code&gt;www&lt;/code&gt; and non-&lt;code&gt;www&lt;/code&gt;, or two hostnames.&lt;/li&gt;
&lt;li&gt;A path or query string that is dropped during the redirect.&lt;/li&gt;
&lt;li&gt;A redirect to a default page, which often means the hostname is recognized but the short-link path is not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While testing, prefer temporary redirects over permanent ones if you control the redirect layer. Browsers cache &lt;code&gt;301&lt;/code&gt; and &lt;code&gt;308&lt;/code&gt; aggressively, and a bad permanent redirect can keep failing after DNS is fixed. Clear the browser cache or test with &lt;code&gt;curl&lt;/code&gt; from a clean environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the Host header and application routing
&lt;/h2&gt;

&lt;p&gt;If DNS points to the right server but the response is a generic 404, 403, or default site, the server may not recognize the hostname. Test the IP directly with the expected &lt;code&gt;Host&lt;/code&gt; header.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; http://&amp;lt;new-ip&amp;gt;/abc &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Host: links.client.com'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns the link, DNS is doing its job and the problem is in virtual-host, TLS, or proxy routing. For an nginx-style reverse proxy, the relevant configuration usually looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;links.client.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://link-service.internal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact configuration depends on the stack. The point is to verify that the hostname reaches the application, not just the server's IP address. A missing &lt;code&gt;server_name&lt;/code&gt;, a missing certificate, or a proxy that rewrites the &lt;code&gt;Host&lt;/code&gt; header can all produce symptoms that look like DNS failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the link service's domain state
&lt;/h2&gt;

&lt;p&gt;DNS only routes traffic. The link shortener or redirect service also has to recognize the branded hostname. Check its domain settings for the hostname status, verification records, certificate status, and any default destination or path prefix.&lt;/p&gt;

&lt;p&gt;Depending on the service, a domain may need a TXT record, a CNAME, or both before it becomes active. If the domain was removed, left pending, or re-added under a different project, links on that host may return a 404 or fall back to a generic page even though DNS resolves correctly. This is a configuration layer, not a DNS layer, and it is easy to miss after a migration.&lt;/p&gt;

&lt;p&gt;To separate a client-domain problem from a general outbound-network problem, test a short URL on a different host from the same machine, such as &lt;a href="https://linkbit.live/xzuy3zv" rel="noopener noreferrer"&gt;LinkBit&lt;/a&gt;. If that resolves and redirects while &lt;code&gt;links.client.com&lt;/code&gt; does not, focus on the client hostname, its certificate, and its link-service configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Account for TTLs and negative caching
&lt;/h2&gt;

&lt;p&gt;If the hostname returned &lt;code&gt;NXDOMAIN&lt;/code&gt; or &lt;code&gt;SERVFAIL&lt;/code&gt; during the change, resolvers may cache that negative answer. Fixing the record does not always make the error disappear immediately. Check the zone's SOA and query multiple public resolvers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig links.client.com SOA
dig @1.1.1.1 links.client.com
dig @8.8.8.8 links.client.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also check from outside the client's network. Split-horizon DNS can make an internal resolver return an old internal address while public resolvers return the new target. If the link works in the office and fails for customers, internal DNS is a likely difference.&lt;/p&gt;

&lt;p&gt;Before future changes, lower the TTL on the branded hostname well in advance. Keep the old target alive until the TTL has expired, and avoid changing nameservers and records at the same time unless you can watch both layers closely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes that look like DNS failures
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Updating records at the old DNS provider after the domain was delegated elsewhere.&lt;/li&gt;
&lt;li&gt;Adding a CNAME and an A record at the same name.&lt;/li&gt;
&lt;li&gt;Using a plain CNAME at the apex without provider flattening.&lt;/li&gt;
&lt;li&gt;Pointing DNS at a CDN but not adding the custom hostname or certificate there.&lt;/li&gt;
&lt;li&gt;Forgetting to recreate a verification TXT record after a zone migration.&lt;/li&gt;
&lt;li&gt;Testing only from an internal network with split-horizon DNS.&lt;/li&gt;
&lt;li&gt;Trusting a browser after a permanent redirect has been cached.&lt;/li&gt;
&lt;li&gt;Changing DNSSEC settings without matching the DS record at the registrar.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The order that saves time is authoritative DNS first, then TLS, then redirects, then the link service's domain state. If the authoritative answer is right and the certificate is valid, the problem is rarely still DNS. It is usually the layer that was changed next.&lt;/p&gt;

&lt;h1&gt;
  
  
  DNS #WebDev #DevOps #Troubleshooting #Networking
&lt;/h1&gt;

</description>
      <category>devops</category>
      <category>infrastructure</category>
      <category>networking</category>
    </item>
    <item>
      <title>How to Track Google Ads Traffic to Amazon Listings with Amazon Attribution</title>
      <dc:creator>Yuto Tanaka</dc:creator>
      <pubDate>Sun, 01 Nov 2026 17:25:10 +0000</pubDate>
      <link>https://dev.to/yutotanaka/how-to-track-google-ads-traffic-to-amazon-listings-with-amazon-attribution-3pbf</link>
      <guid>https://dev.to/yutotanaka/how-to-track-google-ads-traffic-to-amazon-listings-with-amazon-attribution-3pbf</guid>
      <description>&lt;p&gt;Amazon detail pages don't run your JavaScript. There's no Google Ads conversion tag you can drop onto an ASIN, and no &lt;code&gt;gtag&lt;/code&gt; call after Add to Cart. If you send paid traffic from Google Ads to an Amazon listing, the conversion happens on a page you don't own, which is the exact problem Amazon Attribution exists to solve.&lt;/p&gt;

&lt;p&gt;This is a wiring problem.&lt;/p&gt;

&lt;p&gt;You need a tagged Amazon URL, a click path that doesn't mangle it, and a campaign structure that lets you join Amazon's conversion data back to Google Ads spend.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Amazon Attribution actually measures a click
&lt;/h2&gt;

&lt;p&gt;Amazon Attribution works on a link, not a pixel. You create a campaign, ad group, and creative inside Amazon Attribution, and Amazon issues a URL for the target ASIN with a &lt;code&gt;maas&lt;/code&gt; parameter attached:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://www.amazon.com/dp/B0C1234XYZ?maas=maas_adg_api_9876543210&amp;amp;ref_=aa_maas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a user lands on Amazon with that parameter, Amazon records a click against the matching creative. Subsequent events (detail page views, add-to-cart, purchases, units, sales) get attributed to that creative inside Amazon's attribution window. (Check the current Amazon Attribution documentation for the window length; it has historically been 14 days for clicks.)&lt;/p&gt;

&lt;p&gt;Two implications for engineers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;maas&lt;/code&gt; value is the entire identity of the click. Lose it and the visit becomes an unattributed organic session.&lt;/li&gt;
&lt;li&gt;You never see the click on your side. Amazon doesn't call your endpoint back with a conversion, so your only join key is the campaign / ad group / creative structure you build on both platforms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't hand-construct the &lt;code&gt;maas&lt;/code&gt; ID. Copy it from the Attribution console, or generate it through the Amazon Ads API, which exposes Attribution campaign, ad group, and creative resources along with reporting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites you can't route around
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An Amazon Ads account with Amazon Attribution access (sellers generally need Brand Registry; vendors get access through Vendor Central).&lt;/li&gt;
&lt;li&gt;The tag and the listing must live in the same marketplace. An &lt;code&gt;amazon.com&lt;/code&gt; tag pointing at &lt;code&gt;amazon.co.uk&lt;/code&gt; will not attribute.&lt;/li&gt;
&lt;li&gt;Google Ads auto-tagging on, if you want &lt;code&gt;gclid&lt;/code&gt; for cross-checking click volume later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wire the tag into Google Ads
&lt;/h2&gt;

&lt;p&gt;There are two ways to get the &lt;code&gt;maas&lt;/code&gt; parameter onto the click.&lt;/p&gt;

&lt;p&gt;Option A: tagged URL as the final URL. Paste the full tagged Amazon URL into the ad's final URL. It works, and it's the fastest thing to ship. The downside: you get no visibility into your own click path, and every ad carries a long parameterized URL that's easy to break when someone copies it without thinking.&lt;/p&gt;

&lt;p&gt;Option B: tracking template plus a redirect you control. The final URL stays the clean listing URL; the tracking template points at your own endpoint, which logs the click and 302s to the tagged URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://track.example.com/go/us-brand-search?campaignid={campaignid}&amp;amp;adgroupid={adgroupid}&amp;amp;creative={creative}&amp;amp;gclid={gclid}&amp;amp;lp={lpurl}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a minimal handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// maas tags issued by Amazon Attribution — one per creative you want to measure.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAAS_BY_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;us-brand-search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;maas_adg_api_9876543210&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/go/:key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAAS_BY_KEY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lpurl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;maas&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;lpurl&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Do not turn this into an open redirect.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lpurl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;www.amazon.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Express already decoded the query value; searchParams.set() re-encodes once.&lt;/span&gt;
  &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;maas&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maas&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ref_&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aa_maas&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;gclid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gclid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;campaignId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;campaignid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;302&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules that matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use 302, not 301. A permanent redirect gets cached by the browser and you lose clicks you can't see.&lt;/li&gt;
&lt;li&gt;Keep the chain to one hop. Every extra redirect adds latency before the Amazon page starts loading, and each hop is another chance for a proxy to strip unknown query parameters.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{lpurl}&lt;/code&gt; arrives percent-encoded as a parameter value, and Express decodes it for you. Don't call &lt;code&gt;decodeURIComponent&lt;/code&gt; a second time. A double decode corrupts any &lt;code&gt;%&lt;/code&gt; sequence in the original URL and can produce a mangled query string.&lt;/li&gt;
&lt;li&gt;If you're appending the tag by hand and the destination already has a query string, use &lt;code&gt;&amp;amp;&lt;/code&gt;, not &lt;code&gt;?&lt;/code&gt;. A second &lt;code&gt;?&lt;/code&gt; silently truncates everything after it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use campaign structure as your join key
&lt;/h2&gt;

&lt;p&gt;Amazon Attribution reports break down by campaign → ad group → creative. Google Ads reports break down by campaign → ad group → ad. Mirror them one-to-one and the join becomes trivial:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Google Ads&lt;/th&gt;
&lt;th&gt;Amazon Attribution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Campaign&lt;/td&gt;
&lt;td&gt;Campaign&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ad group&lt;/td&gt;
&lt;td&gt;Ad group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ad&lt;/td&gt;
&lt;td&gt;Creative&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Give each level a stable, machine-readable name (such as &lt;code&gt;gads-us-brand-exact&lt;/code&gt; or &lt;code&gt;gads-us-brand-exact-b0c1234xyz&lt;/code&gt;) rather than "Test 2" and "Test 2 copy". You'll be joining CSVs in a script or a spreadsheet, and descriptive names are the difference between a five-minute join and an afternoon of guessing which row is which.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate before you scale
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Click your ad (or hit the tracking URL directly) in a normal browser with DevTools open, and confirm the redirect chain ends on Amazon with &lt;code&gt;maas&lt;/code&gt; intact.&lt;/li&gt;
&lt;li&gt;Check the Attribution console for the click within a few hours. Clicks usually land faster than conversions.&lt;/li&gt;
&lt;li&gt;Cross-check Google Ads click counts against Amazon Attribution clicks after a day. They will not match exactly: Google filters invalid traffic, Amazon filters too, and some users never finish the redirect. Large gaps point at a broken tag; small gaps are normal noise.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Failure modes worth knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A tag can be stripped by an intermediary. URL shorteners, link cleaners, and some ad platforms drop parameters they don't recognise. Test every hop you introduce.&lt;/li&gt;
&lt;li&gt;Amazon may route a click to a different ASIN than the one you tagged for size or colour variants. Attribution still works, but your expected-ASIN report won't line up.&lt;/li&gt;
&lt;li&gt;Amazon's app-link handling can open the listing inside the app. Parameter behaviour there isn't under your control, so judge mobile performance by reported numbers rather than by a desktop click test.&lt;/li&gt;
&lt;li&gt;Ad blockers and privacy extensions can block long parameterized redirect chains more often than a plain Amazon link.&lt;/li&gt;
&lt;li&gt;Wrong marketplace: covered above, and still the most common cause of "no data at all".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Joining spend to conversions
&lt;/h2&gt;

&lt;p&gt;Pull the Amazon Attribution report on a schedule (console export or Ads API), pull spend and clicks from the Google Ads API or a scheduled report, and join on date plus campaign name. The numbers you actually want are cost per detail page view, cost per purchase, and ROAS by campaign. Google Ads can never produce those figures for an Amazon destination because the conversion simply isn't visible to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  One note on link length
&lt;/h2&gt;

&lt;p&gt;If a tagged Amazon URL needs to live somewhere a 150-character parameterized link is awkward, such as a QR code on packaging, a slide, or a bio link, shorten the full tagged URL rather than the plain listing, then verify the &lt;code&gt;maas&lt;/code&gt; parameter survives the redirect before you print or ship anything. &lt;a href="https://linkbit.live/okkg78h" rel="noopener noreferrer"&gt;LinkBit&lt;/a&gt; is one option for that.&lt;/p&gt;

&lt;p&gt;The discipline stays the same regardless of tooling: one tag per ad, a click path you control, a naming scheme that survives a CSV join, and a click test after every change to the chain.&lt;/p&gt;

</description>
      <category>googleaichallenge</category>
      <category>amazonads</category>
      <category>ads</category>
      <category>attribution</category>
    </item>
    <item>
      <title>How to Disclose Affiliate Links in Social Posts When Using Short URLs</title>
      <dc:creator>Yuto Tanaka</dc:creator>
      <pubDate>Sun, 01 Nov 2026 11:44:00 +0000</pubDate>
      <link>https://dev.to/yutotanaka/how-to-disclose-affiliate-links-in-social-posts-when-using-short-urls-11ml</link>
      <guid>https://dev.to/yutotanaka/how-to-disclose-affiliate-links-in-social-posts-when-using-short-urls-11ml</guid>
      <description>&lt;p&gt;A short URL does two jobs at once: it makes a link fit a character limit, and it hides where the link goes. The first job is why you use it. The second is why disclosure gets harder the moment you swap a raw affiliate URL for a shortened one, and that part is easy to forget when you post from a script instead of by hand.&lt;/p&gt;

&lt;p&gt;The disclosure has to move into the post itself, and there are three ways to make sure it gets there: a text convention, an interstitial redirect that shows the disclosure before forwarding, and a CI check that fails the build when an affiliate link goes out unlabelled.&lt;/p&gt;

&lt;h2&gt;
  
  
  The disclosure signal a short URL deletes
&lt;/h2&gt;

&lt;p&gt;A raw affiliate URL usually announces itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://shop.example.com/pricing?plan=team&amp;amp;ref=yourname&amp;amp;utm_source=social
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ref=&lt;/code&gt; parameter is a tracking leak, but it does tell a careful reader that something is going on. Replace that URL with &lt;code&gt;https://sho.rt/spring-sale&lt;/code&gt; and the signal is gone. The reader sees an opaque code and a domain they may not recognise.&lt;/p&gt;

&lt;p&gt;Two other things disappear at the same time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rel&lt;/code&gt; attributes. On a blog you can mark an outbound link &lt;code&gt;rel="sponsored nofollow"&lt;/code&gt;. Social posts are plain text; there's no HTML to attach it to, and the platform may rewrite or re-shorten your URL anyway.&lt;/li&gt;
&lt;li&gt;The destination domain. Hovering a short link in a desktop client may show the short domain rather than the final page. On mobile there's nothing to hover.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The underlying requirement doesn't change. The FTC's Endorsement Guides (16 CFR Part 255) call for a clear and conspicuous disclosure of material connections; the UK's ASA/CMA rules take a similar line, and other jurisdictions have their own. In practice that means the disclosure has to be visible in the post itself, next to the link, in words a normal person reads as "this is an ad". Search-engine qualification via &lt;code&gt;rel="sponsored"&lt;/code&gt; is a separate concern and does nothing for a reader deciding whether to tap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: put the disclosure in the post text, before the link
&lt;/h2&gt;

&lt;p&gt;This is the baseline. Everything else is a supplement to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ad: I moved our schema migrations to Example and wrote up what broke.
https://sho.rt/spring-sale
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few conventions that hold up in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lead with the label. Use anything unambiguous: &lt;code&gt;Ad:&lt;/code&gt;, &lt;code&gt;#ad&lt;/code&gt;, &lt;code&gt;Affiliate link:&lt;/code&gt;. A label at the start gets seen; a hashtag buried in a block of tags does not.&lt;/li&gt;
&lt;li&gt;Put it before the link, not after. The reader should know what the link is before they commit to tapping it.&lt;/li&gt;
&lt;li&gt;Don't outsource it to your bio or a reply. A disclosure the reader has to go looking for isn't conspicuous.&lt;/li&gt;
&lt;li&gt;Don't let a hashtag block carry it. &lt;code&gt;#ad #dev #node #typescript&lt;/code&gt; reads as keyword stuffing, and the one meaningful tag disappears into the noise.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pattern 2: make the short link show the disclosure itself
&lt;/h2&gt;

&lt;p&gt;If you own the redirect, you can render a small disclosure page instead of forwarding instantly, and only forward when the visitor clicks through.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// links.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;links&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;spring-sale&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://shop.example.com/pricing?ref=yourname&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;affiliate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;docs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://shop.example.com/docs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;affiliate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;links&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./links.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/:code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;links&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unknown link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;affiliate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;302&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-Robots-Tag&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;noindex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang="en"&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset="utf-8"&amp;gt;
    &amp;lt;meta name="robots" content="noindex"&amp;gt;
    &amp;lt;meta property="og:title" content="Affiliate link"&amp;gt;
    &amp;lt;meta property="og:description" content="This is an affiliate link. I may earn a commission if you buy through it."&amp;gt;
    &amp;lt;title&amp;gt;Affiliate link&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;main&amp;gt;
      &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Affiliate link.&amp;lt;/strong&amp;gt; If you buy through this link I may earn a
      commission. The price you pay is the same.&amp;lt;/p&amp;gt;
      &amp;lt;p&amp;gt;&amp;lt;a href="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" rel="sponsored nofollow noopener"&amp;gt;Continue&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;/main&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes on the implementation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Escape &lt;code&gt;link.destination&lt;/code&gt; before interpolating it into HTML, or build the page with a template engine that escapes by default. The snippet above is short enough to read but not safe for untrusted input.&lt;/li&gt;
&lt;li&gt;Don't auto-redirect with &lt;code&gt;&amp;lt;meta http-equiv="refresh"&amp;gt;&lt;/code&gt;. An interstitial that forwards after zero seconds is a redirect with extra steps; the whole point is that the person sees the disclosure.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;rel="sponsored nofollow"&lt;/code&gt; on the outbound anchor. Inside your own HTML it's the one place the machine-readable signal still exists.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;noindex&lt;/code&gt;. A thin disclosure page shouldn't compete with the destination in search results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off is an extra tap, which costs you some clicks. If the link is the entire point of the post, pattern 1 is usually enough. Use the interstitial when the link travels on its own: in a bio, a newsletter, a QR code, anywhere the surrounding text may not follow it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: check what the preview card says
&lt;/h2&gt;

&lt;p&gt;Most social clients turn a posted URL into a preview card. If the card is what people see, the disclosure has to be in it. The &lt;code&gt;og:title&lt;/code&gt; and &lt;code&gt;og:description&lt;/code&gt; in the snippet above do that for the interstitial page. If the preview fetches the destination directly, you can't control the wording, which is another argument for not letting an affiliate URL be the thing that gets previewed.&lt;/p&gt;

&lt;p&gt;Test it: paste the short link into a private message or an unpublished draft and read what the card says before you publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate the check in CI
&lt;/h2&gt;

&lt;p&gt;Hand-checking every draft doesn't scale past a few posts a week. Because the redirector already knows which codes are affiliate links, the same &lt;code&gt;links.js&lt;/code&gt; map can drive a pre-publish check over your drafts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/check-disclosures.mjs&lt;/span&gt;
&lt;span class="c1"&gt;// Usage: node scripts/check-disclosures.mjs drafts/*.md&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;links&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../links.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SHORT_LINK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/https&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\/\/&lt;/span&gt;&lt;span class="sr"&gt;sho&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;rt&lt;/span&gt;&lt;span class="se"&gt;\/([&lt;/span&gt;&lt;span class="sr"&gt;A-Za-z0-9_-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DISCLOSURE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;#ad&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;ad:|&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;affiliate link&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;paid link&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;sponsored&lt;/span&gt;&lt;span class="se"&gt;\b)&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WINDOW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;140&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// characters of context required before the link&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SHORT_LINK&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;links&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="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;affiliate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;WINDOW&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;DISCLOSURE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; affiliate link without a nearby disclosure`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;process&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="nx"&gt;failed&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it into the same pipeline that lints your markdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check affiliate disclosures&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node scripts/check-disclosures.mjs drafts/*.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The window is deliberately crude. A regex can't judge whether a disclosure is clear and conspicuous, only whether one exists within a reasonable distance of the link. That's enough to catch the real failure mode, which is forgetting entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the redirect actually behaves
&lt;/h2&gt;

&lt;p&gt;After deploying, check headers rather than the rendered page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://sho.rt/spring-sale | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'^(HTTP/|location|x-robots-tag)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an affiliate code you want no &lt;code&gt;Location&lt;/code&gt; header, a &lt;code&gt;200&lt;/code&gt;, and &lt;code&gt;x-robots-tag: noindex&lt;/code&gt;. A &lt;code&gt;302&lt;/code&gt; means the affiliate branch isn't being hit, usually because the &lt;code&gt;affiliate&lt;/code&gt; flag is missing from the map.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge cases worth deciding in advance
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Character limits. An affiliate URL with tracking parameters can eat most of a post. Budget the disclosure first (&lt;code&gt;Ad:&lt;/code&gt; is four characters) and shorten what's left.&lt;/li&gt;
&lt;li&gt;Threads. Repeat the disclosure in every post that carries a link, not just the first one.&lt;/li&gt;
&lt;li&gt;Image-only posts. The disclosure belongs in the text of the post containing the image, not only in your profile.&lt;/li&gt;
&lt;li&gt;Quote posts and reposts. Your disclosure doesn't travel with someone else's quote of your link. Assume it's lost and don't rely on it.&lt;/li&gt;
&lt;li&gt;Multiple links. One clear disclosure at the top covering the whole post is normally fine; just make sure it isn't separated from the first link by a wall of text.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>affiliatemarketing</category>
      <category>webdev</category>
      <category>node</category>
      <category>socialmedia</category>
    </item>
    <item>
      <title>Should You A/B Test Your Call to Action or Your Landing Page First?</title>
      <dc:creator>Yuto Tanaka</dc:creator>
      <pubDate>Tue, 27 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/yutotanaka/should-you-ab-test-your-call-to-action-or-your-landing-page-first-410m</link>
      <guid>https://dev.to/yutotanaka/should-you-ab-test-your-call-to-action-or-your-landing-page-first-410m</guid>
      <description>&lt;p&gt;A campaign lands in your lap and the numbers are flat. The button says "Submit." The page is three paragraphs of feature copy above the fold. You have one experiment slot this sprint, one shared traffic budget, and two candidate changes that both look obviously wrong.&lt;/p&gt;

&lt;p&gt;So which one do you ship first?&lt;/p&gt;

&lt;p&gt;The useful answer is neither "test both" nor a coin flip. The two changes answer different questions, and one of them is usually wasted if you run it in the wrong order.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two problems look identical in a dashboard
&lt;/h2&gt;

&lt;p&gt;Both failures surface as the same line: campaign traffic arrives, signups don't happen. To separate them, split the funnel into two questions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the page earn attention? Can a first-time visitor understand the offer within the first screen?&lt;/li&gt;
&lt;li&gt;Does the ask earn commitment? Once the value is clear, is the action obvious, specific, and low-friction?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first question is about the landing page. The second is about the call to action. A dashboard that only tracks sessions and signups cannot tell you which one broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  A diagnostic table before you write any code
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the data shows&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Where to start&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Most sessions end within a few seconds; low scroll depth&lt;/td&gt;
&lt;td&gt;The page does not match the promise in the ad&lt;/td&gt;
&lt;td&gt;Landing page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deep scroll, pricing or docs section viewed, few form starts&lt;/td&gt;
&lt;td&gt;The ask is vague, buried, or costly&lt;/td&gt;
&lt;td&gt;Call to action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Many form starts, few completions&lt;/td&gt;
&lt;td&gt;Form friction&lt;/td&gt;
&lt;td&gt;Neither. Fix the form&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low engagement and low action, traffic source unclear&lt;/td&gt;
&lt;td&gt;Targeting or message mismatch upstream&lt;/td&gt;
&lt;td&gt;Neither. Fix the campaign&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row matters more than it looks. If the audience arriving from the campaign was never a fit, no amount of button rewording or hero-copy polishing will rescue the page. Fixing the targeting is cheaper than any experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the order matters more than the individual change
&lt;/h2&gt;

&lt;p&gt;A call to action is read in the context of the headline, the proof, the price, and the surrounding copy.&lt;/p&gt;

&lt;p&gt;That produces two ordering rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule one: a button cannot fix a page that never establishes value.&lt;/strong&gt; If visitors leave before they understand what they are being asked to do, a sharper label has nothing to attach to. You will measure noise and conclude that labels do not matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule two: do not optimize a component you are about to replace.&lt;/strong&gt; If a page rewrite is already on the roadmap for this quarter, running a careful label experiment on the current page is a throwaway. The result will not transfer to the new layout, because the context that produced it no longer exists.&lt;/p&gt;

&lt;p&gt;The second rule is the one teams break most often, because label experiments are fast to ship and feel productive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do the math before you commit to either
&lt;/h2&gt;

&lt;p&gt;A small change usually produces a small effect, and a small effect demands a large sample. A page rewrite can plausibly shift behavior by several points; a button label change often moves it by a fraction of one.&lt;/p&gt;

&lt;p&gt;A quick calculation, runnable in any Python environment, shows how steep that difference is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sessions_needed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_control&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_treatment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;power&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.80&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;z_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.959963985&lt;/span&gt;
    &lt;span class="n"&gt;z_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.8416212336&lt;/span&gt;
    &lt;span class="n"&gt;p_bar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_control&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p_treatment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;spread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z_a&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;p_bar&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p_bar&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
              &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;z_b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_control&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p_control&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p_treatment&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p_treatment&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spread&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_treatment&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p_control&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nf"&gt;sessions_needed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.04&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.045&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# a 0.5-point lift on a 4% baseline
&lt;/span&gt;&lt;span class="nf"&gt;sessions_needed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.04&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.06&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# a 2-point lift on the same baseline
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a 4% baseline, detecting a 0.5-point improvement at 80% power takes roughly 25,000 sessions per arm. Detecting a 2-point improvement takes roughly 1,900. Same campaign, same page, a thirteen-fold difference in required traffic.&lt;/p&gt;

&lt;p&gt;Two conclusions follow. First, if your campaign delivers a few thousand sessions a month, a micro-optimization of the button label is probably not measurable, no matter how good the label is. Second, the size of the expected effect should drive the choice of experiment far more than how easy the change is to ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  A default order that holds up in practice
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Verify the instrumentation.&lt;/strong&gt; One exposure event per session, assignment logged server-side rather than inferred from a query parameter, internal traffic excluded, bots filtered. Every downstream conclusion inherits the quality of this step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check message match.&lt;/strong&gt; Put the ad headline next to the page headline. If they describe different things, stop here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If message match fails, run the landing page experiment first.&lt;/strong&gt; Change the hero, the offer framing, or the proof: the elements that determine whether a visitor stays long enough to be asked anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If message match holds and engagement looks healthy, run the call-to-action experiment.&lt;/strong&gt; Label, placement, and the one or two lines of supporting copy next to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-run the call-to-action experiment after any page rewrite.&lt;/strong&gt; Treat the old result as historical, not as a default.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 4 is where a lot of teams want to start, and it is a legitimate starting point, but only when the diagnostic in the table above says the page is already doing its job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Track what happens after the action, not just the action
&lt;/h2&gt;

&lt;p&gt;A call to action that increases signups while filling the pipeline with people who never come back is a net loss. Before you run either experiment, decide on guardrail events and log them alongside the primary metric:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// One exposure event per session, emitted once and cached client-side&lt;/span&gt;
&lt;span class="nf"&gt;trackEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;experiment_exposure&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;experiment_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cta_copy_q3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;arm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;specific-verb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;campaign_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;launch_aug&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Downstream quality signals, tied to the same session&lt;/span&gt;
&lt;span class="c1"&gt;// activation_started, first_key_action, repeat_usage_7d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the primary metric moves up and a guardrail moves down, that is a trade-off that needs a decision. Guardrails are especially important for landing page experiments, because a page that over-promises will inflate the action rate and damage downstream quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not change both at once
&lt;/h2&gt;

&lt;p&gt;Running a page experiment and a call-to-action experiment simultaneously on the same traffic makes the result unreadable. You cannot tell whether the change came from the page, the ask, or the interaction between them.&lt;/p&gt;

&lt;p&gt;If you genuinely need both answers and have the traffic to support it, use a factorial design: four arms, so every page version is paired with every call to action. The interaction term is often the most interesting number in the output, because it tells you whether a strong page makes the label irrelevant. The cost is roughly four times the traffic of a two-arm experiment.&lt;/p&gt;

&lt;p&gt;For most campaigns, sequential experiments are the practical choice: page first, then the ask, with each experiment consuming a pre-committed block of traffic. Avoid switching between them mid-run based on how the numbers look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Link hygiene when campaign traffic is split
&lt;/h2&gt;

&lt;p&gt;If you are routing campaign traffic between page builds and call-to-action experiments, the link you publish becomes part of the workflow. Campaign copy already competes for space with a headline and a button, and long parameter-heavy URLs wrap badly in chat apps and social posts. A short link such as LinkBit keeps the destination compact and readable in the creative. That is a formatting convenience. Your experiment assignment should live in your own instrumentation, not in the link.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;Run the landing page experiment first when the funnel shows that visitors never engage, when message match is broken, or when a rewrite is already planned. Run the call to action first when the page is doing its job and the ask is the weak link, and only when your traffic is large enough to detect the smaller effect you are likely to see.&lt;/p&gt;

&lt;p&gt;Before either, confirm what your instrumentation measures. Most experiment programs that stall are running careful changes on a funnel they cannot see.&lt;/p&gt;

</description>
      <category>experimentation</category>
      <category>conversionrate</category>
      <category>landingpages</category>
      <category>webanalytics</category>
    </item>
    <item>
      <title>Why Your Link A/B Test Gets Uneven Traffic, and How to Fix It</title>
      <dc:creator>Yuto Tanaka</dc:creator>
      <pubDate>Mon, 19 Oct 2026 08:01:12 +0000</pubDate>
      <link>https://dev.to/yutotanaka/why-your-link-ab-test-gets-uneven-traffic-and-how-to-fix-it-1mcn</link>
      <guid>https://dev.to/yutotanaka/why-your-link-ab-test-gets-uneven-traffic-and-how-to-fix-it-1mcn</guid>
      <description>&lt;p&gt;You set a redirect test to split 50/50. A day later the dashboard says 62/38. Before you call it a winner, check the plumbing. In link tests, uneven traffic is often an assignment or caching bug, not random variation.&lt;/p&gt;

&lt;p&gt;This post covers the common causes and a server-side pattern that keeps the split honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Random assignment does not guarantee even observed traffic
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Math.random() &amp;lt; 0.5&lt;/code&gt; gives each request an equal chance of variant A. It does not guarantee that the first 100 requests split 50/50. With small samples, variance is normal. With thousands of exposures, a 60/40 split is a signal.&lt;/p&gt;

&lt;p&gt;Two things to separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Assignment&lt;/strong&gt;: which variant the system chose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exposure&lt;/strong&gt;: which variant actually reached the user and was logged.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most 'uneven traffic' bugs live in exposure or caching, not in the random function.&lt;/p&gt;

&lt;h2&gt;
  
  
  The usual suspects
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Permanent redirects
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;301&lt;/code&gt; tells browsers and CDNs to cache the redirect. The first variant can stick for every later request. Use &lt;code&gt;302&lt;/code&gt; or &lt;code&gt;307&lt;/code&gt; for tests, and set &lt;code&gt;Cache-Control: no-store&lt;/code&gt; on the redirect response.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. CDN or edge cache
&lt;/h3&gt;

&lt;p&gt;If your redirect is cached at the edge, the first request decides the variant for everyone in that cache region. Make the redirect response uncacheable, or move assignment to the edge with a key that varies per user.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Assignment after the redirect
&lt;/h3&gt;

&lt;p&gt;If you assign the variant on the destination page with JavaScript, you miss users who block JS, bots, prefetchers, and link previews. You also log exposure late. Assign before the redirect and log there.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Unstable assignment keys
&lt;/h3&gt;

&lt;p&gt;A new cookie per request, a session ID that resets, or a hash of IP + user agent can reshuffle users. Mobile IPs change, UA strings change, and cookie blockers delete state. Use a first-party cookie with a generated ID, and fall back to a deterministic hash only when necessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Bots and prefetch
&lt;/h3&gt;

&lt;p&gt;Slack, email scanners, and browsers prefetch links. They can hit both variants or hit one repeatedly. Filter known bots from analysis, and log user-agent plus a bot flag at exposure time.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Multiple entry points
&lt;/h3&gt;

&lt;p&gt;If one campaign link uses the splitter and another points directly to variant B, your traffic is not a clean 50/50. Route every test link through the same splitter.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Race conditions
&lt;/h3&gt;

&lt;p&gt;Parallel requests from the same user can assign twice before the cookie is written. This is rare, but it happens with prefetch plus navigation. A deterministic hash of the stable cookie ID prevents double assignment.&lt;/p&gt;

&lt;h2&gt;
  
  
  A server-side pattern that holds up
&lt;/h2&gt;

&lt;p&gt;Use a stable anonymous ID, hash it with the test name, and map the hash to a bucket. Set the cookie if missing, redirect with &lt;code&gt;302&lt;/code&gt;, and disable caching.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buckets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readUInt32BE&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="nx"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/test/:testName&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cookieName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`ab_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;testName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;anonId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cookieName&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;anonId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;anonId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cookieName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;anonId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;httpOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sameSite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lax&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;testName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;anonId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/landing-a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/landing-b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;logExposure&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;testName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;anonId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ua&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;302&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&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;The cookie name includes the test name so different tests do not collide.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;no-store&lt;/code&gt; is not optional. Without it, a CDN can freeze the split.&lt;/li&gt;
&lt;li&gt;Log exposure before the redirect. Destination-page analytics cannot see users who never load the page.&lt;/li&gt;
&lt;li&gt;If you need to run this at the edge, use the same hash and set a first-party cookie or use an existing stable ID.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What about fairness? SHA-256 modulo 2 is close enough for practical tests. For more than two variants, use modulo N. For very large tests, consider a uniform hash range and map to buckets to avoid modulo bias, though the bias is negligible for small N.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check for sample ratio mismatch
&lt;/h2&gt;

&lt;p&gt;Sample ratio mismatch (SRM) means the observed split differs from the planned split. A quick chi-square check catches broken assignment early.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;srmWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expectedRatio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;chi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;expectedRatio&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nx"&gt;chi&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 1 degree of freedom, p &amp;lt; 0.001&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;chi&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;10.828&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;620&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;380&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;srmWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sample ratio mismatch: check redirect caching, bot traffic, and assignment keys.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see SRM, do not 'fix' it by rerunning the test and stopping when the split looks even. Fix the cause, then start a fresh test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;302&lt;/code&gt; or &lt;code&gt;307&lt;/code&gt;, never &lt;code&gt;301&lt;/code&gt;, for test redirects.&lt;/li&gt;
&lt;li&gt;Send &lt;code&gt;Cache-Control: no-store&lt;/code&gt; on the redirect response.&lt;/li&gt;
&lt;li&gt;Assign from a stable first-party ID, not a fresh random value per request.&lt;/li&gt;
&lt;li&gt;Log exposure at redirect time with variant, test name, timestamp, and a bot flag.&lt;/li&gt;
&lt;li&gt;Route every campaign link through one splitter.&lt;/li&gt;
&lt;li&gt;Exclude known bots and internal traffic before analysis.&lt;/li&gt;
&lt;li&gt;Monitor SRM daily; stop and debug if it appears.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When you share test links
&lt;/h2&gt;

&lt;p&gt;A/B tests create more URLs: variant A, variant B, docs, Slack messages. Manual edits cause mistakes. A short link can keep the shared URL stable while the splitter handles the variant choice. LinkBit is a link shortener you can use for that kind of sharing: &lt;a href="https://linkbit.live/lbqfzui" rel="noopener noreferrer"&gt;LinkBit&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The main fix is still server-side assignment and cache-safe redirects. Short links just make distribution tidier.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>analytics</category>
    </item>
    <item>
      <title>How to A/B Test Two Landing Pages Using Campaign Links</title>
      <dc:creator>Yuto Tanaka</dc:creator>
      <pubDate>Fri, 16 Oct 2026 00:01:00 +0000</pubDate>
      <link>https://dev.to/yutotanaka/how-to-ab-test-two-landing-pages-using-campaign-links-p44</link>
      <guid>https://dev.to/yutotanaka/how-to-ab-test-two-landing-pages-using-campaign-links-p44</guid>
      <description>&lt;p&gt;You have two landing pages and one campaign link. You want to know which page converts better without sending half your traffic to a page you haven't tested. The usual answer is an A/B test, but the mechanics of routing a single campaign link to two destinations and then measuring the result are less obvious.&lt;/p&gt;

&lt;p&gt;This article covers a practical setup: a stable campaign URL that redirects visitors to variant A or B, assigns each visitor consistently, and passes the variant through to your analytics. The code is small; the discipline is in consistent assignment and clean measurement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The routing problem
&lt;/h2&gt;

&lt;p&gt;A campaign link is the URL you put in ads, emails, social posts, or QR codes. If you change that URL for every experiment, you lose attribution and break links that people have already shared. So the campaign link should stay stable, and the decision about which landing page to show should happen behind it.&lt;/p&gt;

&lt;p&gt;The simplest architecture:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Campaign link (short or long) points to a redirect endpoint, e.g. &lt;code&gt;https://example.com/campaign&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The redirect endpoint assigns a variant and sends a &lt;code&gt;302&lt;/code&gt; to the matching landing page.&lt;/li&gt;
&lt;li&gt;The landing page records the variant and reports conversions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This keeps one public URL while you test behind the scenes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assignment: random once, then sticky
&lt;/h2&gt;

&lt;p&gt;The most common mistake is assigning a variant on every page load. If the same visitor sees page A, then page B on refresh, your data is noisy and the user experience is confusing.&lt;/p&gt;

&lt;p&gt;You want &lt;em&gt;sticky&lt;/em&gt; assignment: once a visitor is assigned to A or B, they see that variant for the duration of the test. A first-party cookie is the simplest way to do this.&lt;/p&gt;

&lt;p&gt;On first visit, pick a variant at random and set a cookie. On later visits, read the cookie and use the same variant.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LANDING_PAGES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/landing-a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/landing-b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getVariant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cookieHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cookie&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cookieHeader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/ab_variant=&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;a|b&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/campaign&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Not found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getVariant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LANDING_PAGES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;variant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&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="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existingCookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cookie&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;existingCookie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ab_variant=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Set-Cookie&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;`ab_variant=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;; Path=/; Max-Age=2592000; SameSite=Lax`&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few details matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;302&lt;/code&gt; (temporary) instead of &lt;code&gt;301&lt;/code&gt; (permanent) so browsers and proxies do not cache the redirect permanently.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Cache-Control: no-store&lt;/code&gt; on the redirect response so a CDN does not serve the same variant to everyone.&lt;/li&gt;
&lt;li&gt;Appending &lt;code&gt;?variant=&lt;/code&gt; to the destination makes the variant available to client-side analytics without reading cookies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you use cookies, check your privacy requirements. A first-party, non-tracking cookie that only stores &lt;code&gt;a&lt;/code&gt; or &lt;code&gt;b&lt;/code&gt; is usually fine, but it should be covered by your cookie policy. If you cannot use cookies, a fallback is hashing a combination of IP and user agent, but that is less stable and can misassign visitors on shared networks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Passing the variant to analytics
&lt;/h2&gt;

&lt;p&gt;The A/B test is only as good as its measurement. You need to know which variant each conversion belongs to.&lt;/p&gt;

&lt;p&gt;The easiest approach is to read the &lt;code&gt;variant&lt;/code&gt; query parameter on the landing page and send it to your analytics tool as a custom dimension or event property.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;variant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Landing Page View&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Signup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use Google Analytics 4, set the variant as a user property or include it in the event parameters. In Plausible or Fathom, you can pass it as a custom property. The exact API depends on your tool, but the idea is the same: every event tied to the experiment carries the variant.&lt;/p&gt;

&lt;p&gt;Do not rely on URL parameters alone if users can navigate away and come back. Either set the cookie on the redirect (as above) or set a cookie on the landing page from the query parameter, so the variant persists across page views.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Client-side redirects cause flicker.&lt;/strong&gt; If you assign the variant with JavaScript after the page loads, the visitor may briefly see the wrong page. Server-side or edge redirects happen before any HTML is rendered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bots skew the results.&lt;/strong&gt; Crawlers and monitoring tools often follow redirects. You can exclude known bot user agents from assignment, or simply filter them out in analytics. Either way, know that your raw traffic numbers include non-humans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stopping too early.&lt;/strong&gt; A/B tests need enough conversions to distinguish a real difference from noise. If you have low traffic, you may need to run the test for weeks. Use a sample size calculator before you start, and don't peek at the results every hour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing during a seasonality spike.&lt;/strong&gt; If you run variant A during a sale and variant B after it, you are measuring time, not the landing page. Run both variants simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting &lt;code&gt;noindex&lt;/code&gt;.&lt;/strong&gt; Landing pages used in a test should have &lt;code&gt;&amp;lt;meta name="robots" content="noindex"&amp;gt;&lt;/code&gt; so search engines don't index two near-duplicate pages. The redirect endpoint should also not be indexed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the campaign link shareable
&lt;/h2&gt;

&lt;p&gt;A redirect endpoint like &lt;code&gt;https://example.com/campaign&lt;/code&gt; is fine, but it may be long, especially if you add tracking parameters. A short link can make the campaign URL easier to paste into ads, social bios, or QR codes.&lt;/p&gt;

&lt;p&gt;You can point a short link at your assignment endpoint. For example, a short link service like &lt;a href="https://linkbit.live/um47iyx" rel="noopener noreferrer"&gt;LinkBit&lt;/a&gt; can give you a compact URL that redirects to &lt;code&gt;https://example.com/campaign&lt;/code&gt;. The edge function still handles variant assignment; the short link just makes the entry point cleaner to share.&lt;/p&gt;

&lt;p&gt;One caution: if a short link service offers its own A/B or split-testing feature, don't layer it on top of your own assignment logic unless you have verified how the two interact. Duplicate splitting can break sticky assignment and make the data harder to interpret. Keep the experiment logic in one place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to measure
&lt;/h2&gt;

&lt;p&gt;For each variant, track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visitors assigned&lt;/strong&gt;: how many people entered the test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversions&lt;/strong&gt;: the action you care about (signup, purchase, demo request).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversion rate&lt;/strong&gt;: conversions divided by visitors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time to conversion&lt;/strong&gt;: useful if the decision is not immediate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compare conversion rates, but also check whether the variants differ in bounce rate, scroll depth, or other engagement signals. A page that converts slightly better but has a much higher bounce rate may not be the winner you think it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;A/B testing two landing pages with one campaign link comes down to three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A stable campaign URL that redirects to the variant.&lt;/li&gt;
&lt;li&gt;Sticky, random assignment stored in a cookie.&lt;/li&gt;
&lt;li&gt;Clean analytics that carry the variant through to conversion.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The code is a few dozen lines. The harder part is resisting the urge to change the test midway, and making sure your measurement is as reliable as your routing.&lt;/p&gt;

&lt;h1&gt;
  
  
  ABTesting #WebDevelopment #EdgeComputing #Analytics #LandingPages
&lt;/h1&gt;

</description>
      <category>analytics</category>
      <category>marketing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Where to Place a QR Code on a Brochure—and What to Write Beside It</title>
      <dc:creator>Yuto Tanaka</dc:creator>
      <pubDate>Wed, 30 Sep 2026 06:30:00 +0000</pubDate>
      <link>https://dev.to/yutotanaka/where-to-place-a-qr-code-on-a-brochure-and-what-to-write-beside-it-31pc</link>
      <guid>https://dev.to/yutotanaka/where-to-place-a-qr-code-on-a-brochure-and-what-to-write-beside-it-31pc</guid>
      <description>&lt;p&gt;A brochure QR code needs to answer two questions before anyone scans it: &lt;em&gt;What will I get, and can my phone read this comfortably?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Placement solves the second problem. The text beside the code solves the first. For developers preparing brochures for a product demo, conference booth, or technical workshop, treat both as part of the interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the code where the reader decides to act
&lt;/h2&gt;

&lt;p&gt;Choose the destination first, then place the QR code beside the content that makes that destination useful.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reader’s next step&lt;/th&gt;
&lt;th&gt;Suitable placement&lt;/th&gt;
&lt;th&gt;Suggested label&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Try a product&lt;/td&gt;
&lt;td&gt;Beside the demo description or final call to action&lt;/td&gt;
&lt;td&gt;“Scan to open the interactive demo”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Follow an implementation guide&lt;/td&gt;
&lt;td&gt;Beside the setup overview&lt;/td&gt;
&lt;td&gt;“Scan for the installation guide”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Register for a workshop&lt;/td&gt;
&lt;td&gt;Beside the date, venue, and registration details&lt;/td&gt;
&lt;td&gt;“Scan to register for the workshop”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;View a code sample&lt;/td&gt;
&lt;td&gt;Beside the relevant technical example&lt;/td&gt;
&lt;td&gt;“Scan to view the sample repository”&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A back panel works well for a general next step, such as booking a demo. It is less useful when the code explains a specific diagram buried inside the brochure. In that case, keep the diagram, label, and code together.&lt;/p&gt;

&lt;p&gt;For a tri-fold brochure, check the &lt;strong&gt;actual folded reading sequence&lt;/strong&gt;, not just the order of panels in your design file. Print a rough copy and fold it. Confirm that the code appears on the panel you intended and that its explanation is visible at the same time.&lt;/p&gt;

&lt;p&gt;Avoid putting essential next steps on a panel readers are unlikely to unfold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep it away from folds, trim edges, and visual noise
&lt;/h2&gt;

&lt;p&gt;Give the QR code its own stable, uncluttered area.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not straddle a fold. A crease can distort the pattern or make it difficult to hold flat.&lt;/li&gt;
&lt;li&gt;Keep it inside the printer’s safe area. Trim tolerances vary; use the print provider’s specifications rather than assuming a universal margin.&lt;/li&gt;
&lt;li&gt;Avoid photographs and textured backgrounds. A solid light background with dark modules is a reliable starting point.&lt;/li&gt;
&lt;li&gt;Do not crowd it with labels or icons. The blank border is part of the code’s scanning requirements.&lt;/li&gt;
&lt;li&gt;Consider how people hold the brochure. A code at a lower corner may be covered by a thumb.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a standard QR Code, preserve a quiet zone at least &lt;strong&gt;four modules wide on every side&lt;/strong&gt;. A module is one small square in the pattern. Keep text, borders, and artwork outside that zone.&lt;/p&gt;

&lt;p&gt;The quiet zone and the print safe area serve different purposes: one helps decoding; the other protects content from trimming. You need both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Size the printed code, not just the image file
&lt;/h2&gt;

&lt;p&gt;There is no single minimum physical size that guarantees scanning. Reliability depends on pattern density, print quality, contrast, lighting, camera performance, and scanning distance.&lt;/p&gt;

&lt;p&gt;For a handheld brochure, a code around &lt;strong&gt;25–30 mm square, plus its quiet zone&lt;/strong&gt;, can be a useful prototype size. It is a starting point to test, not a production guarantee. Dense codes or longer scanning distances may need more space.&lt;/p&gt;

&lt;p&gt;Developer-friendly checks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep the encoded value reasonably short. Longer values can require more modules, making each module smaller at the same printed size.&lt;/li&gt;
&lt;li&gt;Export as vector artwork when your print workflow supports it. SVG or a suitable vector PDF avoids raster scaling artifacts.&lt;/li&gt;
&lt;li&gt;If using a raster image, avoid blur and smoothing. Check it at its final placed size; nominal DPI alone does not guarantee readable modules.&lt;/li&gt;
&lt;li&gt;Do not assume higher error correction fixes poor layout. It can increase symbol density and does not replace adequate size, contrast, or a quiet zone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you choose a shortened destination, a service such as &lt;a href="https://linkbit.live/1ahfmsr" rel="noopener noreferrer"&gt;LinkBit&lt;/a&gt; can shorten the URL before you generate the QR code. That may reduce the encoded payload, but it also introduces a redirect dependency. Confirm the final destination and plan for the link to remain usable throughout the brochure’s lifetime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write the benefit beside the code
&lt;/h2&gt;

&lt;p&gt;“Scan me” describes the interaction, not the reason to do it.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Scan to + specific action or resource + relevant expectation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Scan to try the browser demo—no installation required.” Use only if that is true.&lt;/li&gt;
&lt;li&gt;“Scan for the Python quickstart and sample project.” Better than “Learn more” when those are the actual resources.&lt;/li&gt;
&lt;li&gt;“Scan to book a 15-minute technical walkthrough.” Appropriate when the destination is a booking page.&lt;/li&gt;
&lt;li&gt;“Scan to download the specification PDF.” Makes the destination format clear.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mention friction when it matters: account requirements, paid registration, or a download rather than a webpage. A reader should not discover a materially different task after scanning.&lt;/p&gt;

&lt;p&gt;Keep the label next to the code, with enough separation to preserve the quiet zone. If scanning is optional, include a readable fallback address outside that zone. Use a genuine, working address—not an illustrative placeholder in the final artwork.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the complete printed interaction
&lt;/h2&gt;

&lt;p&gt;A QR image that scans on a monitor has not passed a brochure test.&lt;/p&gt;

&lt;p&gt;Before approving the print run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Print at &lt;strong&gt;100% of the intended physical size&lt;/strong&gt;, without “fit to page” scaling.&lt;/li&gt;
&lt;li&gt;Fold the brochure and scan it while holding it naturally.&lt;/li&gt;
&lt;li&gt;Try more than one phone under ordinary indoor lighting.&lt;/li&gt;
&lt;li&gt;Check the finished stock or proof where possible; glossy reflections can interfere with scanning.&lt;/li&gt;
&lt;li&gt;Confirm that redirects resolve and the final page uses a valid HTTPS connection.&lt;/li&gt;
&lt;li&gt;Test the destination on mobile, including forms, downloads, and any login requirements.&lt;/li&gt;
&lt;li&gt;Verify that the label accurately describes what opens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you include several QR codes, give each a distinct purpose and label. For a single next step, one well-placed code is usually clearer than repeating it across every panel.&lt;/p&gt;

&lt;p&gt;The final check is simple: can someone understand the offer, scan without rearranging the brochure awkwardly, and reach the promised resource? If not, fix the placement, label, or destination before adding more visual styling.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
