كلاود
development

استضافة تطبيق Inertia مع SSR: الإعداد الذي يرفع ظهورك في البحث

كلاود|١٧ أكتوبر ٢٠٢٦|6 دقائق قراءة|٩٤ مشاهدة
استضافة تطبيق Inertia مع SSR: الإعداد الذي يرفع ظهورك في البحث

تطبيق Inertia بلا SSR يرسل لمحركات البحث صفحة شبه فارغة تنتظر JavaScript، ومع SSR يصل المحتوى كاملاً في أول استجابة. تشغّل خادم SSR كخدمة systemd بجانب Laravel بذاكرة من 100 إلى 200MB، وتتحقق بأمر curl بسيط أن الصفحة مرسومة فعلاً.

تطبيق Inertia العادي يرسل للمتصفح صفحة HTML فيها عنصر واحد تقريباً: <div id="app" data-page="..."> مع بيانات الصفحة بصيغة JSON، ثم يأتي React ويرسم المحتوى في المتصفح. المستخدم لا يلاحظ الفرق، لكن محرك البحث يلاحظه، لأن أول ما يصله صفحة بلا عناوين ولا نصوص ولا روابط. تفعيل SSR (العرض على الخادم) يحل هذا: عملية Node تعمل بجانب Laravel وتستقبل بيانات الصفحة وترجع HTML مرسوماً كاملاً، فيصل المحتوى في أول استجابة قبل تنفيذ أي JavaScript. الإعداد المختصر: تبني نسخة SSR بأمر vite build --ssr، تفعّل ssr.enabled في config/inertia.php، تشغّل الخادم بأمر php artisan inertia:start-ssr كخدمة systemd على المنفذ 13714، ثم تتحقق بـcurl أن الصفحة تحوي نصوصها فعلاً. استهلاك عملية Node الفعلي في تطبيق متوسط بين 100 و200MB، وإذا سقطت الخدمة لا يتعطل الموقع بل يرجع Inertia تلقائياً للعرض في المتصفح. هذا الإعداد بالذات هو ما نشغّله على موقع كلاود نفسه، وكان أكبر رافعة لظهور المقالات في نتائج البحث.

ماذا يرى Googlebot فعلاً

صحيح أن Googlebot ينفذ JavaScript، لكنه يفعل ذلك على مرحلتين: يزحف للصفحة ويقرأ HTML الخام أولاً، ثم يضعها في طابور العرض، وقد يتأخر العرض من ساعات إلى أيام حسب ميزانية الزحف لموقعك. خلال هذه الفترة صفحتك في نظره شبه فارغة. المشكلة أكبر مع بقية الزواحف: محركات بحث أخرى، ومعاينات الروابط في واتساب وX ولينكدإن، وزواحف أنظمة الذكاء الاصطناعي التي تجمع المحتوى للإجابات، وأغلبها لا ينفذ JavaScript أصلاً.

العنصرSPA بلا SSRInertia مع SSR
محتوى أول استجابةعنصر div فارغ وJSONHTML كامل بالعناوين والنصوص
وسوم title وmetaتُضاف بعد التنفيذموجودة في الاستجابة الأولى
معاينة الرابط في واتسابغالباً فارغة أو عامةالعنوان والوصف الصحيحان
زواحف الذكاء الاصطناعيلا ترى المحتوى غالباًتقرأ النص مباشرة
زمن ظهور المحتوى للمستخدمبعد تحميل الحزمة وتنفيذهافوراً ثم يتفاعل بعد التحميل

تستطيع أن ترى الفرق بنفسك في Google Search Console: افتح أداة فحص الرابط لأي صفحة، ثم اختر عرض الصفحة التي زحف إليها، وانظر في HTML المسجّل. في تطبيق بلا SSR ستجد عنصر div واحداً وكتلة JSON طويلة، ومع SSR ستجد العناوين والفقرات والروابط الداخلية كلها. هذه الروابط الداخلية بالذات مهمة، لأن الزاحف يكتشف صفحاتك الأخرى من خلالها، فإذا لم يرها في HTML الخام يتأخر اكتشاف المقالات الجديدة أسابيع أحياناً بدل أيام.

