🔧 AI Nachrichten Debian is Voting on Whether to Allow AI-Assisted Contributions(23.08.2026 um 09:34 Uhr)
🔧 AI Nachrichten The Linux Kernel Is Approaching 2,000 CVEs Per Release(29.08.2026 um 20:00 Uhr)
⚠️ Malware / Trojaner / VirenCitrix Adds a Linux-Powered Escape Hatch For Compromised Windows PCs(30.08.2026 um 17:34 Uhr)
🔧 AI Nachrichten Debian is Voting on Whether to Allow AI-Assisted Contributions(23.08.2026 um 09:34 Uhr)
🔧 AI Nachrichten The Linux Kernel Is Approaching 2,000 CVEs Per Release(29.08.2026 um 20:00 Uhr)
⚠️ Malware / Trojaner / VirenCitrix Adds a Linux-Powered Escape Hatch For Compromised Windows PCs(30.08.2026 um 17:34 Uhr)

🔧 Programmierung 🕛 vor 2 Monaten 18 Min Lesezeit
0

The 200-byte trap: why WordPress core updates break Arabic URLs

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

You update WordPress on a quiet afternoon — a routine release, the kind you've

installed a hundred times. The dashboard says everything went fine. Then the

404s start: not a handful, but every long-headlined article in your archive, all

at once, all in Arabic.



Nothing in the update log mentions it. No plugin changed. And the cruel part:

the data that made those URLs work is already gone — shaved off inside a

database-upgrade routine that ran for a few milliseconds and reported success.

This isn't a freak accident or a broken plugin. It's three separate assumptions

baked into WordPress core, each hard-coding the same number — 200 — and

Arabic sites are almost uniquely exposed to all three. We hit this running

WordPress for Arabic newsrooms, the same high-traffic publishing we've written

about , born from exactly

this kind of data loss). VARCHAR got no such guard. So a TEXT column is safe

from accidental shrinking; your VARCHAR(1024) slug column is not. dbDelta

shrinks it without hesitation, and restoring the column to 1024 afterwards brings

back the width but not the data — those bytes were handed to MySQL and

dropped.



That's why your fix keeps coming undone: always right after an update, always

silently. It isn't a bug aimed at you — it's core doing exactly what it was

designed to do, to a column you were never expected to change.




For accuracy: a separate routine, pre_schema_upgrade(), also runs hand-written

ALTERs, but those are version-gated to historic migrations and won't touch a

modern site's post_name. The reversion you're seeing is dbDelta.






The fix — don't repair the damage, prevent the shrink



The instinct is to write something that detects the reverted column and

re-widens it after each update. Resist it. By the time you can detect the shrink,

the data is already gone — you'd just be putting an empty wider column back over

truncated slugs. The only fix that actually saves the data is one that stops

the shrink from ever happening.



And core hands you the seam to do it. dbDelta exposes a filter,

dbdelta_create_queries, that receives the canonical CREATE TABLE statements

before they're compared to the live database. Rewrite the canonical

post_name varchar(200) to varchar(1024) in that filter, and dbDelta's idea of

"correct" now matches what's actually on disk. It sees no mismatch. It emits no

CHANGE COLUMN. There is nothing to truncate.




CODE
<?php
/**
* 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.
*/

defined( 'ABSPATH' ) || exit;

const SLUG_COLUMN_LEN = 1024;

// Rewrite the canonical schema dbDelta diffs against, so "desired" already
// equals the live 1024-wide column — no CHANGE COLUMN is ever emitted.
add_filter( 'dbdelta_create_queries', function ( $queries ) {
global $wpdb;

foreach ( array( $wpdb->posts => 'post_name', $wpdb->terms => 'slug' ) as $table => $column ) {
if ( isset( $queries[ $table ] ) ) {
$queries[ $table ] = preg_replace(
'/(\b' . $column . '\s+varchar\()\s*200\s*(\))/i',
'${1}' . SLUG_COLUMN_LEN . '${2}',
$queries[ $table ]
);
}
}
return $queries;
} );






That single filter is the whole prevention story for Layer 1, and it's robust in

a way an after-the-fact repair can never be: it runs inside dbDelta, so it

covers every path that triggers a schema reconcile — the admin "database

update required" screen, background auto-updates, and wp core update-db on the

CLI alike.



Where this code lives matters as much as what it does. Put it in a must-use

plugin
— a single .php file in wp-content/mu-plugins/ — not a regular

plugin. MU-plugins always load, can't be deactivated from the dashboard, and load

before the upgrade routine runs. And because a core update only ever replaces

wp-admin, wp-includes and the root PHP files — it never touches wp-content

