كنت أبني مدونتي الشخصية منذ فترة، وأردت إضافة الوضع الداكن. وكحال معظم المطورين، بدأت بالطريقة الكلاسيكية:
أحمّل الصفحة، أشغّل بعض JavaScript، أتحقق من الثيم المخزن في localStorage
، وأقوم بتبديل السمة. بسيط، أليس كذلك؟
في الواقع، هذه الطريقة ليست عملية أبدًا، وتتسبب في واحدة من أشهر المشاكل البصرية في المتصفحات: ظاهرة الـ FOUC أو Flash Of Unstyled Content.
والسبب؟ JavaScript تستغرق بعض الوقت للتحميل، وبالتالي يقوم المتصفح بعرض الصفحة أولًا بالوضع الافتراضي (غالبًا الفاتح)، ثم بعد تحميل JavaScript يتم تطبيق الوضع الداكن. النتيجة؟ وميض أبيض مزعج يظهر للحظة، يفسد التجربة البصرية ويشتّت الانتباه.
كيف تعمل معظم تطبيقات الوضع الداكن (وما الخطأ فيها؟)
- يبدأ المتصفح بتحميل الصفحة وعرض المحتوى الأولي.
- يتم تحميل وتشغيل ملفات JavaScript بعد مرحلة التحميل الأساسي.
- تقوم الشيفرة البرمجية بالتحقق من تفضيل المستخدم لمظهر الواجهة (الفاتح أو الداكن) من خلال
localStorage
. - يتم تطبيق السمة الداكنة على الصفحة بعد التحقق.
- لكن قبل اكتمال هذه العملية، يظهر المحتوى أولًا بالوضع الفاتح، مما يؤدي إلى ومضة بيضاء مفاجئة تُربك المستخدم وتؤثر على التجربة البصرية.
أكيد رأيت هذا مئات المرات. ذلك الانتقال المزعج من الأبيض إلى الداكن، الذي يحدث حتى في بعض المواقع الشهيرة، وقد قبلنا بها جميعًا على أنها "أمر طبيعي".

الحل – الكشف عن السمة على مستوى الخادم (Server-Side)
بدلاً من التعامل مع السمة في المتصفح، نقلت عملية اكتشاف السمة إلى الخادم. الفكرة بسيطة:
اكتشف ما يريده المستخدم قبل إرسال أي HTML.
- يستقبل الخادم الطلب من المستخدم.
- يتم قراءة الكوكيز لتحديد تفضيل المستخدم من حيث المظهر (فاتح أو داكن).
- يقوم الخادم بإنشاء صفحة HTML مهيأة مسبقًا بالسمة المناسبة.
- يُرسل الرد إلى المتصفح مع السمة الصحيحة مطبّقة منذ البداية.
- النتيجة: تجربة سلسة بدون أي ومضات أو انتقالات مفاجئة.
وبهذا، يصبح الخادم هو المصدر الوحيد للحقيقة في ما يخص سمة الموقع.
المستخدم لا يرى السمة الخطأ أبدًا، ولا يحتاج إلى انتظار JavaScript لتعديل شيء بعد فوات الأوان.
كيف بنيت هذا النظام
أنا أستخدم Hono 🔥 في مدونتي (وأعشقه)، لكن المفهوم هذا يمكن تطبيقه مع أي إطار عمل يعتمد على الخادم.
الخطوة 1 – الوسيط (Middleware)
export async function middleware(c, next) {
const theme = getCookie(c, "theme")
if (!theme) {
// الزائر لأول مرة يحصل على الوضع الفاتح افتراضيًا
setCookie(c, "theme", "light", {
maxAge: 60 * 60 * 24 * 365, // سنة كاملة
httpOnly: false, // للسماح بالوصول إليه من JavaScript
secure: c.env.NODE_ENV === "production",
sameSite: "Lax",
})
}
await next()
}
لا شيء معقد هنا؛ فقط نتحقق من وجود تفضيل للسمة في الكوكيز 🍪
الخطوة 2 – قراءة الكوكيز داخل الـ Layout
const Layout: FC = async (props) => {
const theme = getCookie(props.c, "theme")
return (
<html class={theme}>
<body>
{/* ... بقية التصميم ... */}
</body>
</html>
)
}
لاحظ السطر class={theme}
في عنصر <html>
؟ هذه هي جوهر الفكرة 💡
فبمجرد أن يصل الـ HTML إلى المتصفح، تكون السمة الصحيحة مطبقة مسبقًا.
لا حاجة لانتظار JavaScript، ولا وجود لأي ومضات مزعجة… كل شيء يعمل بسلاسة من اللحظة الأولى.
الخطوة 3 – تبديل السمة من المتصفح
export async function toggleTheme() {
const html = document.documentElement;
const isDarkMode = html.classList.toggle("dark");
const newTheme = isDarkMode ? "dark" : "light";
const expires = new Date();
expires.setTime(expires.getTime() + 365 * 24 * 60 * 60 * 1000);
document.cookie = `theme=${newTheme};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
};
الفكرة هنا أن الكود في المتصفح لا يحتاج إلى معرفة السمة الابتدائية — الخادم قد قام بالمهمة مسبقًا. كل ما يفعله هذا الكود هو تحديث الكوكيز عندما يُبدّل المستخدم السمة.
لماذا هذه الطريقة أفضل بكثير؟
- تجربة وصول أفضل (Accessibility) – المستخدم يرى السمة الصحيحة فورًا. لا وميض أبيض، لا قفز في المحتوى، لا انتقالات مزعجة.
- أداء محسّن – لا حاجة لتشغيل JavaScript في كل مرة لتحديد السمة. بما أن الخادم يعرف مسبقًا تفضيل المستخدم، نتجاوز هذه الخطوة كليًا. أقل JavaScript = أداء أفضل.
- محركات البحث ترى النسخة الحقيقية – تظهر مدونتي لمحركات البحث بالسمة الصحيحة تمامًا. وهذا يُحدث فرقًا في لقطات المعاينة (previews) في وسائل التواصل، وسكرينشوتات جوجل.