تحويل Markdown والشيفرة على الخادم
كيف يحوّل هذا الموقع نصوص Markdown القادمة من نظام المحتوى إلى HTML آمن ومُلوّن قبل أن يصل إلى المتصفح، باستخدام unified وrehype-sanitize وShiki.
دحمان عبد الملك
27 أكتوبر 2026قراءة دقيقتينالجزء 3 من 3
دراسات الحالة ووصف الوظائف والمقالات مثل هذا المقال تُكتب بصيغة Markdown في نظام المحتوى. تحويل Markdown إلى HTML أمر سهل. أما فعل ذلك بأمان، مع تلوين الشيفرة، ودون أن يُضطر كل زائر إلى تنزيل محلّل ومُلوّن، فيتطلب تفكيرًا أكثر قليلًا.
أنجز العمل مرة واحدة، على الخادم
لا يرى المتصفح Markdown أبدًا. تجلب دوال الخادم المحتوى من Strapi، وتحوّله إلى HTML، وترسل النتيجة. الحقول التي تحمل HTML جاهزًا تنتهي بـ Html (achievementsHtml وdescriptionHtml)، فيعرف المكوّن من النظرة الأولى أن النص جاهز للعرض.
تتراكم الفوائد: لا محلّل Markdown ولا مُلوّن في حزمة JavaScript، ولا حسابات على هاتف بطيء، ومكان واحد فقط يتحوّل فيه المحتوى إلى HTML.
أنجز العمل المكلف مرة واحدة، حيث يكون أرخص، وأرسل النتيجة.
سلسلة المعالجة
المعالجة سلسلة unified: تحليل Markdown، وإضافة امتدادات GitHub مثل الجداول، والتحويل إلى HTML، والتنقية، وتلوين الشيفرة، ثم إعادتها نصًا.
function createMarkdownProcessor(highlighter: Highlighter) {
return (
unified()
.use(remarkParse)
.use(remarkGfm)
// Raw HTML in the Markdown is dropped, not passed through.
.use(remarkRehype)
// Defence in depth: strips unsafe URLs (javascript:…) and attributes.
.use(rehypeSanitize)
// After sanitizing, which would otherwise strip Shiki's colours.
.use(rehypeShikiFromHighlighter, highlighter, {
...SHIKI_OPTIONS,
defaultLanguage: 'text',
fallbackLanguage: 'text',
})
.use(rehypeStringify)
)
}آمن افتراضيًا
المحتوى يأتي من نظام أتحكّم فيه، فلماذا التنقية أصلًا؟ لأن «أنا أتحكّم فيه» يصحّ إلى أن لا يصحّ: مقطع منسوخ، أو حساب مخترق، أو كاتب ضيف ذات يوم. طبقتان تحميان كل صفحة:
- يُحذف HTML الخام داخل Markdown أثناء التحويل، ولا يُمرَّر أبدًا.
- ثم يزيل
rehype-sanitizeكل ما تبقّى من عناصر غير آمنة، مثل روابطjavascript:.
شيفرة فاتحة وداكنة دون إعادة عرض
يستطيع Shiki أن يكتب اللونين الفاتح والداكن لكل رمز في HTML معًا، على شكل متغيّرات CSS. ويختار ملف التنسيق أحدهما بحسب السمة الحالية، فيتغيّر لون الشيفرة لحظة تغيّر السمة: لا طلب ثانٍ، ولا إعادة عرض، ولا وميض.
// Light and dark colours both go into the HTML as CSS variables; app.css picks
// one from the site theme, so highlighted code switches theme with no re-render.
const SHIKI_OPTIONS = {
themes: { light: 'github-light', dark: 'github-dark' },
defaultColor: false,
} as const.shiki,
.shiki span {
color: var(--shiki-light);
}
.dark .shiki,
.dark .shiki span {
color: var(--shiki-dark);
}أنشئ الأشياء المكلفة مرة واحدة
تحميل قواعد Shiki اللغوية يستغرق وقتًا، لذا يُنشأ المُلوّن والمعالج مرة واحدة لكل خادم، وتتشاركهما كل الطلبات بعد ذلك.
// Loading grammars is slow, so the highlighter and processor are created once per server.
let highlighter: Promise<Highlighter> | undefined
function getHighlighter() {
highlighter ??= createHighlighter({
themes: Object.values(SHIKI_OPTIONS.themes),
langs: LANGUAGES,
})
return highlighter
}هذا كل ما في السلسلة حاليًا. وتفصيل أخير: مرّ هذا المقال بالسلسلة نفسها، فالشيفرة أعلاه لوّنتها الشيفرة أعلاه.
النشرة البريدية
تصلك المقالات الجديدة على بريدك
رسالة واحدة لكل مقال جديد. يمكنك إلغاء الاشتراك في أي وقت.
تفضّل قارئ الخلاصات؟ خلاصة RSS
الردود
تواصل
لديك مشروع في ذهنك؟
لنبنِه معًا.
متاح لوظائف عن بُعد ولمشاريع العمل الحر.