<?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: ManTek Technologies</title>
    <description>The latest articles on DEV Community by ManTek Technologies (mantekio).</description>
    <link>https://dev.to/mantekio</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%2Forganization%2Fprofile_image%2F13693%2Fd40fcc29-8382-4d5c-8bb3-8aebdb137304.png</url>
      <title>DEV Community: ManTek Technologies</title>
      <link>https://dev.to/mantekio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mantekio"/>
    <language>en</language>
    <item>
      <title>Short links that outlive the vendor: a newsroom URL shortener on WordPress and AWS</title>
      <dc:creator>Jaafar Abazid</dc:creator>
      <pubDate>Wed, 24 Jun 2026 20:32:21 +0000</pubDate>
      <link>https://dev.to/mantekio/short-links-that-outlive-the-vendor-a-newsroom-url-shortener-on-wordpress-and-aws-gja</link>
      <guid>https://dev.to/mantekio/short-links-that-outlive-the-vendor-a-newsroom-url-shortener-on-wordpress-and-aws-gja</guid>
      <description>&lt;p&gt;An editor hits publish on a breaking story and pushes it to a million followers.&lt;br&gt;
The headline is sharp. The link beneath it is forty characters of&lt;br&gt;
&lt;code&gt;%D8%A7%D9%84%D8%B0...&lt;/code&gt;, unreadable, often cut off by the platform, telling the&lt;br&gt;
reader nothing about where the tap will take them.&lt;/p&gt;

&lt;p&gt;That is what a long URL looks like on social when the slug is Arabic. WordPress&lt;br&gt;
stores and emits the slug percent-encoded, so a clean Arabic headline turns into&lt;br&gt;
a wall of &lt;code&gt;%XX&lt;/code&gt; bytes the moment it leaves your site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/الذكاء-الاصطناعي  →  /%D8%A7%D9%84%D8%B0%D9%83%D8%A7%D8%A1-%D8%A7%D9%84%D8%A7%D8%B5%D8%B7%D9%86%D8%A7%D8%B9%D9%8A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(We have written before about &lt;a href="https://www.mantek.io/insights/wordpress-arabic-slug-truncation" rel="noopener noreferrer"&gt;how those Arabic slugs get truncated in the first&lt;br&gt;
place&lt;/a&gt;. This is the other half of the&lt;br&gt;
story: what happens to them out in the world.)&lt;/p&gt;

&lt;p&gt;Every serious newsroom solves this the same way, with branded short links:&lt;br&gt;
&lt;code&gt;sho.rt/9Kx2a&lt;/code&gt; instead of the wall of bytes. Short, on brand, and the same length&lt;br&gt;
whether the article is Arabic, English, or anything else. The only real question&lt;br&gt;
is whether you buy that capability from a service like Bitly or TinyURL, or build&lt;br&gt;
it yourself.&lt;/p&gt;

&lt;p&gt;We did both, on purpose. Here is why, and how.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why both
&lt;/h2&gt;

&lt;p&gt;A short link, once shared, is effectively permanent. It lives in tweets, Telegram&lt;br&gt;
channels, WhatsApp forwards, and printed QR codes for years. So the day a vendor&lt;br&gt;
raises its prices, retires a feature, or simply shuts down, every one of those&lt;br&gt;
links dies, and the traffic with them. For a newsroom whose archive is its&lt;br&gt;
capital, that is not an acceptable dependency.&lt;/p&gt;

&lt;p&gt;So we set one rule: &lt;strong&gt;the ability to resolve our own short links must never depend&lt;br&gt;
on a company we do not control.&lt;/strong&gt; Buy the convenience, own the lifeline.&lt;/p&gt;

&lt;p&gt;In practice that means Bitly serves the short domain today, because it gives the&lt;br&gt;
newsroom polished click analytics and a link UI editors like. But sitting behind&lt;br&gt;
it, fully in sync, is an in-house resolver running on our own AWS edge, ready to&lt;br&gt;
take over with a single DNS change. The reader never sees the seam, because the&lt;br&gt;
short codes are identical on both fronts. This runs on the same WordPress and AWS&lt;br&gt;
stack we have &lt;a href="https://www.mantek.io/insights/breaking-news-wordpress-aws-architecture" rel="noopener noreferrer"&gt;written about before&lt;/a&gt;:&lt;br&gt;
WordPress on EC2 behind an Application Load Balancer, Aurora for the database, and&lt;br&gt;
CloudFront at the edge.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feed59017z286mcp63swo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feed59017z286mcp63swo.png" alt="The two interchangeable fronts" width="799" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One short code, two interchangeable fronts. DNS decides who answers, so failover is a single DNS change, and the reader always lands on the same canonical article.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The one decision everything hangs on
&lt;/h2&gt;

&lt;p&gt;Before any infrastructure, there is a single design choice that makes the rest&lt;br&gt;
possible: &lt;strong&gt;one short ID, minted once in WordPress, reused everywhere.&lt;/strong&gt; The same&lt;br&gt;
string is the key in our database, the key in DynamoDB, and the custom back-half&lt;br&gt;
on Bitly.&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;9Kx2a&lt;/code&gt; is literally the same value in all three places, switching who&lt;br&gt;
answers the short domain is invisible to anyone holding the link. There is no&lt;br&gt;
remapping, no lookup table between vendors, no links left behind. That one&lt;br&gt;
decision is what turns "own the lifeline" from a slogan into a DNS change.&lt;br&gt;
Everything below serves it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Generating the ID
&lt;/h2&gt;

&lt;p&gt;The ID is drawn from a 64-character, URL-safe alphabet: &lt;code&gt;A-Z&lt;/code&gt;, &lt;code&gt;a-z&lt;/code&gt;, &lt;code&gt;0-9&lt;/code&gt;, plus&lt;br&gt;
&lt;code&gt;-&lt;/code&gt; and &lt;code&gt;_&lt;/code&gt;. Those 64 characters all live in a URL path without encoding, so the&lt;br&gt;
short link stays clean no matter what.&lt;/p&gt;

&lt;p&gt;We generate it &lt;strong&gt;only when a post is published&lt;/strong&gt;, never on draft. That keeps the&lt;br&gt;
keyspace clean (no IDs burned on posts that never go live), avoids leaking a URL&lt;br&gt;
for unpublished work, and means a short link never resolves to a 404 or a private&lt;br&gt;
draft. A short link should only ever exist for something a reader can actually&lt;br&gt;
read.&lt;/p&gt;

&lt;p&gt;The generator uses a cryptographic random source, not &lt;code&gt;rand()&lt;/code&gt;, so codes are not&lt;br&gt;
guessable or enumerable, and it leans on a &lt;code&gt;UNIQUE&lt;/code&gt; constraint to stay correct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;SHORTLINK_ALPHABET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;SHORTLINK_LENGTH&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// 64^5 ≈ 1.07B. Use 6 for ≈ 68.7B of headroom.&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;mantek_generate_short_id&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$alphabet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SHORTLINK_ALPHABET&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$max&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$alphabet&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$id&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&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="nv"&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="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;SHORTLINK_LENGTH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&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="nv"&gt;$id&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$alphabet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nb"&gt;random_int&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="nv"&gt;$max&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;  &lt;span class="c1"&gt;// CSPRNG: not guessable&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Runs on the publish transition only. The UNIQUE index on short_id turns a&lt;/span&gt;
&lt;span class="c1"&gt;// collision into a failed INSERT, so we simply re-mint and try again.&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;mantek_assign_short_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$post_id&lt;/span&gt; &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'short_links'&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="nv"&gt;$attempt&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="nv"&gt;$attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$attempt&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="nv"&gt;$short_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mantek_generate_short_id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$inserted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;"INSERT INTO &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (short_id, post_id, full_url, provider, status, created_at)
             VALUES (%s, %d, %s, %s, 'active', NOW())"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$short_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get_permalink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post_id&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;mantek_active_provider&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$inserted&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="nv"&gt;$short_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// clean insert: the ID was free&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c1"&gt;// Duplicate key (MySQL 1062): the ID was taken. Loop and re-mint.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s2"&gt;"Could not mint a unique short_id for post &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A fair question: with random generation, will codes collide? Yes, and you must&lt;br&gt;
plan for it rather than hope. The maths is the birthday problem, not the raw&lt;br&gt;
keyspace: at five characters you have a better-than-even chance of &lt;em&gt;some&lt;/em&gt;&lt;br&gt;
collision once you have minted roughly 38,000 links, which a busy newsroom passes&lt;br&gt;
within a year. That is exactly why the &lt;code&gt;UNIQUE&lt;/code&gt; constraint and the retry loop are&lt;br&gt;
not optional.&lt;/p&gt;

&lt;p&gt;What that maths does &lt;em&gt;not&lt;/em&gt; mean is that the retry fires often. The chance that any&lt;br&gt;
single new code clashes is the number of live links divided by the keyspace, so&lt;br&gt;
even at a million live links roughly one insert in a thousand needs a second try,&lt;br&gt;
and a third try is a one-in-a-million event. Five characters with the retry is&lt;br&gt;
comfortable essentially forever; six characters (about 68.7 billion) makes the&lt;br&gt;
retry all but theoretical, at the cost of one extra character. Pick five unless&lt;br&gt;
you want the longest possible runway.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where the mapping lives
&lt;/h2&gt;

