المفتاح
RENTAL-SITE-REBUILD 2026

شركة Navic — قسم Create

إعادة بناء كاملة لموقع ونظام إدارة محتوى شركة لتأجير معدّات البناء والطاقة المتجدّدة

إعادة بناء موقع قسم Create في شركة Navic. نظام إدارة محتوى على Nuxt 4 وواجهة على Astro 7 يتشاركان قاعدة Cloudflare D1 واحدة. 47,938 سطرًا، و378 اختبارًا، و3.3 أشهر من التطوير الفردي.

إعادة بناء كاملة لموقع ونظام إدارة محتوى شركة لتأجير معدّات البناء والطاقة المتجدّدة

نظرة عامة

العميل هو قسم Create في شركة Navic (navic-inc.jp)، ويعمل في تأجير وبيع معدّات الطاقة الشمسية وبطاريات التخزين وأجهزة السلامة. كان الموقع القديم مبنيًّا على Wix، فكانت صفحات تفاصيل المنتجات محكومةً بقيود القالب، ولم يكن ممكنًا تركيبها بحرّية بما يوافق طبيعة العمل.

في الموقع الجديد لم أستعمل أيّ نظام إدارة محتوى جاهز، بل بنيتُ لوحة الإدارة والواجهة من الصفر بـ Nuxt 4 وAstro 7. والهدف أن أسلّم فريق التشغيل القدرة على تركيب صفحات تفاصيل المنتجات، مع الإبقاء على سرعة الموقع الساكن في جانب الزائر. وقد تولّيتُ كل شيء وحدي، من تصميم قاعدة البيانات إلى إعدادات النشر.

حجم الكود 47,938 سطرًا (TS / Vue / Astro / SQL)، و346 ملفًا
الاختبارات 350 اختبار وحدة، و28 اختبارًا شاملًا. يشغّل CI فحص lint وفحص الأنواع واختبارات الوحدة، وشرط العبور في الثلاثة جميعًا هو صفر تحذيرات
مدّة التطوير منذ 27 أبريل 2026، نحو 3.3 أشهر، و839 التزامًا، ونسبة المساهمة الفردية 99.9%

البنية

يتشارك Worker اثنان على Cloudflare قاعدة D1 نفسها المسمّاة navic-rental: نظام إدارة المحتوى على Nuxt 4 يكتب، وواجهة Astro 7 للقراءة فقط. وتُخزَّن الوسائط في R2، ويرفعها المتصفّح مباشرةً عبر روابط موقَّعة يُصدرها نظام إدارة المحتوى بـ aws4fetch، دون المرور بالـ Worker.

تُلقي حزمة AWS SDK الرسمية على workerd الخطأ loadConfig is not a function عند تحميل الوحدة، لأن سلسلة اعتمادها تستدعي fs الخاصّة بـ Node في المستوى الأعلى. حللتُ ذلك باستبدالها بـ aws4fetch، وتركتُ خلاصة التقصّي في تعليق داخل r2-presigner.ts.

تُصيَّر صفحات الواجهة الـ19 مسبقًا وقت البناء، ولا يعمل في وقت التشغيل سوى 3 نقاط نهاية API. مكوّنات الأقسام بـ Vue بصفر JavaScript، وجزيرة التصفية بـ Solid، وحدّ الترجمة مفصول باتفاق المجلّدات في include داخل astro.config.mjs.

مخطّط عامّ لبنية الـ Worker اثنين المتشاركَين قاعدة D1: الكتابة من نظام إدارة المحتوى، وقراءة التصيير المسبق في الواجهة، والرفع المباشر إلى R2، ومسار النشر
مخطّط عامّ لبنية الـ Worker اثنين المتشاركَين قاعدة D1: الكتابة من نظام إدارة المحتوى، وقراءة التصيير المسبق في الواجهة، والرفع المباشر إلى R2، ومسار النشر

المواضع الصعبة تقنيًّا

أداة تركيب أقسام صفحة تفاصيل المنتج

كان على صفحة تفاصيل المنتج أن تعيد إنتاج دليل بصري محدَّد كما هو: 15 نوعًا من الأقسام و41 صيغة. والصعوبة لم تكن في العدد، بل في أن ثلاثة مواضع — نموذج المحرّر، ومعاينة المحرّر، والتصيير في الإنتاج — كان كلٌّ منها ينحرف عن الآخرَين على هواه. فكلّما أُضيف حقل إلى صيغة واحدة، وجب إصلاح المواضع الثلاثة جميعًا.

كان الحلّ أن أفصل الأقسام في حزمة workspace مستقلّة اسمها ‎@repo/sections، وأن أجعل مخطّط zod من 340 سطرًا هو العقد الوحيد. فتقرأ عمليةُ التحقّق في لوحة الإدارة والتصييرُ في الواجهة النوعَ نفسه. و27 مكوّن Vue تتولّى معاينة NodeView في tiptap داخل نظام إدارة المحتوى، والتصيير بصفر JavaScript في Astro.