your guard is guaranteed to still be in place at the exact moment the upgrade

fires. A regular plugin could be deactivated the one time it mattered; don't bet

your archive on a toggle.





Layer 2 — stop new slugs truncating at birth



Prevention on the column keeps your existing slugs safe. But Layer 2 is still

cutting every new slug to 200 bytes at the moment of creation, inside

sanitize_title_with_dashes(). The relevant line, unchanged in current core, is:




CODE
$title = utf8_uri_encode( $title, 200 );






That function is attached to the sanitize_title filter, so you can cleanly swap

it for an identical copy that uses a larger byte budget:




CODE
// Layer 2: replace core's slug generator with a byte-for-byte copy whose only
// change is the encoding cap. Re-sync if core ever rewrites this function (rare).
remove_filter( 'sanitize_title', 'sanitize_title_with_dashes' );
add_filter( 'sanitize_title', 'asg_sanitize_title_with_dashes', 10, 3 );

function asg_sanitize_title_with_dashes( $title, $raw_title = '', $context = 'display' ) {
// … the body is line-for-line core, with one change:
// utf8_uri_encode( $title, 200 ) → utf8_uri_encode( $title, 1000 )
// 1000 leaves headroom under the 1024 column for a "-2" collision suffix.
}






Pick a sane budget — around 1000 bytes (≈ 160 Arabic characters) leaves room

under the column for the numeric suffix WordPress adds to resolve duplicates. You

don't want unlimited slugs; you want enough.



Layer 3 — _truncate_post_slug() — caps at 200 too, but it only runs when a slug

collides and needs a -2 suffix. For unique news headlines that almost never

fires, and it isn't filterable, so most sites can leave it alone. If you

genuinely need more than 200 bytes even on collisions, you take over uniqueness

through the pre_wp_unique_post_slug filter — an advanced case worth a footnote,

not a paragraph.





A tripwire, because silent failure is the real enemy



The filter prevents the known incident. But the failure mode that actually hurt

you was that it happened silently — so the last piece is making sure you'd

never again learn about it from a drop in search traffic. The same MU-plugin can

verify the column widths after every update and shout if anything is wrong:




  • Hook upgrader_process_complete to flag that a core update just ran.

  • On the next admin request, check the real column widths in information_schema
    and, if either has shrunk, write to the error log and email ops.

  • Expose a WP-CLI command for cron-based monitoring:



CODE
# Nightly: verify the slug columns, email ops if either has reverted.
0 3 * * * cd /var/www/site && wp asg verify | grep -q REVERTED \
&& wp asg verify | mail -s "WP slug schema reverted on $(hostname)" [email protected]





One discipline to internalise: this tripwire restores confidence, not data.

If it ever reports a reverted column, treat it as an incident, not a self-healing

event — the slugs longer than the new width are already gone and must come back

from a backup. Prevention (the filter) and backups are your safety net; the

tripwire just tells you when to reach for them.





"Isn't VARCHAR(1024) dangerous?" — no, and the reason is reassuring



Widening these columns sounds risky — indexes, utf8mb4, InnoDB limits — and

it's the part people most often get scared off by. It's almost entirely a

non-issue, and one fact explains why:



The index on these columns is a fixed 191-character prefix, independent of

the column's declared length.
Core defines KEY post_name (post_name(191)) — it

indexes the first 191 characters whether the column is 200 wide or 1024. Widening

the column doesn't enlarge the index by a single byte.



That dissolves the usual worries:





  • Index / InnoDB prefix limits. 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 towards that limit. (A quiet irony: slugs are stored as percent-encoded
    ASCII, so they were never going to hit four bytes per character anyway.)


  • Row size. VARCHAR stores the actual length of each value, not its
    declared maximum — a 30-character slug takes ~30 bytes in a VARCHAR(1024)
    exactly as it would in a VARCHAR(200). Rows don't get bigger because the
    ceiling moved up.


  • Query plans. Lookups use the same 191-prefix index to narrow, then verify
    the full value. Identical behaviour at 200 or 1024.


  • Plugins. SEO plugins, caches and CDNs read post_name and emit longer URLs
    without complaint.



The genuine risks aren't in steady state — they're tools that recreate the

schema
from an old definition: a migration plugin that runs its own

CREATE TABLE, or, most commonly, restoring a SQL dump taken before you widened

the columns. A dump of the current database preserves VARCHAR(1024)

correctly; importing an old structure silently puts you back at 200. Write that

one down in your runbook.





Doing it safely on a site with millions of posts



There's one operational gotcha, and it's about the migration itself, not the

result. Going from VARCHAR(200) to VARCHAR(1024) cannot be an instant,