&lt;p&gt;The short code needs a home in WordPress. The tempting shortcut is the existing&lt;br&gt;
&lt;code&gt;guid&lt;/code&gt; column on &lt;code&gt;wp_posts&lt;/code&gt;, since it already holds something URL-shaped. Resist&lt;br&gt;
it (more on why in a moment). We use a dedicated table instead:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;short_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the code itself, with a &lt;code&gt;UNIQUE&lt;/code&gt; index for the retry loop and the reverse lookup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;post_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the local post this code is attached to, used to refresh &lt;code&gt;full_url&lt;/code&gt;; instance-local, never used for resolution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;full_url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the article's current canonical URL, a pointer we refresh when it changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;provider&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;which backend was active when the code was minted (&lt;code&gt;bitly&lt;/code&gt;, &lt;code&gt;inhouse&lt;/code&gt;); a historical marker, since the DNS is the real router&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;lifecycle: &lt;code&gt;active&lt;/code&gt;, &lt;code&gt;pending&lt;/code&gt;, or &lt;code&gt;gone&lt;/code&gt;, so the resolver knows what to do when a post is unpublished&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;created_at&lt;/code&gt; / &lt;code&gt;updated_at&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;timestamps for reconciliation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A dedicated table owns its own schema, is properly indexed for the reverse lookup,&lt;br&gt;
and has zero blast radius on the rest of WordPress.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why not just reuse the &lt;code&gt;guid&lt;/code&gt; column?&lt;/strong&gt; Because the GUID is not a usable URL,&lt;br&gt;
despite looking like one. Its real job is to be a permanent, never-changed&lt;br&gt;
identifier that feed readers use to decide whether an item is new. Overwrite it&lt;br&gt;
and you risk re-notifying every subscriber, breaking the WXR importer's&lt;br&gt;
de-duplication (and newsrooms migrate often), and corrupting media references,&lt;br&gt;
since for attachments the GUID &lt;em&gt;is&lt;/em&gt; the real file URL. Indexing it also means&lt;br&gt;
altering a core table. A dedicated table avoids all of that for almost nothing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Resolving at the edge: redirect, not proxy
&lt;/h2&gt;

&lt;p&gt;Here is the most important architectural decision, and the one most people get&lt;br&gt;
subtly wrong. A short link should &lt;strong&gt;redirect&lt;/strong&gt;. It resolves the code to the&lt;br&gt;
article's URL and returns a &lt;code&gt;301&lt;/code&gt;, and the reader's browser then loads the real&lt;br&gt;
article from the normal stack. The short-link layer never fetches or serves the&lt;br&gt;
article's HTML itself.&lt;/p&gt;

&lt;p&gt;The alternative, serving the article's HTML &lt;em&gt;under&lt;/em&gt; the short URL, is a trap. It&lt;br&gt;
splits every article across two URLs, which confuses search engines and forces&lt;br&gt;
canonical-tag gymnastics, it leaves the reader looking at a &lt;code&gt;sho.rt&lt;/code&gt; address&lt;br&gt;
instead of your trusted domain, and it doubles your cache, since CloudFront now&lt;br&gt;
stores the full article twice. The redirect keeps the short-link layer trivially&lt;br&gt;
small: the cached object is a few hundred bytes, and it points everyone at the one&lt;br&gt;
canonical article.&lt;/p&gt;

&lt;p&gt;That also dissolves a performance worry worth naming. Resolving a short link is&lt;br&gt;
&lt;strong&gt;one&lt;/strong&gt; key lookup followed by a redirect. It never disassembles the full URL into&lt;br&gt;
date, category, and slug, and it never runs a &lt;code&gt;WP_Query&lt;/code&gt;. WordPress already&lt;br&gt;
resolves its permalinks once, for &lt;em&gt;all&lt;/em&gt; traffic, and CloudFront caches the result.&lt;br&gt;
The short link does not add that cost; it just prepends a tiny redirect hop in&lt;br&gt;
front of it.&lt;/p&gt;

&lt;p&gt;The in-house path works like this. The short domain's DNS points at CloudFront. On&lt;br&gt;
a cache hit, CloudFront returns the cached &lt;code&gt;301&lt;/code&gt; immediately, with no compute at&lt;br&gt;
all. On a miss, it runs a small Lambda@Edge function that does a single DynamoDB&lt;br&gt;
lookup on the short code, builds the redirect, and caches it for next time:&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;// Lambda@Edge, viewer-request on the short domain's CloudFront distribution.&lt;/span&gt;
&lt;span class="c1"&gt;// Resolves /&amp;lt;short_id&amp;gt; to a 301 toward the article's current URL.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DynamoDBClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GetItemCommand&lt;/span&gt; &lt;span class="p"&gt;}&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;@aws-sdk/client-dynamodb&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;ddb&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;DynamoDBClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-east-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Records&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;cf&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;shortId&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;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&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;+/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;shortId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GetItemCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;short_links&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;short_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;shortId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;ProjectionExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;full_url, #s&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ExpressionAttributeNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#s&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;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;Item&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gone&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;goneOrNotFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Item&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;Item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;full_url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;S&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="nf"&gt;isOwnDomain&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// open-redirect guard&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;301&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;statusDescription&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Moved Permanently&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Location&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;appendUtm&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;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="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="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="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public, max-age=86400&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A DynamoDB lookup on the partition key is single-digit milliseconds, and most hits&lt;br&gt;
never reach it anyway, because CloudFront is serving the cached redirect. One&lt;br&gt;
detail to know: a Lambda@Edge function is authored in &lt;code&gt;us-east-1&lt;/code&gt; and replicated to&lt;br&gt;
every edge, and its DynamoDB call reaches the table's home region, so for global&lt;br&gt;
low latency you either replicate the table with DynamoDB Global Tables or front it&lt;br&gt;
with DAX. In practice it rarely bites, precisely because the cache absorbs the vast&lt;br&gt;
majority of requests.&lt;/p&gt;

