كم حرفاً من لغتك يتسع له رابط ووردبريس؟
جعفر أبازيد
عندما نشرت مقال فخّ الـ 200 بايت، طرح أحد القرّاء على CoderLegion السؤال الذي كان ينبغي أن أجيب عنه في المقال نفسه: هل هذه مشكلة تخصّ العربية وحدها؟
المشكلة لا تقتصر على العربية. لكن عبارة “لا، ليست كذلك” ليست إجابة كافية، والإجابة التي قدّمتها في التعليقات كانت حسابية لا تستند إلى قياسات فعلية. لذلك مرّرت عنواناً إخبارياً نموذجياً بخمس عشرة لغة في موقع ووردبريس 7.1.3 جديد، وراقبت ما نتج عن ذلك. ثم فتحت الروابط الناتجة بالطرق التي يستخدمها القرّاء فعلياً: سواءً كانت روابط كاملة، أو مقتطعة، أو بعد تغيير الـ slug.
الإجابة المختصرة: تحصل الإنجليزية على 200 حرف. أما العربية والعبرية واليونانية والروسية، فتحصل على نحو 33 حرفاً. وتحصل الهندية والتايلاندية والصينية واليابانية على 22 حرفاً. أما الإجابة المفصّلة، ففيها بعض المفاجآت، وإعداد واحد يجعل هذه المشكلة تتضاءل.
القاعدة: 200 حرف، تُحتسب بعد الترميز
يبني ووردبريس الـ slug من العنوان باستخدام الدالة sanitize_title_with_dashes(). ويُخزَّن كل ما يتجاوز نطاق ASCII العادي بالترميز المئوي Percent-Encoding، أي بصيغة %XX نفسها التي ينتقل بها عبر الشبكة، ويُطبَّق الاقتطاع على هذه الصيغة المرمّزة في سطر واحد من نواة ووردبريس الحالية:
$title = utf8_uri_encode( $title, 200 );
يتحول كل بايت من بايتات UTF-8 إلى رمز %XX مكوّن من ثلاثة أحرف، لذا تعتمد كلفة الحرف على عدد البايتات التي تحتاجها أبجديته:
- حرف لاتيني أو رقم أو شرطة: بايت واحد، وكلفته 1 حرف
- العربية والفارسية والأردية والعبرية واليونانية والسيريلية: بايتان، والكلفة 6 أحرف
- الهندية والتايلاندية والصينية واليابانية والكورية: ثلاثة بايتات، والكلفة 9 أحرف
- الرمز التعبيري Emoji: 4 بايتات في الغالب، أي 12 حرفاً، وبعض الرموز يكلّف أكثر بكثير
اقسم 200 على الكلفة لتحصل على الحدّ الأقصى. لا يحدث الاقتطاع في منتصف الحرف، لأن utf8_uri_encode() تتوقف قبل الوصول إلى حرف لن تتسع له المساحة المتبقية. لكنها قد تقتطع الكلمات، إذ لا تعرف أين تنتهي الكلمة. ويمرّ الـ slug الخاص بالتصنيفات والوسوم عبر الدالة نفسها، وبالحدّ نفسه البالغ 200 حرف.
يخفي سطر الرموز التعبيرية مفاجأتين. يتكوّن القلب ❤️ من نقطتي ترميز Code Points، تبلغ كلفة كل منهما 9 أحرف، والثانية غير مرئية لكنها تبقى في الرابط. أما الرمز التعبيري للعائلة، فيتكوّن من ثلاثة أشخاص تربط بينهم علامات وصل غير مرئية ZWJ. يحتسب ووردبريس هذه العلامات، ثم يحذفها، لينتهي الرابط بثلاثة أشخاص متفرقين.
الأرقام الفعلية من القياس
إليك ما احتفظ به ووردبريس 7.1.3 من العنوان الإخباري لكل لغة. تُحتسب “كلفة الحرف” بعد إزالة العلامات الصوتية التي تعلو الحروف اللاتينية Accents، لذا تكون كلفة é مماثلة لكلفة e. ويشمل عدد “الأحرف المتبقية” الشرطات التي حلّت محل المسافات، وعادةً ما تكون الكلمة الأخيرة المحسوبة جزءاً من كلمة وليس كلمة كاملة.
| اللغة | كلفة الحرف | الأحرف المتبقية | دون الشرطات | الكلمات |
|---|---|---|---|---|
| الإنجليزية | 1 | 199 | 170 | 30 |
| الفرنسية | 1 | 190 | 161 | 30 |
| الألمانية | 1 | 198 | 174 | 25 |
| الفيتنامية | 1 | 157، العنوان كاملاً | 123 | 35 |
| العربية | 6 | 38 | 32 | 7 |
| الفارسية | 6 | 36 | 30 | 7 |
| الأردية | 6 | 40 | 32 | 9 |
| العبرية | 6 | 38 | 32 | 7 |
| اليونانية | 6 | 37 | 32 | 6 |
| الروسية | 6 | 36 | 32 | 5 |
| الهندية | 9 | 26 | 21 | 6 |
| الكورية | 9 | 27 | 21 | 7 |
| التايلاندية | 9 | 22 | 22 | 1، بلا مسافات |
| الصينية | 9 | 22 | 22 | 1، بلا مسافات |
| اليابانية | 9 | 22 | 22 | 1، بلا مسافات |
ثلاثة أمور في هذا الجدول تستحق نظرة ثانية.
توقفت الأبجديات ذات البايتين عند 32 حرفاً، وليس 33، وعند 30 حرفاً للفارسية. فالشرطات تُحتسب من الحدّ نفسه البالغ 200 حرف، لذا يحتفظ عنوان يحتوي على ست مسافات بـ 32 حرفاً وست شرطات، أما الحرفان الناقصان في الفارسية فلهما تفسير سنأتي عليه لاحقاً. عملياً، يعني ذلك الاحتفاظ بخمس إلى تسع كلمات، بحسب طول الكلمات، وغالباً ما تكون الكلمة الأخيرة مقتطعة: ينتهي الـ slug العربي بـ “ال”، وهما حرفا أداة التعريف لكلمة لم تتسع لها المساحة المتبقية.
اثنان وعشرون حرفاً تعني عدداً أقل مما يبدو في الهندية والتايلاندية. فالعدد يشير إلى نقاط الترميز Code Points، وعلامات الحركات وحروف العلّة وعلامات النبر في هاتين اللغتين هي نقاط ترميز مستقلة. ينتهي الـ slug التايلاندي بـ”ใหม”، وهي “ใหม่” دون علامة النبر.
لا تحصل التايلاندية والصينية واليابانية على أي شرطات على الإطلاق. فهذه اللغات لا تضع مسافات بين الكلمات، لذا يكون الـ slug بأكمله سلسلة واحدة من 22 حرفاً.
هناك كلفة أخرى لا تظهر في الجدول. عندما يكون الـ slug مستخدماً بالفعل مع مقال سابق، يقتطعه ووردبريس مرة أخرى ليتّسع للّاحقة، لذا فقد المقالان الثاني والثالث اللذان يحملان العنوان الأردي نفسه حرفاً إضافياً لكل منهما لإفساح المجال لـ-2 و-3. ومع وجود خمس إلى تسع كلمات فقط للتمييز بين العناوين، يزداد احتمال تطابق الـ slug لعنوانين غير لاتينيين، للسبب نفسه الذي يجعل الروابط المقتطعة تؤدي إلى وجهات خاطئة، كما سنرى لاحقاً.
اللاتينية بعلاماتها تنال الـ 200 كاملة، ولغة الموقع تقرر كيف
احتفظت الفرنسية والألمانية والفيتنامية بالقدر نفسه الذي احتفظت به الإنجليزية، لأن علاماتها الصوتية لا تصل إلى دالة الترميز أصلاً. فعند حفظ الـ slug، تستدعي sanitize_title() الدالة remove_accents() أولاً، فتحوّل é إلى e، وجاء العنوان الفيتنامي بأكمله بصيغة ASCII عادية.
تعتمد طريقة تحويل الحروف على لغة الموقع. ففي موقع لغته الإنجليزية، تحولت “Maßnahmen” إلى masnahmen، و “Größe” إلى grose، وهي كلمة مختلفة. أما حين تكون لغة الموقع الألمانية، فتنتج الدالة نفسها massnahmen وgroesse. وتحظى الدنماركية (soester وpaa)، والكتالونية (collegi)، والصربية (djordje) بالعناية نفسها من خلال إعداداتها المحلية Locales. ولأن الـ slug يُولَّد عند حفظ المقال، اضبط لغة الموقع قبل أن يكبر الأرشيف، لا بعد ذلك.
ما يستهلك المساحة ولا يظهر في الرابط
يحدث الاقتطاع قبل أن ينظّف ووردبريس الـ slug، لذا تُحتسب بعض الأحرف أولاً، ثم تُحذف أو تُستبدل. والعناوين العربية والفارسية مليئة بها، أما العناوين الإنجليزية فليست خالية منها أيضاً.
- علامتا التنصيص الزاويتان. تكلّف كل واحدة من «» 6 أحرف، ثم تُحذفان بعد احتسابهما. فعنوانٌ افتُتح بكلمة “عاجل” ووردت فيه كلمة أخرى بين علامتي تنصيص، احتفظ بحرف واحد من آخر كلمة فيه، كلمة “الشركات”، في حين احتفظ العنوان نفسه دون علامات التنصيص بخمسة أحرف: “الشرك”.
- العلامات غير المرئية. تكلّف علامة اتجاه الكتابة من اليمين إلى اليسار RLM 9 أحرف، وكذلك علامة عدم الوصل ZWNJ المستخدمة في الفارسية. وكلاهما يُحذف. ويكشف العنوان الفارسي عن كلفة أخرى أيضاً: تحولت “کسبوکار” إلى “کسبوکا”، إذ التصقت أجزاؤها ببعضها ثم اقتُطعت.
- علامات الترقيم المنحنية، في أي لغة. تكلّف الفاصلة العليا المنحنية Apostrophe، كما في what’s، 9 أحرف ثم تُحذف، وكذلك علامتا التنصيص المنحنيتان وعلامة الحذف Ellipsis. أما الشرطة المتوسطة En Dash والشرطة الطويلة Em Dash فتكلّف كل منهما 9 أحرف وتتحولان إلى شرطتين عاديتين. وغالباً ما تكون العناوين المنسوخة من برامج معالجة النصوص مليئة بهذه العلامات: حيث فقد عنوان إنجليزي طويل يحتوي على أربع فواصل عليا منحنية وزوج من علامتي التنصيص المنحنيتين وشرطة 62 حرفاً من الـ slug بسببها، فتوقف عند 133 حرفاً.
ما يستهلك المساحة ويبقى في الرابط
والأسوأ هو الأحرف التي تُحتسب ثم تبقى في الرابط. فقائمة الأحرف التي يحذفها ووردبريس صُممت للنصوص اللاتينية، لذا تصل علامات الأبجديات الأخرى إلى الرابط بكامل كلفتها.
- الأرقام العربية الهندية. تكلّف ٥٠٠٠ أربعة وعشرين حرفاً، بينما تكلّف 5000 أربعة أحرف فقط، وقد يقع الاقتطاع داخل الرقم نفسه. انتهى عنوان يتناول منحة بقيمة 5,000 دينار بعبارة “بقيمة-٥٠٠”، أي 500 فقط: رقم يبدو صحيحاً لكنه ليس كذلك. وعند كتابة العنوان نفسه بالأرقام الغربية، احتفظ بالرقم كاملاً كما هو.
- علامات التشكيل. تكلّف كل علامة تشكيل 6 أحرف. وقد اتسع عنوان مشكول بالكامل لثلاث كلمات ونصف فقط: “الحُكُومَةُ-تُعْلِنُ-خُطَّةً-جَدِيد”.
كما تبقى علامات الترقيم الخاصة بالأبجديات الأخرى أيضاً، بكلفة تتراوح بين 6 و9 أحرف للعلامة الواحدة. وهذا هو الناتج الفعلي لنواة ووردبريس نفسها:
- الفاصلة وعلامة الاستفهام العربيتان:
عاجل،-الحكومة-تعلن-خطة؟-جديدة - الفاصلة المنقوطة العربية:
أولاً؛-ثانياً - النقطة الأردية:
حکومت-کا-اعلان۔-نیا-منصوبہ - علامات الترقيم اليابانية:
「速報」政府が新計画を発表! - علامات الترقيم الصينية:
政府宣布,新计划。
والأسوأ هي العلامات غير المرئية. فووردبريس يحذف علامات التضمين الاتجاهية القديمة Embeddings بعد احتسابها، لكنه يحتفظ بعلامات العزل الأحدث Isolates، من U+2066 إلى U+2069، وبعلامة الحرف العربي Arabic Letter Mark. تكلّف علامات العزل 9 أحرف لكل منها، وتكلّف علامة الحرف العربي 6 أحرف، وتبقى جميعها في الرابط دون أن تظهر مطلقاً في العنوان.
ماذا يحدث عندما يفتح أحدهم رابطاً مقتطعاً
تستخدم معظم المواقع روابط دائمة Permalinks تعتمد على الـ slug وحده، /%postname%/. فتحت المقال العربي التجريبي بأربع طرق:
| الطلب | النتيجة |
|---|---|
| الـ slug المخزَّن كما هو | 200، المقال نفسه |
| الاقتطاع بعد أربع كلمات، وهي بداية مشتركة بين عنوانين | 301 إلى أحد المقالين، تختاره قاعدة البيانات |
| الاقتطاع عند نقطة لا يطابق فيها الرابط سوى عنوان واحد | 301 إلى المقال الصحيح |
| رابط أطول من الـ slug المخزّن | 404 |
تنتج الحالتان الثانية والثالثة بسبب الدالة redirect_guess_404_permalink(). فعندما لا يعثر ووردبريس على الـ slug المطلوب، يبحث عن slug يبدأ بالنص المطلوب، باستخدام post_name LIKE 'cut%'، ثم يعيد التوجيه Redirect إلى أول تطابق. ولا يتضمن الاستعلام عبارة ORDER BY، لذا عندما تتشارك عناوين مقالين الكلمات الأولى، تقرر قاعدة البيانات أيّهما يصل إلى القارئ. في موقعي التجريبي، فاز المقال الأقدم، لكن لا تعوّل على ذلك؛ فقد تُرجع قاعدة بيانات أخرى، أو قاعدة البيانات نفسها عند القراءة عبر فهرس مختلف، المقال الآخر. تتشارك العناوين الإخبارية نفس البدايات بشكل متكرر، مثل “القبض على المتهمين في” …، وإعادة التوجيه هنا من النوع 301، أي أنها تُبلغ المتصفحات ومحركات البحث أن الانتقال دائم.
إذا كنت تفضّل أن يقوم ووردبريس بمطابقة الرابط تطابقاً تاماً بدلاً من التخمين، فهناك مرشّح Filter يتيح ذلك:
add_filter( 'strict_redirect_guess_404_permalink', '__return_true' );
باستخدام هذا المرشّح، يؤدي الرابط المقتطع إلى خطأ 404 حتى إن كان التخمين سيقود إلى المقال الصحيح. أما المطابقات التامة فيُعاد توجيهها كما كانت، وهذا مهم لاحقاً: فبعد التبديل إلى روابط دائمة Permalinks تعتمد على المعرّف ID، يعثر ووردبريس على الروابط القديمة التي تعتمد على الـ slug وحده باستخدام آلية التخمين نفسها، وتساعد المطابقة الصارمة على إبقائها تعمل. أما المرشّح الذي يعطّل التخمين بشكل كامل، do_redirect_guess_404_permalink، فسيودي بهذه الروابط إلى خطأ 404.
الحالة الأخيرة هي تجسيد مصغّر لفخّ الـ 200 بايت. فالرابط الأطول من الـ slug المخزّن لا يطابقه أبداً، لأن الـ slug المخزّن لا يبدأ به. وهذا ما يواجهه القرّاء عندما يُقلّص تحديثٌ لنواة ووردبريس عموداً في قاعدة البيانات سبق توسيعه، بينما تكون الروابط التي تعتمد عليه قد انتشرت بالفعل بين الناس.
ضع المعرّف في الرابط
الحل الذي قد يجعل هذه المشكلة غير ذات أهمية يوجد ضمن الإعدادات ← الروابط الدائمة: بنية مخصّصة هي /%post_id%/%postname%/. تبقى الكلمات في الرابط ليقرأها الزائر، بينما يحدّد المعرّف المقال المقصود. جرّبت نفس الطلبات السابقة، إلى جانب بعض الحالات الأكثر صرامة:
| الطلب | النتيجة |
|---|---|
| المعرّف والـ slug المخزّن | 200 |
| المعرّف و slug أطول | 301 إلى المقال |
| المعرّف والرابط المقتطع المشترك الذي يضم أربع كلمات | 301 إلى المقال الصحيح |
| المعرّف وكلمات لا علاقة لها بالعنوان | 301 إلى المقال |
| المعرّف وحده | 200، مع وسم Canonical يحدّد الرابط الكامل |
| رابط قديم يعتمد على الـ slug وحده، بعد التبديل | 301 إلى المقال |
| الـ slug الصحيح مع معرّف مقال آخر | 301 إلى ذلك المقال الآخر |
والسبب هو بضعة أسطر في redirect_canonical(). فحين لا يتطابق الـ slug، يُعامل الطلب مؤقتاً على أنه خطأ 404، لكن معرّف المقال ID يظل موجوداً فيه، فيوجّه ووردبريس القارئ إلى الرابط الدائم الحقيقي لذلك المقال:
if ( is_404() ) {
// Redirect ?page_id, ?p=, ?attachment_id= to their respective URLs.
$post_id = max( get_query_var( 'p' ), get_query_var( 'page_id' ), get_query_var( 'attachment_id' ) );
$redirect_post = $post_id ? get_post( $post_id ) : false;
// … and for a public post: $redirect_url = get_permalink( $redirect_post );
والسطر الأخير في الجدول هو الثمن: فالمعرّف يتقدّم على الكلمات، لذا فإن كتابة معرّف خاطئ ستقود إلى مقال آخر. تُنسخ الروابط أكثر بكثير مما تُكتب يدوياً، وبالنسبة إلى أرشيف كبير، فإن هذا ثمن أقبل بدفعه.
كان التبديل سلساً عند الاختبار على موقع قائم: أُعيد توجيه الروابط القديمة التي تعتمد على الـ slug وحده إلى مقالاتها باستخدام آلية التخمين نفسها المذكورة أعلاه، سواء كانت المطابقة الصارمة مفعّلة أم لا. لذا، بعد التبديل، اترك التخمين مفعّلاً، وراقب سجل أخطاء 404 لبعض الوقت.
أعِد المساحة الضائعة للـ slug
تستعيد إضافة صغيرة من نوع must-use المساحة التي تهدرها تلك الأحرف، وتمنع علامات الترقيم الخاصة في الأبجدية العربية من الظهور في الرابط. وتعمل الإضافة قبل أن يبدأ ووردبريس بالاحتساب، وعند حفظ الـ slug فقط، لذا تظل الروابط الحالية مطابقة تماماً لما كانت عليه من قبل:
<?php
/**
* Give slugs their budget back: drop or replace characters that cost slug
* space, before WordPress counts them. Mostly Arabic, Persian and Urdu ones,
* plus the curly quotes, dashes and ellipses that English headlines pick up.
*/
add_filter( 'sanitize_title', function ( $title, $raw_title = '', $context = 'display' ) {
if ( 'save' !== $context ) {
return $title; // Leave lookups alone, so existing URLs keep matching.
}
// Arabic diacritics, superscript alef and tatweel: 6 each, and they clutter the URL.
$title = preg_replace( '/[\x{064B}-\x{065F}\x{0670}\x{0640}]/u', '', $title );
// Quotes, the ellipsis and invisible marks, which WordPress strips anyway,
// but only after counting them.
$title = preg_replace( '/[\x{00AB}\x{00BB}\x{2039}\x{203A}\x{2018}-\x{201F}\x{2026}\x{200B}-\x{200F}\x{FEFF}]/u', '', $title );
// Direction controls: WordPress strips the embeddings only after counting
// them, and keeps the isolates and the Arabic letter mark in the URL.
$title = preg_replace( '/[\x{061C}\x{202A}-\x{202E}\x{2066}-\x{2069}]/u', '', $title );
// Punctuation that WordPress keeps in the URL (Arabic comma, semicolon and
// question mark, Urdu full stop) or turns into a hyphen only after counting
// it (no-break space, en and em dash): make it a space, which costs 1.
$title = preg_replace( '/[\x{060C}\x{061B}\x{061F}\x{06D4}\x{00A0}\x{2013}\x{2014}]/u', ' ', $title );
// Arabic-Indic and Persian digits cost 6 each; 0 to 9 cost 1.
$title = strtr( $title, array_combine(
preg_split( '//u', '٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹', -1, PREG_SPLIT_NO_EMPTY ),
str_split( '01234567890123456789' )
) );
return $title;
}, 9, 3 );
في الموقع التجريبي، جاء الـ slug للعنوان الذي يحتوي على علامات التنصيص مطابقاً تماماً لنظيره دونها. أما عنوان طويل يحتوي على فاصلة عربية وعلامة استفهام، فاستُبدلت العلامتان بشرطات عادية، واحتفظ العنوان بحرفين إضافيين. وفقد عنوان يحتوي على علامات عزل غير مرئية هذه العلامات، واحتفظ بثلاثة أحرف إضافية، كما حُذفت علامات الحرف العربي أيضاً. واحتفظ عنوان المنحة برقمه كاملاً، 5000، واتسع العنوان المشكول لست كلمات بدل ثلاث ونصف، دون أي علامات تشكيل في الرابط. وتترك الإضافة علامات الترقيم الصينية واليابانية كما هي، لذا تظل كلفة كل منها 9 أحرف وتبقى في الرابط. وتظهر العناوين الإنجليزية كما ينشئها ووردبريس في النواة، لكن دون أن تُقتطع مبكراً: فقد ارتفع طول العنوان الإنجليزي الطويل من 133 حرفاً إلى 195 حرفاً.
إذن، هل هي مشكلة تخصّ العربية؟
لا. تدفع كل لغة لا تستخدم الأبجدية اللاتينية ثمن الأحرف الـ 200 نفسها بكلفة أعلى: 6 لكل حرف في الأبجديات ذات البايتين، و9 لكل حرف في الأبجديات ذات الثلاثة بايتات. وحتى الإنجليزية تدفع الثمن كلما نُسخت إليها فاصلة عليا منحنية أو شرطة. وقد واجهت المشكلة أول مرة في غرف الأخبار العربية، لأن معظم عملي معها، وليس لأن القاعدة تعامل العربية معاملة مختلفة.
إليك ما يمكنك فعله لمعالجة المشكلة، مرتّباً تقريباً حسب الجهد الذي تتطلبه:
- إذا كانت لغتك تستخدم الحروف اللاتينية ذات العلامات الصوتية، فاضبط لغة الموقع قبل أن تبدأ النشر.
- عدّل الـ slug الطويل يدوياً. فخمس إلى سبع كلمات في الرابط الواحد كافية.
- في المواقع الروسية أو غيرها من المواقع التي تستخدم الأبجدية السيريلية، فكّر في استخدام إضافة للنقحرة Transliteration لتحويل الحروف إلى اللاتينية، مثل Cyr-To-Lat، التي تنشئ الـ slug بحروف لاتينية، فتكون كلفتها مماثلة لكلفة الحروف الإنجليزية. ونادراً ما تُستخدم هذه الطريقة مع العربية، لأن العربية حين تُكتب بحروف لاتينية يصعب على القارئ أن يستعيدها.
- في المواقع العربية أو الفارسية أو الأردية، أو أي موقع تصل إليه العناوين بعلامات تنصيص منحنية وشرطات، ثبّت الإضافة السابقة.
- في الأرشيفات الكبيرة، اجعل معرّف المقال ID أساس الروابط الدائمة Permalinks.
- إذا كنت تحتاج إلى slug أطول مما تسمح به كل هذه الحلول، فوسّع العمود، وارفع الحدّ المسموح به، وامنع تحديثات النواة من تقليصه مجدداً. وهذا ما تفعله إضافة wp-arabic-slug-schema-guard.
قِيس كل رقم هنا على موقع ووردبريس 7.1.3 بإعداداته الافتراضية، باستخدام بيئة WordPress Playground التي تعمل على SQLite. حُسبت أطوال الـ slug باستخدام PHP، ولذلك ستبقى النتائج كما هي على MySQL. أما المقال الذي يقود إليه رابط ذو بداية مشتركة، فقد يختلف، وهذا تحديداً هو جوهر المشكلة.