in-place change. InnoDB only keeps a VARCHAR length change "in place" while both

lengths stay in the same size class — values under 256 use one length byte,

values 256 and over use two. Crossing 255 forces ALGORITHM=COPY: a full table

rebuild. On a wp_posts with millions of rows, a raw ALTER means a long

operation and a problematic lock.



So on a large live site, do the initial widening with an online schema-change

tool, not a bare ALTER:




CODE
pt-online-schema-change \
--alter "MODIFY post_name VARCHAR(1024) NOT NULL DEFAULT ''" \
--chunk-time=0.5 --max-load Threads_running=50 \
--execute D=wordpress,t=wp_posts






(gh-ost is an equally good triggerless alternative.) wp_terms is tiny and

takes a plain ALTER without ceremony; wp_posts is the one that needs the

online tool. The rollout that's bitten no one:





  1. Back up, and confirm the dump shows VARCHAR(1024) afterwards.


  2. Clone to staging, deploy the MU-plugin, and run a real major core update
    there. Confirm no CHANGE COLUMN appears and the verify command reports OK.


  3. Deploy the MU-plugin to production before the next core update
    prevention has to be in place ahead of the event it prevents.


  4. Widen the production columns off-peak with pt-osc / gh-ost.


  5. Add a nightly verify cron and a 404 monitor, so anything unexpected
    surfaces in hours, not in a month of lost rankings.


  6. Keep the MU-plugin in version control, and never import a pre-widening
    structure dump.





The deeper fix — stop betting your URLs on a column width



Everything above makes Arabic slugs safe. But there's a more permanent way to

think about it, and for a large news archive it's worth the shift: don't let

the URL depend on the slug surviving at all.



Anchor your permalinks on the numeric post ID, and keep the Arabic slug as a

descriptive — but cosmetic — suffix:




CODE
https://example.com/123456/الذكاء-الاصطناعي






Resolve the post by its ID, and the slug becomes decoration. If it's ever

truncated, malformed, or edited, the URL still resolves to the right article

instead of 404-ing — and WordPress can 301 to the canonical form. You keep the

Arabic keywords in the URL for search relevance and you become structurally

immune to the entire class of problem. (If you want to stop touching core tables

altogether, you can go one step further and store the long slug in your own

indexed routing table, which dbDelta has no opinion about — but for most teams,

ID-anchored permalinks are the sweet spot.)



That's the difference between patching a bug and designing it out.





The complete plugin



Everything above, assembled into one must-use plugin. Drop it in

wp-content/mu-plugins/arabic-slug-schema-guard.php, widen the columns once with

an online schema-change tool, and all three layers — plus the tripwire — are

handled in a single place. The slug generator is a copy of core's

sanitize_title_with_dashes(); only the byte cap differs.




📦 Prefer it ready-made? The plugin is open-source on GitHub as .



If you run a WordPress publication in Arabic and want it to survive its own

updates, and the — the TEXT/BLOB downsize protection that VARCHAR never got




  • upgrader_process_complete hook

  • Vollständiger Original-Bericht
    Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
    ↗ Original-Artikel auf dev.to lesen
    Wie bewertest du diesen Beitrag?
    1 Klick Feedback
    Teilen mit Netzwerk & Team:

    Community-Analysen & Experten-Meinungen 0

    Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
    Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
    Community Pulse: Relevanz-Einschätzung
    1 Klick Experten-Votum
    🔴 Akute Relevanz 0%
    🟡 In Evaluierung 0%
    🟢 Keine Auswirkung 0%
    Spannende Innovation 0%
    Verwandte Story-Cluster & Quellen (Vektor-KI)
    Port 8095 Engine
    1 Quelle
    Bits und so #1021 (Passwort für Laufwerk)
    1 Quelle
    Bits und so #1022 (Wie Weißbier)
    1 Quelle
    KI-Agenten entdecken deutsches Wiki als Kommunikationskanal
    Ähnliche Beiträge
    🔍 Verwandte News

    Auch interessante Nachrichten The 200-byte trap: why WordPress core updates break Arabic URLs

    Thematisch verwandte Begriffe: 200byte, trap, WordPress, core · 6 Treffer

    Laden...

    Videos werden geladen ...

    Laden...

    Beiträge werden geladen ...

    Laden...

    Videos werden geladen ...

    Laden...

    Beiträge werden geladen ...

    Laden...

    Videos werden geladen ...

    Laden...

    Beiträge werden geladen ...

    Laden...

    Videos werden geladen ...

    Laden...

    Beiträge werden geladen ...

    Laden...

    Videos werden geladen ...