&lt;p&gt;A few decisions baked into that function:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;301, not 302.&lt;/strong&gt; We want search engines to consolidate the article's ranking
signals on the canonical URL, and they treat the &lt;code&gt;301&lt;/code&gt; status itself as that
signal. The explicit &lt;code&gt;Cache-Control&lt;/code&gt; keeps the redirect from being pinned
forever in browser caches, so a later URL change is still picked up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open-redirect guard.&lt;/strong&gt; The resolver only ever sends readers to our own
domains. An unknown or tampered target returns a plain 404, so the shortener can
never be abused to bounce users to a phishing page. Random codes make scanning
for valid links low-yield too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway, honestly.&lt;/strong&gt; You will see URL shorteners put API Gateway in this
path. Its only real job there is to expose a regional Lambda to CloudFront as an
origin. For a pure redirect you usually do not need it: Lambda@Edge resolves
directly, and where a regional function is preferred, a Lambda Function URL is a
lighter front door. (This architecture already runs API Gateway, but for
&lt;a href="https://www.mantek.io/insights/breaking-news-wordpress-aws-architecture" rel="noopener noreferrer"&gt;headless apps&lt;/a&gt;, not for the
shortener.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzou5do9xcz99ule0t7z7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzou5do9xcz99ule0t7z7.png" alt="In-house resolution and the full stack" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Resolving the short link is one cached redirect at the edge, with a DynamoDB lookup only on a cache miss. The browser then follows the 301 to the article through the normal stack, exactly like any other reader.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Migration-proof by design
&lt;/h2&gt;

&lt;p&gt;Notice what we actually resolve by. The durable handle is the &lt;code&gt;short_id&lt;/code&gt; itself,&lt;br&gt;
the value we mint, control, and put out into the world. The &lt;code&gt;full_url&lt;/code&gt; is just a&lt;br&gt;
pointer we keep current beside it. That pairing is what lets a short link outlast&lt;br&gt;
not only a vendor but your own URL scheme.&lt;/p&gt;

&lt;p&gt;It is worth being precise about why, because the obvious candidate for a permanent&lt;br&gt;
identity, the WordPress &lt;code&gt;post_id&lt;/code&gt;, is not actually stable. Post IDs are&lt;br&gt;
auto-increment integers local to one database; migrate to a fresh WordPress (or to&lt;br&gt;
anything else) and they get reassigned. So we never resolve by &lt;code&gt;post_id&lt;/code&gt;. We&lt;br&gt;
resolve by the &lt;code&gt;short_id&lt;/code&gt;, and we carry the &lt;code&gt;short_id → full_url&lt;/code&gt; mapping across&lt;br&gt;
every migration, refreshing &lt;code&gt;full_url&lt;/code&gt; to wherever the article now lives. Newsrooms&lt;br&gt;
re-platform, restructure categories, and change permalink patterns over the years,&lt;br&gt;
and each time the old URLs change. A short link keyed on the ID we own does not&lt;br&gt;
care: share &lt;code&gt;sho.rt/9Kx2a&lt;/code&gt; once, and it keeps landing on the right article through&lt;br&gt;
every future migration, because resolution ends at a code we control, never at a&lt;br&gt;
database's internal ID or a frozen URL string.&lt;/p&gt;

&lt;p&gt;The one rule that keeps this true: &lt;strong&gt;when an article's URL changes, update the&lt;br&gt;
&lt;code&gt;full_url&lt;/code&gt; in DynamoDB and invalidate the cached redirect.&lt;/strong&gt; Active invalidation is&lt;br&gt;
the primary mechanism, so the change is picked up at once, and the bounded&lt;br&gt;
&lt;code&gt;Cache-Control&lt;/code&gt; is a safety net that heals anything an invalidation missed within a&lt;br&gt;
day. For an everyday slug edit, invalidate the single path. For a mass migration of&lt;br&gt;
thousands of URLs at once, let a short TTL expire instead, since firing thousands&lt;br&gt;
of individual invalidations is slower and costlier. This is also why the serving&lt;br&gt;
mapping lives in DynamoDB rather than only inside WordPress: it sits outside the&lt;br&gt;
CMS, so it survives any migration by definition. After a re-platform you refresh the&lt;br&gt;
&lt;code&gt;full_url&lt;/code&gt; values to the new URLs, and every link already in the wild keeps&lt;br&gt;
resolving.&lt;/p&gt;
&lt;h2&gt;
  
  
  Keeping the stores in sync at publish
&lt;/h2&gt;

&lt;p&gt;When a post goes live, three things happen in order: WordPress mints the ID into&lt;br&gt;
its own table, upserts the mapping into DynamoDB, and calls the active shortener.&lt;br&gt;
Each store has a clear, non-overlapping role:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Store&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WordPress (&lt;code&gt;wp_short_links&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Authoring source of truth: mints the ID, knows the canonical URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DynamoDB&lt;/td&gt;
&lt;td&gt;Serving source of truth at the edge, and the vendor-neutral warm standby&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bitly&lt;/td&gt;
&lt;td&gt;Downstream replica: the active analytics front today, and swappable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest complication with writing to three places is partial failure: the&lt;br&gt;
DynamoDB write succeeds but the Bitly call times out, say. So the external call&lt;br&gt;
goes through a small queue (SQS) with retries, so a transient blip never silently&lt;br&gt;
loses a mapping, and a periodic reconciliation job diffs WordPress against&lt;br&gt;
DynamoDB and Bitly to catch any drift. WordPress is always the tie-breaker.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffdaxiu8y3kg15709etol.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffdaxiu8y3kg15709etol.png" alt="The publish write-path" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;On publish, WordPress mints the id and writes its own table, then upserts DynamoDB and enqueues the Bitly call through SQS, so a transient failure never loses a mapping. WordPress stays the source of truth.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Bitly, and how we keep it swappable
&lt;/h2&gt;

&lt;p&gt;The call to the shortener does not name Bitly. It goes through an interface, so&lt;br&gt;
Bitly is one implementation behind a contract the rest of the code never has to&lt;br&gt;
think about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ShortLinkProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$shortId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$fullUrl&lt;/span&gt; &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;ShortLinkResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;updateTarget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$shortId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$newUrl&lt;/span&gt; &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;ShortLinkResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$shortId&lt;/span&gt; &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;?ShortLinkStats&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// if the vendor exposes it&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BitlyProvider&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShortLinkProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$shortId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$fullUrl&lt;/span&gt; &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;ShortLinkResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'POST'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/v4/bitlinks'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'long_url'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$fullUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'domain'&lt;/span&gt;         &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;brandedDomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;// our custom short domain&lt;/span&gt;
            &lt;span class="s1"&gt;'custom_bitlink'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;brandedDomain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$shortId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// our ID as the back-half&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Bitly may SILENTLY fall back to a random back-half if ours is somehow&lt;/span&gt;
        &lt;span class="c1"&gt;// taken, instead of failing. Never accept that: it would break the&lt;/span&gt;
        &lt;span class="c1"&gt;// "same ID everywhere" invariant. Assert it returned exactly what we asked.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;backHalf&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$shortId&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ShortLinkCollision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$shortId&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// caller re-mints and retries&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ShortLinkResult&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;link&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="c1"&gt;// updateTarget(), stats() ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things we confirmed against Bitly's own documentation before relying on them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom back-halves are unique per branded domain, not globally.&lt;/strong&gt; On our own
short domain, any code that is unique in our database is automatically unique on
Bitly, so our &lt;code&gt;UNIQUE&lt;/code&gt; constraint is the single source of truth. No second
uniqueness problem to manage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bitly links are case-sensitive,&lt;/strong&gt; which is why our mixed-case alphabet is safe
and why &lt;code&gt;9Kx2a&lt;/code&gt; and &lt;code&gt;9kX2A&lt;/code&gt; are different links.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one trap, captured in the code above: if you request a custom back-half that is&lt;br&gt;
already in use, Bitly may return a link with a &lt;em&gt;random&lt;/em&gt; back-half rather than an&lt;br&gt;
error. Accept that blindly and your database says &lt;code&gt;9Kx2a&lt;/code&gt; while Bitly serves&lt;br&gt;
something else. The integration must assert the returned back-half matches the&lt;br&gt;
requested one, and treat a mismatch as a collision.&lt;/p&gt;

&lt;p&gt;While Bitly is the active front, our DynamoDB table is effectively write-only,&lt;br&gt;
since the short domain points at Bitly and our resolver is never triggered. That is&lt;br&gt;
not waste, it is a warm standby. Storing a few hundred bytes per link and doing the&lt;br&gt;
occasional write costs almost nothing, and it is exactly what makes failover a DNS&lt;br&gt;
change rather than a frantic rebuild.&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff: failover is a DNS change
&lt;/h2&gt;

&lt;p&gt;Put it together and the failover story is almost boring, which is the point. The&lt;br&gt;
short domain points at Bitly today. To take it back in-house, you repoint its DNS&lt;br&gt;
to our CloudFront distribution. Every link already in the wild keeps working,&lt;br&gt;
because the codes are identical on both fronts and our DynamoDB table already holds&lt;br&gt;
every mapping.&lt;/p&gt;

&lt;p&gt;The one operational honesty here: that cutover is only as fast as DNS lets it be,&lt;br&gt;
and the TTL is a knob you manage, not a constant. In steady state, pointing at&lt;br&gt;
Bitly with no change in sight, a normal TTL (say an hour) is fine; the record is&lt;br&gt;
not changing, so there is nothing to propagate faster. The TTL only matters around&lt;br&gt;
a switch. For a planned move off Bitly, lower it to 60 to 300 seconds a day or two&lt;br&gt;
ahead (long enough for the old, higher TTL to age out everywhere), make the change,&lt;br&gt;
then raise it back. If instead you want to be ready for an unplanned failover, say&lt;br&gt;
Bitly itself goes down, keep the TTL permanently low as cheap insurance, since the&lt;br&gt;
cost on modern anycast DNS is negligible. One caveat: some resolvers clamp very low&lt;br&gt;
values, so do not go below about 60 seconds or treat the TTL as an absolute&lt;br&gt;
guarantee.&lt;/p&gt;

&lt;p&gt;With DNS control on your side, the short-link layer survives a vendor shutdown, a&lt;br&gt;
price hike, a removed feature, or a full CMS re-platform. The links keep resolving&lt;br&gt;
regardless.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part: analytics
&lt;/h2&gt;

&lt;p&gt;If the in-house resolver can do everything, why pay Bitly at all? Analytics. Bitly&lt;br&gt;
gives a newsroom per-link click counts, geographic and referrer breakdowns, an&lt;br&gt;
editor-friendly UI for creating and tracking links, and the bot-filtering that&lt;br&gt;
makes those numbers trustworthy. That is real product work, and it is the reason to&lt;br&gt;
buy rather than build.&lt;/p&gt;

&lt;p&gt;Could you build it? Yes, and the edge resolver is the perfect place to tap, because&lt;br&gt;
every redirect is already an event. There are two complementary ways to capture it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client-side&lt;/strong&gt;, by having the redirect append a marker (UTM parameters) to the
destination, so the analytics already on the article page (GA4, or newsroom tools
like Chartbeat and Parse.ly) attribute the visit. This counts human readers and
naturally excludes bots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-side&lt;/strong&gt;, by having the resolver send a GA4 Measurement Protocol event on
each resolution, which counts every hit including the ones that never render a
page, closer to how Bitly counts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One correction worth making, since it is a common assumption: GA cannot read the&lt;br&gt;
request headers of a redirect, and the redirect runs no JavaScript. The signal that&lt;br&gt;
reaches your analytics is the marker parameter or the server-side event, not a&lt;br&gt;
header.&lt;/p&gt;

&lt;p&gt;So the honest line is not "GA cannot track short links." It can. The gap is that&lt;br&gt;
matching Bitly's per-link dashboards, editor UI, and bot filtering is a product you&lt;br&gt;
would own and maintain, and for many newsrooms paying for that polish is the right&lt;br&gt;
call. We built the durable core so we are never locked in, and we let Bitly handle&lt;br&gt;
the analytics, with our own edge waiting as the hot standby. Buy the convenience,&lt;br&gt;
own the lifeline.&lt;/p&gt;




&lt;p&gt;That is the whole shape of it: build the part that must never disappear, the&lt;br&gt;
resolution and the mapping, buy the convenience layer on top, and keep them&lt;br&gt;
interchangeable so a vendor decision is never an existential one. A short link your&lt;br&gt;
newsroom shared years ago should still land tomorrow, no matter who is answering&lt;br&gt;
the domain.&lt;/p&gt;

&lt;p&gt;This is the kind of WordPress and AWS engineering &lt;a href="https://www.mantek.io/wordpress-aws" rel="noopener noreferrer"&gt;our practice&lt;/a&gt; is&lt;br&gt;
built on. If you run a publication that lives on social and cannot afford to lose&lt;br&gt;
the links it has shared, &lt;a href="https://www.mantek.io/contact" rel="noopener noreferrer"&gt;let's talk&lt;/a&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-at-the-edge.html" rel="noopener noreferrer"&gt;Lambda@Edge&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html" rel="noopener noreferrer"&gt;CloudFront cache invalidation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GlobalTables.html" rel="noopener noreferrer"&gt;DynamoDB Global Tables&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DAX.html" rel="noopener noreferrer"&gt;DAX&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.bitly.com/docs/tutorials/shorten-customize-links/" rel="noopener noreferrer"&gt;Bitly: shorten and customise links&lt;/a&gt; and &lt;a href="https://support.bitly.com/hc/en-us/articles/360056593371-Can-I-use-the-same-back-half-in-multiple-groups" rel="noopener noreferrer"&gt;custom back-half uniqueness&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/analytics/devguides/collection/protocol/ga4" rel="noopener noreferrer"&gt;GA4 Measurement Protocol&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>wordpress</category>
      <category>aws</category>
      <category>architecture</category>
      <category>serverless</category>
    </item>
    <item>
      <title>The 200-byte trap: why WordPress core updates break Arabic URLs</title>
      <dc:creator>Jaafar Abazid</dc:creator>
      <pubDate>Sun, 21 Jun 2026 12:47:31 +0000</pubDate>
      <link>https://dev.to/mantekio/the-200-byte-trap-why-wordpress-core-updates-break-arabic-urls-5d5k</link>
      <guid>https://dev.to/mantekio/the-200-byte-trap-why-wordpress-core-updates-break-arabic-urls-5d5k</guid>
      <description>&lt;p&gt;You update WordPress on a quiet afternoon — a routine release, the kind you've&lt;br&gt;
installed a hundred times. The dashboard says everything went fine. Then the&lt;br&gt;
404s start: not a handful, but every long-headlined article in your archive, all&lt;br&gt;
at once, all in Arabic.&lt;/p&gt;

&lt;p&gt;Nothing in the update log mentions it. No plugin changed. And the cruel part:&lt;br&gt;
the data that made those URLs work is already gone — shaved off inside a&lt;br&gt;
database-upgrade routine that ran for a few milliseconds and reported success.&lt;br&gt;
This isn't a freak accident or a broken plugin. It's three separate assumptions&lt;br&gt;
baked into WordPress core, each hard-coding the same number — &lt;strong&gt;200&lt;/strong&gt; — and&lt;br&gt;
Arabic sites are almost uniquely exposed to all three. We hit this running&lt;br&gt;
WordPress for Arabic newsrooms, the same high-traffic publishing we've written&lt;br&gt;
about &lt;a href="https://www.mantek.io/insights/breaking-news-wordpress-aws-architecture" rel="noopener noreferrer"&gt;surviving breaking-news spikes&lt;/a&gt;.&lt;br&gt;
We traced it to the exact lines in core and built a fix that survives every&lt;br&gt;
future update. Here's the whole story.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Arabic URLs hit a wall English never does
&lt;/h2&gt;

&lt;p&gt;WordPress stores a post's slug in the &lt;code&gt;post_name&lt;/code&gt; column of &lt;code&gt;wp_posts&lt;/code&gt;, and a&lt;br&gt;
category or tag slug in the &lt;code&gt;slug&lt;/code&gt; column of &lt;code&gt;wp_terms&lt;/code&gt;. Both are &lt;code&gt;VARCHAR(200)&lt;/code&gt;&lt;br&gt;
by default — room for 200 characters, which for an English headline is generous.&lt;br&gt;
&lt;em&gt;"Everything you need to know about our new pricing"&lt;/em&gt; is barely fifty. You'd have&lt;br&gt;
to write a paragraph to run out.&lt;/p&gt;

&lt;p&gt;Arabic is a different arithmetic, because of what WordPress actually stores in&lt;br&gt;
that column. It doesn't keep the raw Arabic text — it stores the&lt;br&gt;
&lt;strong&gt;percent-encoded&lt;/strong&gt; form, the same &lt;code&gt;%XX&lt;/code&gt; sequence that travels in the URL. Take a&lt;br&gt;
single word:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;الذكاء  →  %d8%a7%d9%84%d8%b0%d9%83%d8%a7%d8%a1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every Arabic letter is two bytes in UTF-8, and every byte becomes a&lt;br&gt;
three-character &lt;code&gt;%XX&lt;/code&gt; token. So &lt;strong&gt;one Arabic character costs about six characters&lt;br&gt;
of column space.&lt;/strong&gt; Do the division: &lt;code&gt;VARCHAR(200)&lt;/code&gt; holds roughly &lt;strong&gt;33 Arabic&lt;br&gt;
characters.&lt;/strong&gt; A normal news headline — &lt;em&gt;"القبض على المتهمين في قضية الاحتيال&lt;br&gt;
الإلكتروني"&lt;/em&gt; — blows past that before it's halfway done.&lt;/p&gt;

&lt;p&gt;So Arabic publishers learn early to widen the columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;wp_posts&lt;/span&gt; &lt;span class="k"&gt;MODIFY&lt;/span&gt; &lt;span class="n"&gt;post_name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;wp_terms&lt;/span&gt; &lt;span class="k"&gt;MODIFY&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt;      &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;VARCHAR(1024)&lt;/code&gt; holds about 170 Arabic characters — comfortable. Permalinks&lt;br&gt;
resolve, the newsroom publishes, and the problem looks solved.&lt;/p&gt;

&lt;p&gt;It isn't. It's about &lt;strong&gt;one-third&lt;/strong&gt; solved — and the other two-thirds are waiting&lt;br&gt;
for the next core update. (This isn't only an Arabic problem, either: any&lt;br&gt;
non-Latin script that percent-encodes — Persian, Urdu, Hebrew, Greek — hits the&lt;br&gt;
same wall. Arabic publishers just hit it first and hardest.)&lt;/p&gt;
&lt;h2&gt;
  
  
  The number 200 lives in three places, not one
&lt;/h2&gt;

&lt;p&gt;Here's the insight that turns this from a recurring mystery into something you&lt;br&gt;
can actually close out. WordPress hard-codes "200" in &lt;strong&gt;three independent&lt;br&gt;
places&lt;/strong&gt;, and widening the column only touches the first:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What it caps at 200&lt;/th&gt;
&lt;th&gt;Does widening the column fix it?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1 — Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the column that physically stores the slug&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; — but a core update silently undoes it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2 — Generation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;every new slug, the moment it is created&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt; — it's PHP; needs a code override&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3 — De-duplication&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;slugs that collide and need a numeric suffix&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt; — but this only fires on collisions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Your &lt;code&gt;ALTER&lt;/code&gt; fixes Layer 1's &lt;em&gt;storage&lt;/em&gt;. But Layer 2 means that even with a&lt;br&gt;
1024-wide column, every new slug is still cut to 200 bytes &lt;em&gt;before it's ever&lt;br&gt;
saved&lt;/em&gt; — unless something on your site already overrode it. (If long Arabic&lt;br&gt;
slugs save correctly for you today, a plugin, your theme, or a past developer&lt;br&gt;
already patched Layer 2 — which means you have an undocumented dependency you&lt;br&gt;
didn't know you were relying on.) And Layer 1, the storage you so carefully&lt;br&gt;
widened, is exactly the one a core update quietly reverts. Let's take each in&lt;br&gt;
turn.&lt;/p&gt;
&lt;h2&gt;
  
  
  Layer 1 — how a core update erases your column width
&lt;/h2&gt;

&lt;p&gt;WordPress keeps a canonical description of its own database schema, and on every&lt;br&gt;
major update it reconciles the live database against that description. The chain&lt;br&gt;
is short and deterministic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A new release bumps &lt;code&gt;$wp_db_version&lt;/code&gt; in &lt;code&gt;version.php&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On the next admin request, &lt;code&gt;wp_upgrade()&lt;/code&gt; notices the bump and runs the
database upgrade.&lt;/li&gt;
&lt;li&gt;That calls &lt;code&gt;make_db_current_silent()&lt;/code&gt;, which calls
&lt;code&gt;dbDelta( wp_get_db_schema() )&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wp_get_db_schema()&lt;/code&gt; is the canonical schema — and it says, verbatim:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;post_name&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;slug&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;dbDelta()&lt;/code&gt; then runs a &lt;code&gt;DESCRIBE&lt;/code&gt; on every live table, compares each column to&lt;br&gt;
the canonical definition, and for any mismatch emits an &lt;code&gt;ALTER TABLE … CHANGE&lt;br&gt;
COLUMN&lt;/code&gt; to "correct" it. Your &lt;code&gt;post_name&lt;/code&gt; is &lt;code&gt;VARCHAR(1024)&lt;/code&gt;; the canonical says&lt;br&gt;
&lt;code&gt;VARCHAR(200)&lt;/code&gt;; they don't match — so dbDelta dutifully issues a &lt;code&gt;CHANGE COLUMN&lt;/code&gt;&lt;br&gt;
back to 200, MySQL rebuilds the table, and every byte past the 200th is&lt;br&gt;
discarded.&lt;/p&gt;

&lt;p&gt;Here's the detail that makes it unrecoverable. Years ago, core added &lt;em&gt;downsize&lt;br&gt;
protection&lt;/em&gt; to dbDelta — but &lt;strong&gt;only for &lt;code&gt;TEXT&lt;/code&gt; and &lt;code&gt;BLOB&lt;/code&gt; columns&lt;/strong&gt; (it was&lt;br&gt;
&lt;a href="https://core.trac.wordpress.org/ticket/36748" rel="noopener noreferrer"&gt;Trac #36748&lt;/a&gt;, born from exactly&lt;br&gt;
this kind of data loss). &lt;code&gt;VARCHAR&lt;/code&gt; got no such guard. So a &lt;code&gt;TEXT&lt;/code&gt; column is safe&lt;br&gt;
from accidental shrinking; your &lt;code&gt;VARCHAR(1024)&lt;/code&gt; slug column is not. dbDelta&lt;br&gt;
shrinks it without hesitation, and restoring the column to 1024 afterwards brings&lt;br&gt;
back the width but &lt;strong&gt;not the data&lt;/strong&gt; — those bytes were handed to MySQL and&lt;br&gt;
dropped.&lt;/p&gt;

&lt;p&gt;That's why your fix keeps coming undone: always right after an update, always&lt;br&gt;
silently. It isn't a bug aimed at you — it's core doing exactly what it was&lt;br&gt;
designed to do, to a column you were never expected to change.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For accuracy: a separate routine, &lt;code&gt;pre_schema_upgrade()&lt;/code&gt;, also runs hand-written&lt;br&gt;
&lt;code&gt;ALTER&lt;/code&gt;s, but those are version-gated to historic migrations and won't touch a&lt;br&gt;
modern site's &lt;code&gt;post_name&lt;/code&gt;. The reversion you're seeing is dbDelta.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  The fix — don't repair the damage, prevent the shrink
&lt;/h2&gt;

&lt;p&gt;The instinct is to write something that detects the reverted column and&lt;br&gt;
re-widens it after each update. Resist it. By the time you can detect the shrink,&lt;br&gt;
the data is already gone — you'd just be putting an empty wider column back over&lt;br&gt;
truncated slugs. The only fix that actually &lt;em&gt;saves the data&lt;/em&gt; is one that stops&lt;br&gt;
the shrink from ever happening.&lt;/p&gt;

&lt;p&gt;And core hands you the seam to do it. dbDelta exposes a filter,&lt;br&gt;
&lt;code&gt;dbdelta_create_queries&lt;/code&gt;, that receives the canonical &lt;code&gt;CREATE TABLE&lt;/code&gt; statements&lt;br&gt;
&lt;em&gt;before&lt;/em&gt; they're compared to the live database. Rewrite the canonical&lt;br&gt;
&lt;code&gt;post_name varchar(200)&lt;/code&gt; to &lt;code&gt;varchar(1024)&lt;/code&gt; in that filter, and dbDelta's idea of&lt;br&gt;
"correct" now matches what's actually on disk. It sees no mismatch. It emits no&lt;br&gt;
&lt;code&gt;CHANGE COLUMN&lt;/code&gt;. There is nothing to truncate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="cd"&gt;/**
 * Plugin Name: Arabic Slug Schema Guard (must-use)
 * Description: Keeps wp_posts.post_name and wp_terms.slug at VARCHAR(1024) across
 *              core DB upgrades, so long Arabic slugs are never truncated.
 *              Prevention-first: dbDelta never even tries to shrink them.
 */&lt;/span&gt;
&lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ABSPATH'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;SLUG_COLUMN_LEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Rewrite the canonical schema dbDelta diffs against, so "desired" already&lt;/span&gt;
&lt;span class="c1"&gt;// equals the live 1024-wide column — no CHANGE COLUMN is ever emitted.&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'dbdelta_create_queries'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$queries&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'post_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;terms&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'slug'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$column&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$queries&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$table&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="nv"&gt;$queries&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'/(\b'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$column&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'\s+varchar\()\s*200\s*(\))/i'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'${1}'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="no"&gt;SLUG_COLUMN_LEN&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'${2}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nv"&gt;$queries&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$table&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="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$queries&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;That single filter is the whole prevention story for Layer 1, and it's robust in&lt;br&gt;
a way an after-the-fact repair can never be: it runs &lt;em&gt;inside&lt;/em&gt; dbDelta, so it&lt;br&gt;
covers &lt;strong&gt;every&lt;/strong&gt; path that triggers a schema reconcile — the admin "database&lt;br&gt;
update required" screen, background auto-updates, and &lt;code&gt;wp core update-db&lt;/code&gt; on the&lt;br&gt;
CLI alike.&lt;/p&gt;

&lt;p&gt;Where this code lives matters as much as what it does. Put it in a &lt;strong&gt;must-use&lt;br&gt;
plugin&lt;/strong&gt; — a single &lt;code&gt;.php&lt;/code&gt; file in &lt;code&gt;wp-content/mu-plugins/&lt;/code&gt; — not a regular&lt;br&gt;
plugin. MU-plugins always load, can't be deactivated from the dashboard, and load&lt;br&gt;
&lt;em&gt;before&lt;/em&gt; the upgrade routine runs. And because a core update only ever replaces&lt;br&gt;
&lt;code&gt;wp-admin&lt;/code&gt;, &lt;code&gt;wp-includes&lt;/code&gt; and the root PHP files — it never touches &lt;code&gt;wp-content&lt;/code&gt; —&lt;br&gt;
your guard is guaranteed to still be in place at the exact moment the upgrade&lt;br&gt;
fires. A regular plugin could be deactivated the one time it mattered; don't bet&lt;br&gt;
your archive on a toggle.&lt;/p&gt;
&lt;h2&gt;
  
  
  Layer 2 — stop new slugs truncating at birth
&lt;/h2&gt;

&lt;p&gt;Prevention on the column keeps your &lt;em&gt;existing&lt;/em&gt; slugs safe. But Layer 2 is still&lt;br&gt;
cutting every &lt;em&gt;new&lt;/em&gt; slug to 200 bytes at the moment of creation, inside&lt;br&gt;
&lt;code&gt;sanitize_title_with_dashes()&lt;/code&gt;. The relevant line, unchanged in current core, is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;utf8_uri_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That function is attached to the &lt;code&gt;sanitize_title&lt;/code&gt; filter, so you can cleanly swap&lt;br&gt;
it for an identical copy that uses a larger byte budget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Layer 2: replace core's slug generator with a byte-for-byte copy whose only&lt;/span&gt;
&lt;span class="c1"&gt;// change is the encoding cap. The copy self-tests against core (see below), so a&lt;/span&gt;
&lt;span class="c1"&gt;// future core rewrite of this function is caught instead of drifting silently.&lt;/span&gt;
&lt;span class="nf"&gt;remove_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'sanitize_title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sanitize_title_with_dashes'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'sanitize_title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'asg_sanitize_title_with_dashes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;asg_sanitize_title_with_dashes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$raw_title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'display'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// … the body is line-for-line core, with one change:&lt;/span&gt;
    &lt;span class="c1"&gt;//     utf8_uri_encode( $title, 200 )  →  utf8_uri_encode( $title, 1000 )&lt;/span&gt;
    &lt;span class="c1"&gt;// 1000 leaves headroom under the 1024 column for a "-2" collision suffix.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick a sane budget — around 1000 bytes (≈ 160 Arabic characters) leaves room&lt;br&gt;
under the column for the numeric suffix WordPress adds to resolve duplicates. You&lt;br&gt;
don't want &lt;em&gt;unlimited&lt;/em&gt; slugs; you want &lt;em&gt;enough.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Layer 3 — &lt;code&gt;_truncate_post_slug()&lt;/code&gt; — caps at 200 too, but it only runs when a slug&lt;br&gt;
collides and needs a &lt;code&gt;-2&lt;/code&gt; suffix. For unique news headlines that almost never&lt;br&gt;
fires, and it isn't filterable, so most sites can leave it alone. If you&lt;br&gt;
genuinely need more than 200 bytes &lt;em&gt;even on collisions&lt;/em&gt;, you take over uniqueness&lt;br&gt;
through the &lt;code&gt;pre_wp_unique_post_slug&lt;/code&gt; filter — an advanced case worth a footnote,&lt;br&gt;
not a paragraph.&lt;/p&gt;
&lt;h3&gt;
  
  
  Keeping the fork honest
&lt;/h3&gt;

&lt;p&gt;Copying a core function buys a maintenance debt: the day WordPress rewrites&lt;br&gt;
&lt;code&gt;sanitize_title_with_dashes()&lt;/code&gt;, your copy diverges — &lt;em&gt;silently&lt;/em&gt;, which is the exact&lt;br&gt;
failure mode we're trying to kill. Pinning the copy to a core version and re-diffing&lt;br&gt;
by hand works right up until the release someone forgets to check, which on a&lt;br&gt;
long-lived site is &lt;em&gt;when&lt;/em&gt;, not &lt;em&gt;if&lt;/em&gt;. So don't rely on remembering. Make the plugin&lt;br&gt;
re-diff itself.&lt;/p&gt;

&lt;p&gt;The lever: &lt;code&gt;remove_filter()&lt;/code&gt; only unhooks the function — &lt;code&gt;sanitize_title_with_dashes()&lt;/code&gt;&lt;br&gt;
stays &lt;strong&gt;callable&lt;/strong&gt;. So you can use core's own current code as a live oracle and assert&lt;br&gt;
your fork still agrees with it on &lt;strong&gt;short&lt;/strong&gt; inputs, where neither byte cap fires and&lt;br&gt;
the only thing left that could differ is the cleanup logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Run once per core version. Short fixtures =&amp;gt; neither length cap engages,&lt;/span&gt;
&lt;span class="c1"&gt;// so any difference is pure logic drift, not the byte budget.&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'الذكاء الاصطناعي'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'عاجل: «مهم» اليوم'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'foo/bar &amp;amp;mdash; baz'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$f&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;sanitize_title_with_dashes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'save'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nf"&gt;asg_sanitize_title_with_dashes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'save'&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="c1"&gt;// core changed the cleanup logic — log + email ops, and (optionally) fall&lt;/span&gt;
        &lt;span class="c1"&gt;// back to core's generator so slugs cap at 200 again: degraded, but safe&lt;/span&gt;
        &lt;span class="c1"&gt;// and known, never a malformed slug from a stale copy.&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;This beats a source-hash check on two counts. It's &lt;strong&gt;semantic, not textual&lt;/strong&gt; — it&lt;br&gt;
stays quiet when core only touches the length handling (which you override anyway)&lt;br&gt;
and fires only when a change actually breaks &lt;em&gt;your&lt;/em&gt; output, so the alerts that do&lt;br&gt;
fire get acted on. And it &lt;strong&gt;degrades safely&lt;/strong&gt;: on drift you fall back to core's exact&lt;br&gt;
behaviour — worst case, new slugs cap at 200 until you re-sync, never a half-encoded&lt;br&gt;
slug from a stale copy. Loud-and-safe beats silent-and-wrong. The full guard (with&lt;br&gt;
the per-core-version cache and the optional fail-safe switch) is in the complete&lt;br&gt;
plugin below.&lt;/p&gt;
&lt;h2&gt;
  
  
  A tripwire, because silent failure is the real enemy
&lt;/h2&gt;

&lt;p&gt;The filter prevents the known incident. But the failure mode that actually hurt&lt;br&gt;
you was that it happened &lt;em&gt;silently&lt;/em&gt; — so the last piece is making sure you'd&lt;br&gt;
never again learn about it from a drop in search traffic. The same MU-plugin can&lt;br&gt;
verify the column widths after every update and shout if anything is wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hook &lt;code&gt;upgrader_process_complete&lt;/code&gt; to flag that a core update just ran.&lt;/li&gt;
&lt;li&gt;On the next admin request, check the real column widths in &lt;code&gt;information_schema&lt;/code&gt;
and, if either has shrunk, write to the error log and email ops.&lt;/li&gt;
&lt;li&gt;Expose a WP-CLI command for cron-based monitoring:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Nightly: verify the slug columns, email ops if either has reverted.&lt;/span&gt;
0 3 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="nb"&gt;cd&lt;/span&gt; /var/www/site &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; wp asg verify | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; REVERTED &lt;span class="se"&gt;\&lt;/span&gt;
           &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; wp asg verify | mail &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"WP slug schema reverted on &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; ops@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;One discipline to internalise: this tripwire restores &lt;em&gt;confidence&lt;/em&gt;, not &lt;em&gt;data&lt;/em&gt;.&lt;br&gt;
If it ever reports a reverted column, treat it as an incident, not a self-healing&lt;br&gt;
event — the slugs longer than the new width are already gone and must come back&lt;br&gt;
from a backup. Prevention (the filter) and backups are your safety net; the&lt;br&gt;
tripwire just tells you when to reach for them.&lt;/p&gt;
&lt;h2&gt;
  
  
  "Isn't VARCHAR(1024) dangerous?" — no, and the reason is reassuring
&lt;/h2&gt;

&lt;p&gt;Widening these columns &lt;em&gt;sounds&lt;/em&gt; risky — indexes, utf8mb4, InnoDB limits — and&lt;br&gt;
it's the part people most often get scared off by. It's almost entirely a&lt;br&gt;
non-issue, and one fact explains why:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The index on these columns is a fixed 191-character &lt;em&gt;prefix&lt;/em&gt;, independent of&lt;br&gt;
the column's declared length.&lt;/strong&gt; Core defines &lt;code&gt;KEY post_name (post_name(191))&lt;/code&gt; — it&lt;br&gt;
indexes the first 191 characters whether the column is 200 wide or 1024. Widening&lt;br&gt;
the column doesn't enlarge the index by a single byte.&lt;/p&gt;

&lt;p&gt;That dissolves the usual worries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Index / InnoDB prefix limits.&lt;/strong&gt; The old InnoDB prefix ceiling is 767 bytes;
WordPress chose 191 because 191 × 4 (utf8mb4's worst case) = 764, just under it.
Because the prefix stays at 191 no matter how wide the column grows, you never
move &lt;em&gt;towards&lt;/em&gt; that limit. (A quiet irony: slugs are stored as percent-encoded
ASCII, so they were never going to hit four bytes per character anyway.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row size.&lt;/strong&gt; &lt;code&gt;VARCHAR&lt;/code&gt; stores the &lt;em&gt;actual&lt;/em&gt; length of each value, not its
declared maximum — a 30-character slug takes ~30 bytes in a &lt;code&gt;VARCHAR(1024)&lt;/code&gt;
exactly as it would in a &lt;code&gt;VARCHAR(200)&lt;/code&gt;. Rows don't get bigger because the
ceiling moved up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query plans.&lt;/strong&gt; Lookups use the same 191-prefix index to narrow, then verify
the full value. Identical behaviour at 200 or 1024.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugins.&lt;/strong&gt; SEO plugins, caches and CDNs read &lt;code&gt;post_name&lt;/code&gt; and emit longer URLs
without complaint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The genuine risks aren't in steady state — they're tools that &lt;strong&gt;recreate the&lt;br&gt;
schema&lt;/strong&gt; from an old definition: a migration plugin that runs its own&lt;br&gt;
&lt;code&gt;CREATE TABLE&lt;/code&gt;, or, most commonly, restoring a SQL dump taken &lt;em&gt;before&lt;/em&gt; you widened&lt;br&gt;
the columns. A dump of the &lt;em&gt;current&lt;/em&gt; database preserves &lt;code&gt;VARCHAR(1024)&lt;/code&gt;&lt;br&gt;
correctly; importing an old structure silently puts you back at 200. Write that&lt;br&gt;
one down in your runbook.&lt;/p&gt;
&lt;h2&gt;
  
  
  Doing it safely on a site with millions of posts
&lt;/h2&gt;

&lt;p&gt;There's one operational gotcha, and it's about the migration itself, not the&lt;br&gt;
result. Going from &lt;code&gt;VARCHAR(200)&lt;/code&gt; to &lt;code&gt;VARCHAR(1024)&lt;/code&gt; &lt;strong&gt;cannot&lt;/strong&gt; be an instant,&lt;br&gt;
in-place change. InnoDB only keeps a &lt;code&gt;VARCHAR&lt;/code&gt; length change "in place" while both&lt;br&gt;
lengths stay in the same size class — values under 256 use one length byte,&lt;br&gt;
values 256 and over use two. Crossing 255 forces &lt;code&gt;ALGORITHM=COPY&lt;/code&gt;: a full table&lt;br&gt;
rebuild. On a &lt;code&gt;wp_posts&lt;/code&gt; with millions of rows, a raw &lt;code&gt;ALTER&lt;/code&gt; means a long&lt;br&gt;
operation and a problematic lock.&lt;/p&gt;

&lt;p&gt;So on a large live site, do the initial widening with an online schema-change&lt;br&gt;
tool, not a bare &lt;code&gt;ALTER&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pt-online-schema-change &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--alter&lt;/span&gt; &lt;span class="s2"&gt;"MODIFY post_name VARCHAR(1024) NOT NULL DEFAULT ''"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--chunk-time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.5 &lt;span class="nt"&gt;--max-load&lt;/span&gt; &lt;span class="nv"&gt;Threads_running&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--execute&lt;/span&gt; &lt;span class="nv"&gt;D&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wordpress,t&lt;span class="o"&gt;=&lt;/span&gt;wp_posts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;gh-ost&lt;/code&gt; is an equally good triggerless alternative.) &lt;code&gt;wp_terms&lt;/code&gt; is tiny and&lt;br&gt;
takes a plain &lt;code&gt;ALTER&lt;/code&gt; without ceremony; &lt;code&gt;wp_posts&lt;/code&gt; is the one that needs the&lt;br&gt;
online tool. The rollout that's bitten no one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Back up&lt;/strong&gt;, and confirm the dump shows &lt;code&gt;VARCHAR(1024)&lt;/code&gt; afterwards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clone to staging&lt;/strong&gt;, deploy the MU-plugin, and run a &lt;em&gt;real&lt;/em&gt; major core update
there. Confirm no &lt;code&gt;CHANGE COLUMN&lt;/code&gt; appears and the verify command reports &lt;code&gt;OK&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy the MU-plugin to production &lt;em&gt;before&lt;/em&gt; the next core update&lt;/strong&gt; —
prevention has to be in place ahead of the event it prevents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Widen the production columns off-peak&lt;/strong&gt; with pt-osc / gh-ost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a nightly verify cron and a 404 monitor&lt;/strong&gt;, so anything unexpected
surfaces in hours, not in a month of lost rankings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the MU-plugin in version control&lt;/strong&gt;, and never import a pre-widening
structure dump.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  The deeper fix — stop betting your URLs on a column width
&lt;/h2&gt;

&lt;p&gt;Everything above makes Arabic slugs safe. But there's a more permanent way to&lt;br&gt;
think about it, and for a large news archive it's worth the shift: &lt;strong&gt;don't let&lt;br&gt;
the URL depend on the slug surviving at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Anchor your permalinks on the numeric post ID, and keep the Arabic slug as a&lt;br&gt;
descriptive — but cosmetic — suffix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/123456/الذكاء-الاصطناعي
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resolve the post by its ID, and the slug becomes decoration. If it's ever&lt;br&gt;
truncated, malformed, or edited, the URL &lt;strong&gt;still resolves&lt;/strong&gt; to the right article&lt;br&gt;
instead of 404-ing — and WordPress can &lt;code&gt;301&lt;/code&gt; to the canonical form. You keep the&lt;br&gt;
Arabic keywords in the URL for search relevance &lt;em&gt;and&lt;/em&gt; you become structurally&lt;br&gt;
immune to the entire class of problem. (If you want to stop touching core tables&lt;br&gt;
altogether, you can go one step further and store the long slug in your own&lt;br&gt;
indexed routing table, which dbDelta has no opinion about — but for most teams,&lt;br&gt;
ID-anchored permalinks are the sweet spot.)&lt;/p&gt;

&lt;p&gt;That's the difference between patching a bug and designing it out.&lt;/p&gt;
&lt;h2&gt;
  
  
  The complete plugin
&lt;/h2&gt;

&lt;p&gt;Everything above, assembled into one must-use plugin. Drop it in&lt;br&gt;
&lt;code&gt;wp-content/mu-plugins/arabic-slug-schema-guard.php&lt;/code&gt;, widen the columns once with&lt;br&gt;
an online schema-change tool, and all three layers — plus the tripwire — are&lt;br&gt;
handled in a single place. The slug generator is a copy of core's&lt;br&gt;
&lt;code&gt;sanitize_title_with_dashes()&lt;/code&gt;; only the byte cap differs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📦 &lt;strong&gt;Prefer it ready-made?&lt;/strong&gt; The plugin is open-source on GitHub as &lt;a href="https://github.com/mantekio/arabic-slug-schema-guard" rel="noopener noreferrer"&gt;&lt;code&gt;mantekio/arabic-slug-schema-guard&lt;/code&gt;&lt;/a&gt; — or &lt;code&gt;composer require mantekio/arabic-slug-schema-guard&lt;/code&gt;. Star it, file an issue, or lift it whole.&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="cd"&gt;/**
 * Plugin Name: Arabic Slug Schema Guard (must-use)
 * Description: Keeps wp_posts.post_name and wp_terms.slug at VARCHAR(1024) across
 *              core DB upgrades, lets core generate long URL-encoded Arabic slugs,
 *              and verifies + alerts after every update. Prevention-first:
 *              dbDelta never truncates, because the canonical schema it diffs
 *              against already says 1024. The slug fork self-tests against core
 *              on each update and can fail safe.
 */&lt;/span&gt;
&lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ABSPATH'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;ASG_COLUMN_LEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// physical column width (bytes)&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;ASG_SLUG_BYTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// max generated slug length, under the column width&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;ASG_SCHEMA_SIG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'post_name:1024;slug:1024'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// define( 'ASG_ALERT_EMAIL', 'ops@example.com' );  // optional, set in wp-config.php&lt;/span&gt;
&lt;span class="c1"&gt;// define( 'ASG_L2_FAILSAFE', true );               // optional: on Layer-2 drift, fall back to core (slugs cap at 200) until you re-sync&lt;/span&gt;

&lt;span class="cm"&gt;/* LAYER 1 — PREVENTION
 * Rewrite the canonical CREATE TABLE dbDelta diffs against, so "desired" already
 * equals the live 1024-wide column and no destructive CHANGE COLUMN is emitted. */&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'dbdelta_create_queries'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$queries&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'post_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;terms&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'slug'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$column&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$queries&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$table&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="nv"&gt;$queries&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'/(\b'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$column&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'\s+varchar\()\s*200\s*(\))/i'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'${1}'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="no"&gt;ASG_COLUMN_LEN&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'${2}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nv"&gt;$queries&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$table&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="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$queries&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="cm"&gt;/* LAYER 2 — GENERATION
 * Core caps new slugs at 200 bytes via utf8_uri_encode($title, 200). Swap in a
 * copy of sanitize_title_with_dashes() whose only change is the byte budget.
 * Installed conditionally: if the drift guard (below) flagged THIS core version
 * and ASG_L2_FAILSAFE is on, stay on core's default (caps at 200 — degraded but
 * safe and known) rather than run a fork we know has drifted. */&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;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'asg_l2_status'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'drift:'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$GLOBALS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'wp_version'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
     &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ASG_L2_FAILSAFE'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="no"&gt;ASG_L2_FAILSAFE&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Known-drifted on this core version → leave core's generator in place.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;remove_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'sanitize_title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sanitize_title_with_dashes'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'sanitize_title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'asg_sanitize_title_with_dashes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;asg_sanitize_title_with_dashes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$raw_title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'display'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strip_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Preserve already-encoded octets through the cleanup below.&lt;/span&gt;
    &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'|%([a-fA-F0-9][a-fA-F0-9])|'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'---$1---'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'%'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'|---([a-fA-F0-9][a-fA-F0-9])---|'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'%$1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$title&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;seems_utf8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$title&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;function_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'mb_strtolower'&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="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;mb_strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'UTF-8'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;utf8_uri_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ASG_SLUG_BYTES&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// core hard-codes 200 here&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$title&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="s1"&gt;'save'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Turn non-breaking spaces, en/em dashes and slashes into hyphens.&lt;/span&gt;
        &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'%c2%a0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'%e2%80%93'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'%e2%80%94'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'&amp;amp;nbsp;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;amp;#160;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;amp;ndash;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;amp;#8211;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;amp;mdash;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;amp;#8212;'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'/&amp;amp;.+?;/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// strip remaining entities&lt;/span&gt;
    &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'/[^%a-z0-9 _-]/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// keep only slug-safe chars&lt;/span&gt;
    &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'/\s+/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'|-+|'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$title&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;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/* LAYER 2 DRIFT GUARD — self-test the fork against core's live implementation.
 * remove_filter() only unhooks sanitize_title_with_dashes(); the function stays
 * callable, so we use core's CURRENT code as a live oracle and assert our fork
 * still agrees on SHORT inputs (where neither byte cap engages — the only thing
 * the fork changed). Any divergence == core altered the cleanup logic and our
 * copy has drifted. Runs once per core version; shouts, and can fail safe. */&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'admin_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'asg_guard_layer2_drift'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;asg_guard_layer2_drift&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$ok&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ok:'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$GLOBALS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'wp_version'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nv"&gt;$drifted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'drift:'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$GLOBALS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'wp_version'&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="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'asg_l2_status'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$drifted&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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// already settled for this core version&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="nb"&gt;function_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'sanitize_title_with_dashes'&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="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// core refactored it away — no oracle to diff against; the fork&lt;/span&gt;
                 &lt;span class="c1"&gt;// still runs as a standalone copy (it needs no removed function).&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Deliberately SHORT fixtures so neither length cap fires — any difference is&lt;/span&gt;
    &lt;span class="c1"&gt;// pure cleanup-logic drift, not the byte budget.&lt;/span&gt;
    &lt;span class="nv"&gt;$fixtures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'الذكاء الاصطناعي'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'عاجل: تطورات «مهمة» اليوم'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'Mixed عربي + English — dash'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'foo/bar &amp;amp;mdash; baz'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'UPPER   multiple   spaces'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'%d8%a7 pre-encoded octet'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$drift&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$fixtures&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$f&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$core&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sanitize_title_with_dashes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'save'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$ours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;asg_sanitize_title_with_dashes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'save'&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="nv"&gt;$core&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$ours&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$drift&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"in=[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] core=[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$core&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] ours=[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$ours&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;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="nv"&gt;$drift&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"[Arabic Slug Schema Guard] Layer-2 DRIFT on WP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$GLOBALS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'wp_version'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: core "&lt;/span&gt;
             &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"sanitize_title_with_dashes() no longer matches our fork — re-sync the copy.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
             &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$drift&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$msg&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="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ASG_ALERT_EMAIL'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;function_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_mail'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;wp_mail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="no"&gt;ASG_ALERT_EMAIL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'WP slug Layer-2 drift: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;wp_parse_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;home_url&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;PHP_URL_HOST&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;$msg&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nf"&gt;update_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'asg_l2_status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$drifted&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="c1"&gt;// autoload — the fail-safe reads it every request&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// do NOT cache "ok" while drifted&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;update_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'asg_l2_status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$ok&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="cm"&gt;/* LAYER 3 — DE-DUPLICATION (optional)
 * _truncate_post_slug() also caps at 200, but only when a slug COLLIDES and needs
 * a "-2" suffix — rare for unique headlines, and it isn't filterable. If you need
 * &amp;gt;200 bytes even on collisions, take over uniqueness here. Most sites skip this.
 *
 * add_filter( 'pre_wp_unique_post_slug', 'asg_unique_post_slug', 10, 6 );
 */&lt;/span&gt;

&lt;span class="cm"&gt;/* TRIPWIRE — verify + alert after every core update
 * This only detects and alerts: it restores the COLUMN, never bytes already
 * truncated. Treat any revert as an incident — restore from backup. */&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'upgrader_process_complete'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$upgrader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$extra&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$extra&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'type'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="s1"&gt;'core'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$extra&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'type'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;delete_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'asg_schema_sig'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// force a re-check on the next admin_init&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&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="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'admin_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'asg_schema_sig'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="no"&gt;ASG_SCHEMA_SIG&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// fast path: already verified since the last update&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;$reverted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;asg_reverted_columns&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="nv"&gt;$reverted&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'[Arabic Slug Schema Guard] Column(s) reverted: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$reverted&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
             &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'. Long slugs were truncated — restore from backup and check 404 logs.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$msg&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="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ASG_ALERT_EMAIL'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;wp_mail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="no"&gt;ASG_ALERT_EMAIL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'WP slug schema reverted: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;wp_parse_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;home_url&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;PHP_URL_HOST&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;$msg&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// do NOT cache "ok" while broken&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;update_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'asg_schema_sig'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ASG_SCHEMA_SIG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;asg_reverted_columns&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$reverted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'post_name'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;terms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'slug'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$column&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="nv"&gt;$len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;"SELECT CHARACTER_MAXIMUM_LENGTH FROM information_schema.COLUMNS
              WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="no"&gt;DB_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$column&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$len&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$len&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ASG_COLUMN_LEN&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$reverted&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$column&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (now &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$len&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$reverted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/* WP-CLI — `wp asg verify` for cron-based monitoring */&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WP_CLI'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="no"&gt;WP_CLI&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="no"&gt;WP_CLI&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;add_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'asg verify'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;asg_reverted_columns&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ok'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="no"&gt;WP_CLI&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ok'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'Schema OK — both columns at VARCHAR(1024).'&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"REVERTED: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;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;This is the kind of problem that never shows up in a tutorial. It lives where&lt;br&gt;
WordPress internals, MySQL's storage rules, Arabic character encoding and SEO all&lt;br&gt;
overlap, and it only reveals itself at the worst possible moment. It's also the&lt;br&gt;
territory we work in every day — keeping Arabic publishing fast, multilingual and&lt;br&gt;
unbreakable on WordPress, &lt;a href="https://www.mantek.io/wordpress-aws" rel="noopener noreferrer"&gt;right through to AWS at scale&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you run a WordPress publication in Arabic and want it to survive its own&lt;br&gt;
updates, &lt;a href="https://www.mantek.io/contact" rel="noopener noreferrer"&gt;let's talk&lt;/a&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.wordpress.org/reference/functions/dbdelta/" rel="noopener noreferrer"&gt;&lt;code&gt;dbDelta()&lt;/code&gt;&lt;/a&gt; and the &lt;a href="https://developer.wordpress.org/reference/hooks/dbdelta_create_queries/" rel="noopener noreferrer"&gt;&lt;code&gt;dbdelta_create_queries&lt;/code&gt; hook&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://core.trac.wordpress.org/ticket/36748" rel="noopener noreferrer"&gt;Trac #36748&lt;/a&gt; — the TEXT/BLOB downsize protection that VARCHAR never got&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.wordpress.org/reference/functions/sanitize_title_with_dashes/" rel="noopener noreferrer"&gt;&lt;code&gt;sanitize_title_with_dashes()&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developer.wordpress.org/reference/functions/_truncate_post_slug/" rel="noopener noreferrer"&gt;&lt;code&gt;_truncate_post_slug()&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.wordpress.org/reference/hooks/upgrader_process_complete/" rel="noopener noreferrer"&gt;&lt;code&gt;upgrader_process_complete&lt;/code&gt; hook&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>webdev</category>
      <category>seo</category>
    </item>
    <item>
      <title>How we built mantek.io for the price of a domain</title>
      <dc:creator>Jaafar Abazid</dc:creator>
      <pubDate>Tue, 16 Jun 2026 23:43:33 +0000</pubDate>
      <link>https://dev.to/mantekio/how-we-built-mantekio-for-the-price-of-a-domain-1c9c</link>
      <guid>https://dev.to/mantekio/how-we-built-mantekio-for-the-price-of-a-domain-1c9c</guid>
      <description>&lt;p&gt;The site you're reading costs us almost nothing to run. There's no server invoice, no hosting bill, no monthly platform fee — just the domain name, renewed once a year. Everything else runs on infrastructure that's free at our scale and fast everywhere in the world.&lt;/p&gt;

&lt;p&gt;That isn't a corner we cut. It's a set of deliberate choices — static output, edge delivery, git as the backbone — that happen to be cheaper &lt;em&gt;and&lt;/em&gt; faster &lt;em&gt;and&lt;/em&gt; simpler than the alternative. Here's the whole stack, and the thinking behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole bill is the domain
&lt;/h2&gt;

&lt;p&gt;Take the site apart and look for the costs, and there's only one line item.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hosting and CDN: nothing.&lt;/strong&gt; The site lives on Cloudflare Pages, which serves it from data centres in hundreds of cities, with unlimited bandwidth on the free tier. The thing that usually costs the most — global delivery — costs zero here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS: nothing.&lt;/strong&gt; Managed on Cloudflare alongside the domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email Sending: nothing.&lt;/strong&gt; Contact-form mail goes through Resend's free tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The domain: the only invoice.&lt;/strong&gt; Renewed yearly, and that's the entire running cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The infrastructure is free; the engineering is the investment. That's the trade — and for a site that has to be fast, bilingual and maintainable for years, it's the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static output, Git as the pipeline
&lt;/h2&gt;

&lt;p&gt;The site is built with Astro and compiled to plain HTML and CSS. There's no application server running in the background — nothing to patch, scale, or wake up when a visitor arrives. Cloudflare serves the finished files from the edge location nearest each reader, which is why pages feel instant.&lt;/p&gt;

&lt;p&gt;Deployment is just git. We push to the main branch; Cloudflare builds the site and publishes it worldwide in a minute or less. Every branch gets its own preview URL, so a change can be reviewed on a real address before it goes live. And because the repository &lt;em&gt;is&lt;/em&gt; the source of truth, undoing a bad change is a git revert — not a 2 a.m. restore from backup.&lt;/p&gt;

&lt;h2&gt;
  
  
  A CMS with no database
&lt;/h2&gt;

&lt;p&gt;This blog is editable in the browser, but there's no database behind it and no CMS server to keep alive. The editor is git-based: saving a post commits a Markdown file to the repository and rides the same automatic deploy as the code.&lt;/p&gt;

&lt;p&gt;That has quiet benefits. Every edit is versioned like code, with full history. There's no database to back up, secure, or migrate. The editor sits behind GitHub sign-in, so there's no separate set of passwords to manage and very little for a scanner to find. Content and code live in one place, and ship the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  A contact form without a back end
&lt;/h2&gt;

&lt;p&gt;A contact form normally means a server somewhere waiting to receive it. Ours is a single serverless function that runs at the edge, on demand: it validates the submission and hands it to Resend, which sends the email. No always-on back end, no queue, no infrastructure to maintain between messages.&lt;/p&gt;

&lt;p&gt;The credentials never touch the codebase. The Resend API key lives in Cloudflare's encrypted environment variables, read by the function at run time — it isn't in the repository and isn't in the published site. Spam is caught with a hidden honeypot field and server-side checks rather than a third-party CAPTCHA. And the form degrades gracefully: with JavaScript switched off it submits natively and returns a thank-you page; with JavaScript on it posts in the background and shows inline status. No cookies, either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chasing a perfect score
&lt;/h2&gt;

&lt;p&gt;A static site is a fast &lt;em&gt;start&lt;/em&gt;, not a finished one. Getting to a perfect Lighthouse score — 100 across Performance, Accessibility, Best Practices and SEO — took a series of small, deliberate decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted fonts, preloaded.&lt;/strong&gt; The fonts are served from our own domain, not a third-party font CDN, and the one the headline renders in is preloaded so it's ready for the first paint. The Arabic typeface only downloads on Arabic pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No render-blocking JavaScript.&lt;/strong&gt; What little script the site needs is inlined and tiny; there's no framework runtime shipping to the browser. These are HTML pages with a few grams of JavaScript, not an app pretending to be a page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One shared stylesheet.&lt;/strong&gt; The CSS is a single file, cached across every page, so moving around the site re-downloads nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Work the browser can skip.&lt;/strong&gt; Sections below the fold are marked so the browser doesn't spend layout effort on them until they're about to be seen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No flash, no shift.&lt;/strong&gt; The colour theme is resolved before the first paint, so nothing flickers from light to dark or jumps around as it loads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these is dramatic on its own. Together they're the difference between "fast enough" and a perfect score.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cached hard, but never stale
&lt;/h2&gt;

&lt;p&gt;Speed and freshness usually pull against each other; the right caching rules give you both. Build assets get fingerprinted filenames, cached for a year and marked &lt;em&gt;immutable&lt;/em&gt; — the browser never asks about them again. Images are cached for a week. But the HTML itself is always revalidated, so the moment we publish, every reader sees the new version.&lt;/p&gt;

&lt;p&gt;The one lesson worth passing on: &lt;em&gt;immutable&lt;/em&gt; means exactly that. It belongs only on files whose name changes when their contents do. Put it on a stable URL and you'll spend a day fighting your own cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secure and findable by default
&lt;/h2&gt;

&lt;p&gt;A set of security headers is applied at the edge, for free: strict HTTPS (HSTS), protection against clickjacking and MIME-sniffing, a conservative referrer policy, and a permissions policy that switches off the camera, microphone and location access the site never uses. There's one canonical hostname; everything else redirects to it.&lt;/p&gt;

&lt;p&gt;Findability is built in the same spirit. Every page declares its canonical URL and its English/Arabic alternates, so search engines read the two languages as one site rather than duplicate content. Each post generates its own social-share card. Structured data, an auto-generated sitemap and an RSS feed ship on every build. And the analytics are cookieless — which means no consent banner slowing the page down or greeting the reader with a pop-up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two languages, one build
&lt;/h2&gt;

&lt;p&gt;The site is fully bilingual: English at the root, Arabic — right-to-left — under &lt;code&gt;/ar/&lt;/code&gt;. There's no duplicate site and no translation plugin. The same components render both languages from a dictionary, down to mirrored layouts and translated structured data. Adding a language is additive, not a rebuild.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're building something similar
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Default to static.&lt;/strong&gt; If a page can be HTML, it should be. Everything downstream — speed, cost, security — gets easier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make git the backbone.&lt;/strong&gt; Deploys, content, rollbacks and history in one place beats four systems that have to be kept in sync.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spend the savings on judgement.&lt;/strong&gt; Free infrastructure frees the budget for the part that actually matters — the engineering.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;This is the same discipline we bring to client work: systems that are fast, cheap to run, and built to last. If you'd like a site that behaves like this one, &lt;a href="https://www.mantek.io/services" rel="noopener noreferrer"&gt;see what we do&lt;/a&gt; or &lt;a href="https://www.mantek.io/contact" rel="noopener noreferrer"&gt;start a conversation&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>cloudflare</category>
      <category>astro</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