تفعيل SSR في المشروع

إن بدأت مشروعك من قوالب Laravel الرسمية لـReact فملف resources/js/ssr.tsx موجود غالباً، وإلا تنشئه ليستدعي createInertiaApp مع ReactDOMServer.renderToString. بعدها تعدّل سكربت البناء في package.json ليبني النسختين:

"scripts": {
  "build": "vite build && vite build --ssr"
}

ثم تفعّل SSR في إعدادات Laravel:

// config/inertia.php
'ssr' => [
    'enabled' => true,
    'url' => 'http://127.0.0.1:13714',
],

بعد البناء يظهر الملف bootstrap/ssr/ssr.js، وهو ما سيشغّله Node. اختبره يدوياً أولاً بأمر php artisan inertia:start-ssr وافتح الموقع، ثم انتقل لتحويله إلى خدمة دائمة.

تشغيل خادم SSR كخدمة systemd

خادم SSR عملية طويلة العمر مثل عمّال الطوابير، فإذا شغّلته من نافذة SSH سيموت معها. هذا ملف الخدمة الذي نستخدمه:

[Unit]
Description=Inertia SSR
After=network.target

[Service]
User=www-data
WorkingDirectory=/var/www/app
ExecStart=/usr/bin/php artisan inertia:start-ssr
Restart=always
RestartSec=2
Environment=NODE_OPTIONS=--max-old-space-size=384

[Install]
WantedBy=multi-user.target

تحفظه في /etc/systemd/system/inertia-ssr.service ثم systemctl enable --now inertia-ssr. المهم بعد كل نشر: الكود الجديد لا يُقرأ تلقائياً لأن Node يحمل الحزمة في الذاكرة، لذلك أضف لسكربت النشر هذا السطر بعد البناء مباشرة:

php artisan inertia:stop-ssr   # systemd يعيد تشغيله بالحزمة الجديدة خلال ثانيتين

نسيان هذه الخطوة يسبب أغرب الأخطاء: الواجهة في المتصفح بالنسخة الجديدة، وHTML الأولي بالنسخة القديمة، فيظهر تحذير hydration mismatch في الكونسول وقد تومض الصفحة.

استهلاك الذاكرة الفعلي لعملية Node

من قياساتنا على تطبيقات Laravel مع React بحجم متوسط (من 40 إلى 80 صفحة):

  • عند الإقلاع تستهلك العملية نحو 70 إلى 90MB.
  • بعد ساعات من الحركة تستقر بين 120 و200MB.
  • زمن رسم الصفحة الواحدة بين 5 و30 مللي ثانية حسب حجم المكونات.

الخيار --max-old-space-size=384 في ملف الخدمة سقف أمان، فإذا تسربت الذاكرة بسبب مكون يخزن بيانات في متغير عام تموت العملية عند 384MB ويعيدها systemd خلال ثانيتين بدل أن تأكل ذاكرة السيرفر كلها. على خادم سحابي بباقة نمو (2 vCPU و4GB) بـ6,900 ريال سنوياً يتسع Laravel مع PHP-FPM وMySQL وخادم SSR بارتياح لموقع يخدم عشرات الآلاف من الزيارات يومياً.

ماذا يحدث لو سقطت خدمة SSR

هنا نقطة قوة Inertia: إذا لم يستجب خادم SSR يلتقط محول Laravel الخطأ بصمت ويرجع الصفحة العادية بعنصر div وبيانات JSON، فيرسمها React في المتصفح كأن SSR غير موجود. المستخدم لا يرى خطأ، لكن الزواحف خلال فترة السقوط ترى صفحات فارغة. لذلك لا تكتفِ بأن الموقع يعمل، بل راقب SSR نفسه:

# فحص كل 5 دقائق في cron
curl -s https://example.sa/ | grep -q '<h1' || echo "SSR down" | mail -s alert ops@example.sa

