- كل المقالات
- معاينات الروابط بنص عربي سليم
معاينات الروابط بنص عربي سليم
توليد صور معاينة الروابط وقت البناء أو عند الطلب، ومعالجة ترتيب النص العربي ووصل حروفه وقياسه. أمثلة من هذا الموقع وUnblock Syria.
تعرض معاينة الرابط صورة يجلبها تطبيق المحادثة أو منصة التواصل من العنوان
المحدد في og:image. تحتفظ المنصة بنسخة منها لتعرضها مع الرابط لاحقًا. لذلك
ينبغي أن تكون الصورة جاهزة بما فيها النص والخطوط؛ عرض المعاينة لا يعتمد على
تنسيق الصفحة في متصفحك.
تستخدم مواقع كثيرة صورة واحدة لكل روابطها. تساعد هذه الصورة على التعرف إلى الموقع، لكنها لا تقول الكثير عن الصفحة المقصودة. عندما تولّد بطاقة لكل صفحة، تستطيع وضع عنوانها ووصفها في المعاينة، فيعرف القارئ ما الذي سيفتحه.
أستخدم طريقتين لتوليد البطاقات في موقعيّ. في omaralbeik.com، يولّدها سكربت Node عند بناء الموقع. وفي unblocksyria.com، تُولّد عند الطلب على Cloudflare Workers. الموقعان بالعربية والإنجليزية، ومعظم العمل الإضافي كان في عرض النص العربي بصورة صحيحة.
قبل أن تبدأ
- ولّد بطاقة لكل صفحة، وحدد في
og:imageعنوانًا مطلقًا للصورة، مع أبعادها ونصها البديل ووسمtwitter:card. - ولّد الصور وقت البناء إذا كان محتواها لا يتغير بين عمليتي نشر.
- ضمّن ملفات الخطوط، وتحقق من الصيغ التي يدعمها محرّك الرسم.
- حدد أولوية العنوان والوصف وحجم الخط عندما لا تتسع المساحة للجميع.
- جهّز صورة بديلة يعيدها المسار إذا فشل توليد البطاقة.
- افحص ترتيب النص العربي، واختيار الخط، ووصل الحروف وقياسها كلًا على حدة. استخدم عينة فيها أسماء لاتينية وأرقام، فقد تخفي العينة العربية وحدها بعض الأخطاء.
- قارن ترتيب النص بمرجع موثوق ضمن الاختبارات.
الوسوم، باختصار
<meta property="og:image" content="https://omaralbeik.com/og/ar/blog/some-post.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="عنوان المقالة — عمر البيك" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://omaralbeik.com/og/ar/blog/some-post.png" />
استخدم عنوانًا مطلقًا للصورة، يتضمن البروتوكول والنطاق، حتى لا تعتمد على طريقة
تعامل كل منصة مع العناوين النسبية. واضبط twitter:card على
summary_large_image لطلب معاينة كبيرة. تحتاج الصفحة أيضًا إلى og:title
وog:description وog:url وog:type.
أستخدم صورًا بقياس 1200×630. قد تقص المنصات الصورة بنسب مختلفة؛ قصّتها X إلى 16:9 في اختباراتي. لذلك أترك هامشًا حول النص، بلغ 96 بكسلًا بعد تجربة عدة معاينات. القياس وحده لا يخبرك بالمساحة التي ستظل ظاهرة بعد القص.
أين تُرسم البطاقة
يتوقف الاختيار على سؤال: هل يمكن أن يتغير محتوى البطاقة دون إعادة نشر الموقع؟
إذا كان المحتوى ثابتًا بين عمليتي نشر، فالتوليد وقت البناء مناسب. لا تحتاج إلى
توليد الصورة أثناء طلب المستخدم، ويمكنك استخدام أي برنامج يلائم المهمة. في
omaralbeik.com، يعمل سكربت Node بعد البناء، ويقرأ العنوان والوصف من <head>
في كل صفحة. بذلك تستخدم البطاقة البيانات نفسها التي تعلنها الصفحة.
"build": "astro build && node scripts/og-render.mjs"
وضعت السكربت خارج إطار العمل عن قصد. مع محوّل Cloudflare، يولّد Astro الصفحات مسبقًا داخل workerd. أما تحميل ملفات الخطوط وتشغيل مكتبة الرسم الأصلية في هذه الطريقة، فيحتاجان إلى بيئة Node التي يعمل فيها السكربت.
إذا كان المحتوى يتغير دون نشر جديد، فالتوليد عند الطلب أنسب. تعرض بطاقة الخدمة في unblocksyria.com حالة الحجب وآخر تقدم في المراسلات. قد تتغير المعلومتان في أي وقت، بينما تحتفظ الصورة المولّدة وقت البناء بحالتهما يوم النشر.
أستخدم هنا ImageResponse من next/og، حيث يتولى satori ترتيب العناصر قبل
تحويلها إلى صورة. أضمّن ملفات الخطوط في حزمة التطبيق بترميز base64: وزنين من
الخط اللاتيني ووزنين من الخط العربي. حجم الملفات 129 كيلوبايت، ويصبح 172
كيلوبايت بعد الترميز. أحتاج العائلتين في كل بطاقة، لأن البطاقة العربية قد
تتضمن اسمًا لاتينيًا، والعكس صحيح.
استخدم WOFF في هذا المسار. لا يقرأ satori ملفات WOFF2، ومحاولة تحميلها ترمي استثناءً بدل استخدام خط بديل.
كلّ مسارٍ ينتهي بصورة
إذا أعاد عنوان og:image خطأ 500، فقد تظهر المعاينة بلا صورة. لا تعتمد على أن
المنصة ستبحث بنفسها عن صورة افتراضية للموقع.
جهّز صورة بديلة داخل مسار توليد البطاقة، وانتبه إلى موضع الخطأ: يعود new ImageResponse(...) قبل اكتمال الرسم. يحدث الرسم الفعلي عند arrayBuffer()،
لذلك يجب أن يكون هذا الاستدعاء داخل try.
export function renderCard(element: ReactElement): Promise<ArrayBuffer> {
return new ImageResponse(element, { width: OG_WIDTH, height: OG_HEIGHT, fonts })
.arrayBuffer();
}
جرّب البطاقة كاملة أولًا. إذا فشلت، أعد المحاولة دون الشعار الذي يُجلب من مصدر خارجي. وإذا فشلت أيضًا، اعرض بطاقة عامة لا تعتمد على بيانات الصفحة.
يمكن تخزين استجابة المعرّف غير الموجود مؤقتًا، لكن تجنب تخزين أخطاء 5xx. وإلا
فقد تبقى البطاقة العامة مرتبطة بصفحة سليمة طوال مدة stale-while-revalidate،
حتى بعد زوال الخطأ.
ضبط النصّ في مساحته
بعد الحصول على قياس صحيح للنص، يمكن تقسيمه إلى أسطر، وتحديد عددها، وإضافة علامة الحذف إذا بقي نص لا يتسع. تحتاج الكلمات التي تتجاوز عرض العمود إلى تقسيم داخلي أيضًا. المشكلة الأصعب هي تقدير الحجم قبل القياس.
عندما ربطت حجم العنوان بعدد حروفه، اختلف التقدير كثيرًا بين العربية والإنجليزية. ظهر «المرصد الأورومتوسطي لرصد الزلازل» بحجم 28 بكسلًا، بينما ظهر «EMSC» بحجم 104، رغم أن كليهما اسم الجهة نفسها في بطاقات Unblock Syria.
وفي هذا التنفيذ، يحتاج satori إلى حساب صريح لعدد الأسطر وارتفاع كتلة النص، حتى لا يمتد العنوان إلى العناصر التي تحته.
قد لا تتسع البطاقة لعنوان من ثلاثة أسطر ووصف من سطرين بالحجم المطلوب. حدد ما تريد الحفاظ عليه أولًا. هنا أحاول إبقاء الوصف كاملًا، ثم الحفاظ على العنوان دون اختصار، وأصغّر الخط عند الحاجة:
for (const descLines of card.description ? [2, 1, 0] : [0]) {
for (const size of [64, 57, 50]) {
const block = layout(card, { size, descLines });
if (block.height <= BAND) return block; // يفوز أوّل ما يتّسع
}
}
ترتيب الحلقتين يحدد هذه الأولوية: نجرب أحجام الخط الثلاثة مع سطرين للوصف، ثم نجربها مع سطر واحد، وأخيرًا دون وصف. أي أننا نصغّر الخط قبل حذف سطر من الوصف. يحافظ ذلك على المعلومات التي يضيفها الوصف إلى العنوان.
من اليمين إلى اليسار
عرض العربية من اليمين إلى اليسار يتطلب معالجة ثلاث مسائل منفصلة: ترتيب المقاطع، واختيار الخط المناسب، ووصل الحروف وقياسها. نجاح واحدة لا يضمن نجاح البقية.
ترتيب الكلمات
في الاختبارات هنا، لم يؤثر تغيير direction وunicode-bidi في خرج resvg؛ بقي
مطابقًا بايتًا ببايت مع embed وisolate وplaintext. لم أستطع إذن إصلاح
ترتيب النص بهذه الخصائص. وفي مسار satori، ظهرت مشكلة ترتيب الكلمات من اليسار
إلى اليمين أيضًا.
يصعب ملاحظة المشكلة عند اختبار نص عربي فقط. يستنتج HarfBuzz الاتجاه من الحروف، وقد يبدو السطر صحيحًا. يظهر الخلل عندما تضيف اسم منتج لاتينيًا أو رقم إصدار، وهو أمر متكرر في موقع عن البرمجة.
في التوليد وقت البناء، استخدمت bidi-js لحساب مستويات الاتجاه. نقسّم النص إلى أسطر وهو في ترتيبه المنطقي، ثم نعيد ترتيب مقاطع كل سطر للعرض. تطبق القاعدة L2 ذلك بدءًا من أعلى مستوى تضمين وصولًا إلى أدنى مستوى فردي، وتعكس المقاطع المتجاورة التي تقع عند المستوى الحالي أو فوقه.
نبدأ بمقاطع يمكن إرسال كل منها إلى محرّك تشكيل الحروف (shaper) دفعة واحدة. يجمع المقطع حروفًا تشترك في مستوى الاتجاه والخط:
const bidi = bidiFactory();
function logicalRuns(text, stack, baseDir) {
const { levels } = bidi.getEmbeddingLevels(text, baseDir);
const runs = [];
for (let i = 0; i < text.length; ) {
const char = String.fromCodePoint(text.codePointAt(i));
const level = levels[i];
const face = faceFor(stack, text.codePointAt(i));
const last = runs[runs.length - 1];
if (last && last.level === level && last.face === face) last.text += char;
else runs.push({ level, face, text: char });
i += char.length;
}
return runs;
}
ثم نطبّق القاعدة L2 لإعادة ترتيب المقاطع:
function reorder(runs) {
const levels = runs.map((r) => r.level);
const highest = Math.max(...levels);
const odd = levels.filter((l) => l % 2 === 1);
const lowestOdd = odd.length ? Math.min(...odd) : highest + 1;
const out = [...runs];
for (let level = highest; level >= lowestOdd; level--) {
for (let start = 0; start < out.length; start++) {
if (out[start].level < level) continue;
let end = start;
while (end + 1 < out.length && out[end + 1].level >= level) end++;
out.splice(start, end - start + 1, ...out.slice(start, end + 1).reverse());
start = end;
}
}
return out;
}
بعد ذلك نحدد أشكال الحروف داخل كل مقطع:
function shapeRun(run) {
const buffer = new HbBuffer();
buffer.addText(run.text);
buffer.guessSegmentProperties();
buffer.setDirection(run.level % 2 === 1 ? Direction.RTL : Direction.LTR);
shape(run.face.font, buffer);
return buffer.getGlyphInfosAndPositions();
}
استدعِ guessSegmentProperties() أولًا لاختيار نظام الكتابة واللغة، ثم عيّن
الاتجاه صراحة. عكس هذا الترتيب قد يجعل المحرّك يستبدل اتجاهك باتجاه يستنتجه من
الحروف. وانتبه إلى نوع الوسيط: الاتجاه قيمة من تعداد رقمي. تمرير النص "rtl"
في هذا المسار يعطي INVALID دون تنبيه، وقد تصبح قياسات التقدم الأفقي صفرًا.
في مسار التوليد على Workers، أستخدم row-reverse لترتيب المقاطع الاتجاهية.
يجب أن يبقى ترتيب الكلمات داخل المقطع اللاتيني كما هو. عكس الكلمات المفصولة
بالمسافات واحدة واحدة قد يحوّل «Google Play» إلى «Play Google» داخل الجملة
العربية.
اختيار الخط لكل مقطع
تظهر المربعات الفارغة عندما لا يحتوي الخط المختار على الحروف المطلوبة، ولا ينتقل محرّك الرسم إلى خط آخر يوفرها.
يوفر Fontsource ملفات منفصلة بحسب مجموعة المحارف. تحميل ملف العربية من IBM Plex Sans Arabic لا يعني أنك حمّلت الحروف اللازمة لكتابة «libVLC»؛ تحتاج إلى الملف اللاتيني أيضًا. وفي مسار satori الذي أستخدمه، قد يؤدي غياب الحروف من الخط المختار إلى استثناء واستجابة 500.
احتفظ بقائمة مرتبة من الخطوط، واختر لكل مقطع خطًا يحتوي على حروفه. افحص المحارف التي يدعمها الملف بدل الاعتماد على اسم العائلة:
export function loadFace(sfnt) {
const face = new Face(new Blob(sfnt), 0);
const font = new Font(face);
font.setScale(face.upem, face.upem);
return { font, upem: face.upem, covers: new Set(face.collectUnicodes()) };
}
const faceFor = (stack, codePoint) =>
stack.find((face) => face.covers.has(codePoint)) ?? stack[0];
تقرأ collectUnicodes جدول cmap لمعرفة المحارف الموجودة في ملف الخط. عندما
يكون الملف العربي أولًا والملف اللاتيني من العائلة نفسها ثانيًا، يبقى شكل النص
متناسقًا عند الانتقال بين اللغتين.
في مسار satori، أختار الخط البديل للنص كاملًا عندما يلزم، حتى لا تظهر بضعة حروف بخط مختلف وسط الاسم. لهذا أستخدم الخط اللاتيني للبطاقات الكردية: حروف مثل Ç وç وê وî وû وş غير موجودة في الملف العربي المستخدم هنا.
الوصل والقياس
يتغير شكل الحرف العربي بحسب موضعه واتصاله بما حوله. وفي الخط المستخدم هنا، تكون كثير من الكلمات بعد وصل حروفها أضيق من مجموع عروض الحروف المنفصلة.
يرسم satori الحروف متصلة، لكنه يحسب صندوق الكلمة من عروضها المنفصلة. يبقى الفرق بين القياسين فراغًا زائدًا عند طرف الصندوق.
في أسماء الخدمات العربية في فهرس Unblock Syria، بلغ وسيط الفراغ الزائد 0.91em، ووصل أكبر فرق إلى 3.49em. عند حجم خط 52 بكسلًا، يعادل ذلك نحو 47 و181 بكسلًا؛ أي أن الخطأ قد يترك فجوة كبيرة داخل الاسم.
لكن سبع عشرة كلمة كانت أعرض من صناديقها، لأن الشكل النهائي لبعض الحروف أعرض من المنفصل. لذلك لا يصلح ضرب جميع القياسات في معامل تصحيح واحد. حسبت عرض كل كلمة من مقاييس الخط، وولّدت جدول القياسات من ملف الخط المضمّن. ويعيد اختبار اشتقاق الجدول من الملف نفسه، حتى لا نستبدل الخط ونترك قياسات النسخة القديمة.
جرّبت أيضًا التشكيل المسبق: استبدال الحروف بأشكالها المناسبة لمواضعها، ثم إرسال النص بالترتيب البصري. لكن المحرّك أعاد ترتيبه مرة أخرى، فلم تعد الأشكال المختارة تطابق المواضع الجديدة. هكذا ظهرت «التجاري» على هيئة «التجاير».
تبادل الحرفان الأخيران موضعيهما. الراء لا تتصل بما بعدها، ولذلك تتوزع «التجاري» على أربعة مقاطع: ا، لتجا، ر، ي. عند إعادة ترتيب المقطعين الأخيرين، سبقت الياء الراء.
المربك أن الياء تتصل بالراء بعدها، فتظهر «ير» متصلة. لا توجد حروف مفقودة أو استثناءات، وقد يبدو الرسم سليمًا لمن لا يقرأ العربية. لكنه يعرض الكلمة بترتيب خاطئ.
وتحتاج الأرقام وعلامات الترقيم إلى معالجة أيضًا. قد يضعها المحرّك في الجانب الخطأ من الكلمة إذا لم يطبق قواعد الاتجاهين. مثلًا، يجب أن تأتي النقطتان إلى يسار «ليجندز» في هذا السياق. والأقواس تتطلب عكس أشكالها عند العرض من اليمين إلى اليسار؛ بدون ذلك ظهرت أقواس «جو (جولانج)» متجهة إلى الخارج.
في مسار البناء، يتولى HarfBuzz تشكيل المقاطع، ثم أحوّل كل شكل إلى <path>.
يرسم resvg هذه المسارات دون استخدام محرّك النص الخاص به. وبذلك أتحكم في
الترتيب واختيار الخط قبل تحويل الرسم إلى صورة.
// السطر اليمينيّ يبدأ من الحافّة اليمنى، فيبدأ القلم على بُعد عرض السطر كلّه
// إلى يسارها. أخطئ هذا تحطّ عربيّةٌ صحيحة في النصف الخطأ من البطاقة.
let pen = dir === "rtl" ? x - measure(text, { stack, size }) : x;
for (const run of reorder(logicalRuns(text, stack, dir))) {
const scale = size / run.face.upem;
for (const glyph of shapeRun(run)) {
const d = run.face.font.glyphToPath(glyph.codepoint);
if (d) {
const gx = pen + glyph.xOffset * scale;
const gy = y - glyph.yOffset * scale;
// مقلوبٌ في المحور الرأسيّ: وحدات الخطّ تصعد، وSVG ينزل.
parts.push(
`<path d="${d}" fill="${fill}" ` +
`transform="translate(${gx} ${gy}) scale(${scale} ${-scale})"/>`,
);
}
pen += glyph.xAdvance * scale;
}
}
مراجعة التصميم بعد تصحيح النص
بعد تصحيح النص، راجع التصميم نفسه. العربية لا تحتوي على حروف كبيرة وصغيرة، وزيادة المسافات بين الحروف قد تفصل ما يجب أن يتصل. لذلك أستخدم تنسيقًا أبسط للعناوين الصغيرة في البطاقة العربية، بحجم يوازن النسخة الإنجليزية.
اخترت للعناوين العربية أحجامًا أصغر ومسافات أكبر بين الأسطر: 58 و52 و46 بكسلًا، مقابل 64 و57 و50 للإنجليزية، وتباعدًا قدره 1.52 مقابل 1.22. تحتاج صواعد الحروف ونوازلها والتشكيل في هذا الخط إلى مساحة رأسية أكبر.
أخطاء شائعة
تبدأ بعض المشكلات قبل أن يصل محرّك الرسم إلى النص:
- إضافة الوسوم في المتصفح فقط. لا تعتمد على أن برامج جلب المعاينات ستشغّل JavaScript. أرسل الوسوم ضمن HTML الصفحة من الخادم.
- عنوان صورة نسبي. استخدم عنوانًا كاملًا، حتى لا تختلف النتيجة بحسب طريقة معالجة المنصة للمسارات النسبية.
- صيغ الصور. اختر PNG أو JPEG لتوافق أوسع بين المنصات؛ دعم المتصفح لـ WebP أو AVIF لا يضمن دعمها في معاينة الرابط.
- تكرار
og:image. قد يضيف القالب ونظام المحتوى الوسم نفسه. افحص HTML الناتج وتأكد من أنه يحدد الصورة المطلوبة. - حجب برنامج جلب المعاينات. قد تمنعه قواعد مكافحة البوتات من الوصول إلى
الصورة. جرّب طلب عنوانها باستخدام ترويسة
User-Agentالخاصة بالمنصة. - بقاء نسخة قديمة في الذاكرة المؤقتة. اطلب تحديث المعاينة من أداة المنصة قبل إعادة نشر الموقع بحثًا عن خطأ لم يعد موجودًا.
- إلحاق مسار الصورة بعد معاملات الاستعلام. اشتق العنوان من مسار الصفحة
دون الاستعلام. إذا أضفت اللاحقة إلى
/articles?page=2، فستصبح جزءًا من قيمةpage، وقد يصل برنامج المعاينة إلى HTML بدل الصورة. - تعارض المعرّفات مع
opengraph-image. احجز هذا الاسم إذا كان إطار العمل يستخدمه لمسار الصورة. وإلا فقد يحجب المسار الثابت صفحة سجل تحمل المعرّف نفسه. - الإعلان عن صورة لم تُولّد. الصفحات التي تُعرض عند الطلب قد لا تترك ملفات HTML يقرأها سكربت البناء. تحقق من وجود الصور التي يفترض توليدها وقت البناء، واختبر مسارات الصور التي تُولّد عند الطلب منفصلة.
وفي العربية، قد يبدو السطر سليمًا مع أن ترتيب كلماته خاطئ. انتبه إلى الآتي:
- عكس النص المخزّن. احتفظ بالحروف في ترتيب القراءة المنطقي، ودع مرحلة العرض تحدد مواضعها. عكس السلسلة نفسها يربك البحث والنسخ وقارئات الشاشة.
- تخزين أشكال الحروف بدل الحروف الأصلية. خُصصت كتلة U+FE70–FEFF للتوافق مع الترميزات القديمة، كما يوضح جدول Unicode. استخدامها لتجاوز محرّك التشكيل قد يؤدي إلى إعادة ترتيب النص بعد اختيار أشكال حروفه.
- الاكتفاء بـ
text-align: right. تضبط هذه الخاصية المحاذاة، لكنها لا تعالج ترتيب المقاطع أو وصل الحروف. - نقل تنسيق العناوين اللاتينية كما هو. لا توجد حروف عربية كبيرة وصغيرة، وزيادة المسافات بين الحروف قد تفصل حروف الكلمة.
- اختبار نص عربي فقط. أضف اسم منتج لاتينيًا ورقم إصدار وعلامات ترقيم، لتختبر انتقال الاتجاه واختيار الخط أيضًا.
هذه أمثلة على الحلول التي يجب تجنبها، وعلى فصل ترتيب المقاطع عن البيانات الأصلية:
// لا تفعل: ليس شيءٌ من هذه دعمًا لليمين إلى اليسار.
const visual = [...name].reverse().join(""); // يكسر البحث والنسخ
const shaped = toPresentationForms(name); // كتلة توافقٍ قديمة
element.style.textAlign = "right"; // محاذاة، لا ترتيب
element.style.letterSpacing = "0.08em"; // يقطع الوصل
// افعل: أبقِ السلسلة بالترتيب المنطقيّ، ودع الطبقة التي ترسمها تحلّ
// الاتّجاه والتشكيل والوجه — ثمّ تحقّق من النتيجة.
const runs = reorder(logicalRuns(name, stack, "rtl"));
واختر عينة الاختبار بعناية:
// تخرج صحيحةً مهما يكن الخلل تحتها.
const bad = "ربط المكتبة مباشرة";
// تمتحن الترتيب واختيار الوجه والمحايدات معًا.
const good = "ربط libVLC مباشرةً من Swift 6 — إصدار 3.0.21";
كيف تتحقّق ممّا لا تراه
قد يبدو السطر العربي مقبولًا لمن لا يقرأه حتى عندما تكون كلماته معكوسة. لذلك يحتاج فحص الترتيب إلى مرجع موثوق.
يمكن استخراج بصمة بسيطة من الصورة: نفحص أعمدة البكسلات، ونحدد المناطق التي تحتوي على حروف، ونفصل بينها عند وجود فراغ كافٍ. نحصل بذلك على تسلسل من العروض يمكن مقارنته بالمرجع:
async function fingerprint(png, gap) {
const { data, info } = await sharp(png).greyscale().raw()
.toBuffer({ resolveWithObject: true });
const widths = [];
let start = -1, blank = 0;
for (let x = 0; x < info.width; x++) {
let inked = false;
for (let y = 0; y < info.height; y++) {
if (data[y * info.width + x] < 128) { inked = true; break; }
}
if (inked) { if (start === -1) start = x; blank = 0; }
else if (start !== -1 && ++blank >= gap) {
widths.push(x - blank + 1 - start);
start = -1; blank = 0;
}
}
if (start !== -1) widths.push(info.width - 1 - start);
return widths;
}
ولّد المرجع بمتصفح يعرض النص نفسه بالحجم والخط نفسيهما. تغيير ترتيب المقاطع يغيّر تسلسل العروض، وقد يغيّره أيضًا خطأ في تشكيل الحروف. احفظ القياسات المرجعية ضمن الاختبارات، واسمح بفارق بكسل أو اثنين. عند تجاوز الفارق، يفشل الاختبار دون أن يحتاج من يشغّله إلى قراءة النص.
لفحص الوسوم، استخدم أدوات التحقق لدى المنصات. اختبر كل تصميم للبطاقة، لا كل مقالة على حدة.
استغرق عرض العربية معظم العمل لأن مولّد البطاقات احتاج إلى تنفيذ أمور يتولاها المتصفح عادة: ترتيب المقاطع، واختيار الخط، ووصل الحروف، وقياسها. فصل هذه المهام جعل تشخيص الأخطاء أسهل. عندما تظهر بطاقة غير صحيحة الآن، أستطيع تحديد المرحلة التي أراجعها بدل تعديل المحاذاة وانتظار نتيجة مختلفة.
هل وجدت محرّك رسم أو مثالًا عربيًا يتصرف بصورة مختلفة؟ أرسل لي المثال، لنتبادل طرق المعالجة. وشارك الدليل مع من يتولى معاينات الروابط في موقعك، خصوصًا إذا كانت اختباراته تقتصر على الإنجليزية.