الصيغ التي تختلف أشكال بياناتها لا أمثّلها بجعل الحقول كلّها اختيارية، بل بـ discriminatedUnion. وبهذا يُردّ النقص في الإدخال عند التحقّق وقت الحفظ، فلا يظهر خطأً في وقت التشغيل بعد أن يكون الزائر قد فتح الصفحة.

LIVE تفاعلي — جرّبه
عرض تفاعلي: تبديل صيغة القسم يعيد تشكيل حقول الإدخال في المحرّر تبعًا لها
محرّر الأقسام في نظام إدارة المحتوى: في اليسار نموذج الإدخال وفي اليمين المعاينة اللحظية، والمكوّنات المستعملة هي عينها مكوّنات الواجهة
محرّر الأقسام في نظام إدارة المحتوى: في اليسار نموذج الإدخال وفي اليمين المعاينة اللحظية، والمكوّنات المستعملة هي عينها مكوّنات الواجهة

التصيير المسبق والتغييرات غير المنشورة

للتصيير المسبق لكلّ الصفحات أثرٌ جانبيّ لا مفرّ منه: لو عدّل المحرِّر المحتوى في لوحة الإدارة، فلن يتغيّر في الموقع شيء حتى تُنفَّذ عملية البناء التالية. وفي مكانٍ يديره شخص واحد، لا أحد يقوى على أن يتذكّر الذهاب إلى لوحة التحكّم ليطلق البناء بيده.

كان الحلّ أن أعامل «غير المنشور» بوصفه حالةً من الدرجة الأولى في النظام. فبعد نجاح مسار الكتابة أستدعي markBuildPending لأرفع راية جدول السطر الواحد build_state. وتحتفظ جملة SQL بوقت أوّل تغيير عبر ‎COALESCE(pending_since, ?)‎، فيمكن للشريط أن يعرض مدّة التراكم.

زرّ النشر يطلب ‎POST /api/build/trigger‎، فيجلب الخادمُ ‎WEB_DEPLOY_HOOK_URL‎ بنفسه. ولا يصل الرابط الحامل للسرّ إلى المتصفّح إطلاقًا. ولم أختر أن يُطلَق البناء تلقائيًّا مع كلّ حفظ، لأن تعديل عشرة حقول تباعًا يعني تشغيل البناء عشر مرّات؛ فجعلتُه نشرًا مجمّعًا بعد أن يراجعه إنسان.

شريط التنبيه بالتغييرات غير المنشورة وزرّ النشر، وهو ثابت في أعلى نظام إدارة المحتوى
شريط التنبيه بالتغييرات غير المنشورة وزرّ النشر، وهو ثابت في أعلى نظام إدارة المحتوى

مسح المراجع في الجداول كلّها قبل حذف صورة

يُشار إلى جدول media من أربعة مواضع، غير أن المفتاحين الأجنبيَّين هما ‎featured_media_id‎ و‎cover_media_id‎ لا غير. أمّا ‎images_json‎ و‎parts_json‎ و‎gallery_json‎ فتحمل معرّفات خامًا، و‎content_json‎ يحمل مفاتيح كائنات R2، ولا يحمي أيًّا منها قيدٌ. وما يفوت يجعل الصورة تُعيد 404، ودون رجعة.

  • أعمدة المفاتيح الأجنبية تُطابَق بالمعرّف كما هو
  • مصفوفات الأرقام تُفكَّك بـ json_each وتُطابَق واحدةً واحدة
  • مصفوفات الكائنات يُستخرج منها معرّف الوسيط بـ json_extract
  • أعمدة الإعدادات غير محدّدة الشكل يُتحقَّق أوّلًا من سلامتها بوصفها JSON ثم تُفكَّك
  • متن الأقسام يحتفظ بمفتاح الكائن، فيُنتقل فيه إلى مطابقة جزئية تشمل صيغته المهرَّبة
  • إن وُجد ما يطابق، يُعاد 409 مع تعداد مواضع الاستعمال، ليعرف فريق التشغيل أيُّ محتوى يستعمل تلك الصورة

المطابقة الجزئية ليست دقيقة، لكنها مفاضلة مقصودة: النتيجة الإيجابية الخاطئة تكلّف رفضًا واحدًا يمكن التراجع عنه، أمّا ما يفوت فيعني كائن R2 محذوفًا لا يعود. وكان التنفيذ السابق يبحث عن السمة ‎data-media-id‎، غير أن تلك السمة لم تُكتب قطّ في أيّ موضع من الكود، فتعطّل المسح مرّةً، في صمت.

لم أسلك طريق تطبيع هذه المراجع في جدول وسيط. هو الصواب في الكتب. غير أن التطبيع يجرّ معه مزامنة delete وinsert مع كلّ حفظ للمحتوى. وحذف الوسائط عملية قليلة التكرار، فمن الأجدى أن أشتري بمسحٍ شامل واحد عبءَ المزامنة الدائم.

الموقع المنشور