سبب آخر شائع للسقوط الصامت هو مكون يستخدم window أو localStorage مباشرة في جسمه، فهذه غير موجودة في Node، وترمي خطأ فيرجع Inertia للعرض في المتصفح لتلك الصفحة فقط. انقل هذه الاستدعاءات داخل useEffect حتى تعمل في المتصفح وحده.

أثر SSR على زمن الاستجابة

سؤال يتكرر: هل يبطئ SSR الموقع؟ الجواب بالأرقام: الرسم يضيف من 5 إلى 30 مللي ثانية للطلب، والاتصال بخادم SSR على 127.0.0.1 يضيف أقل من مللي ثانية واحدة لأنه داخل نفس السيرفر. في المقابل يرى المستخدم المحتوى قبل تحميل حزمة JavaScript، والتي قد تكون 300KB أو أكثر وتحتاج ثانية كاملة على جوال متوسط بشبكة ضعيفة، فالحصيلة أن الصفحة تظهر أسرع بوضوح. نقطة يغفل عنها كثيرون: SSR يعمل فقط في أول تحميل كامل للصفحة، أما التنقل داخل التطبيق بعد ذلك فهو طلبات Inertia بصيغة JSON لا تمر على Node أصلاً، لذلك حمل خادم SSR أقل بكثير من عدد مشاهدات الصفحات.

إذا كان موقعك خلف شبكة توزيع محتوى، تستطيع تخزين الصفحات العامة مثل المقالات وصفحات المنتجات مؤقتاً على الحافة لدقائق، فيُرسم المقال مرة واحدة ويُخدم لمئات الزوار دون أن يلمس Node أو PHP. أما صفحات البوابة خلف تسجيل الدخول فلا تخزنها أبداً لأنها خاصة بكل مستخدم، والزواحف لا تصلها أصلاً، فوجود SSR فيها لا يضر لكنه لا يفيد الظهور في البحث. حين يكبر الموقع وتزيد الحركة، فكّر في نقل خادم SSR إلى عمليتين خلف منفذين مختلفين، لكن على أغلب المواقع التي نراها تكفي عملية واحدة بسهولة، لأن الرسم يحتاج مللي ثوانٍ معدودة.

كيف تتحقق بـcurl أن الصفحة مرسومة فعلاً

لا تعتمد على ما تراه في المتصفح، فهو ينفذ JavaScript دائماً. اطلب الصفحة كما يطلبها الزاحف:

curl -s -A "Googlebot" https://example.sa/blog/some-post | grep -o '<h1[^>]*>[^<]*' | head -1
curl -s https://example.sa/ | grep -c '<title'

الأمر الأول يجب أن يطبع عنوان المقال الفعلي، فإذا لم يطبع شيئاً فالصفحة غير مرسومة. الثاني يجب أن يرجع 1 بالضبط، فإذا رجع 2 فأنت تضع وسم title في قالب Blade وفي مكون Head معاً، وهذا تكرار يربك محركات البحث. نصيحة أخيرة: قارن حجم الاستجابة قبل SSR وبعده، فصفحة مقال تقفز عادة من نحو 3KB إلى 25KB أو أكثر لأن النص صار داخل HTML.

مكون Head والبيانات المنظمة

مع SSR يُرسم مكون Head من Inertia داخل الاستجابة الأولى، فتظهر وسوم title وmeta description وog:image والروابط القانونية canonical كما يراها المستخدم. ضع البيانات المنظمة JSON-LD للمقالات والمنتجات داخل نفس المكون حتى تخرج مع كل صفحة، واحرص أن تكون الأسعار فيها مطابقة لما في الصفحة، مثل باقات الأسعار التي تُكتب بالريال شامل الضريبة.

خطوات التنفيذ

  1. أضف vite build --ssr لسكربت البناء وفعّل ssr.enabled في config/inertia.php.
  2. أنشئ خدمة systemd بسقف ذاكرة 384MB وRestart=always.
  3. أضف inertia:stop-ssr لسكربت النشر بعد البناء.
  4. انقل أي استخدام لـwindow إلى useEffect.
  5. تحقق بـcurl من العنوان ووسم title الواحد، ثم ضع فحصاً دورياً كل 5 دقائق.
مشاركة