تخطَّ إلى المحتوى

لماذا بنيت موقعي الشخصي بـ TanStack Start وStrapi

التقنيات التي يقوم عليها هذا الموقع وأسباب اختيارها: مسارات مضبوطة الأنواع، ودوال خادم، ونظام محتوى أستضيفه بنفسي، وثلاث لغات منذ اليوم الأول.

دحمان عبد الملك

20 أكتوبر 2026قراءة دقيقتينالجزء 1 من 3

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

تشرح هذه السلسلة كيف بُني الموقع. يتناول هذا الجزء الأول أهم قرارين: TanStack Start للموقع، وStrapi لإدارة المحتوى.

بنية المشروع

مستودع واحد يضم تطبيقين، تديرهما أداتا pnpm وTurborepo:

  • apps/web هو الموقع: TanStack Start وTanStack Router وTanStack Query، مع Tailwind CSS للتنسيق.
  • apps/cms هو Strapi 5، حيث يُحفظ كل نص ومشروع ووظيفة باللغات الثلاث.

يعمل كلاهما في حاويات Docker على خادمي الافتراضي الخاص، وأنشرهما عبر Dokploy. لا ارتباط بمنصة بعينها، وفاتورة شهرية واحدة يمكن توقّعها.

لماذا TanStack Start

أستخدم Next.js في معظم مشاريع عملائي، لذا كان هذا تغييرًا مقصودًا. أقنعتني ثلاثة أمور.

المسارات مضبوطة الأنواع. الرابط إلى صفحة غير موجودة يفشل عند الترجمة. كل عنوان في هذا الموقع يبدأ باللغة (/en و/ar و/fr)، ومدقّق الأنواع يتأكد أن أي رابط لا ينساها.

دوال الخادم تتحقق من مدخلاتها. كل ما يقرأ من نظام المحتوى يعمل في دالة خادم مع مخطط Zod لمعاملاتها. لا يتصل المتصفح بـ Strapi مباشرة أبدًا، ولا يعرف عنوانه الداخلي.

features/projects/projects.functions.ts
export const getProjects = createServerFn({ method: 'GET' })
  .validator(z.object({ lang: languageSchema }))
  .handler(async ({ data }) => {
    const response = await getStrapiClient()
      .collection('projects')
      .find({
        locale: data.lang,
        sort: ['order:asc', 'title:asc'],
        populate: CARD_POPULATE,
      })
    return projectListSchema.parse(response).data
  })

تحميل البيانات قابل للتركيب. يطلب مُحمّل كل مسار من TanStack Query ما تحتاجه الصفحة بالضبط. والصفحات التي تحتاج البيانات نفسها تتشارك النتيجة المخزّنة: الصفحة الرئيسية وصفحة المشاريع تقرآن قائمة المشاريع نفسها، فالانتقال بينهما لا يكلّف طلبًا جديدًا.

routes/$lang/projects/index.tsx
export const Route = createFileRoute('/$lang/projects/')({
  validateSearch: projectsSearchSchema,
  loader: ({ context, params }) =>
    context.queryClient.query({
      ...projectsQuery(params.lang),
      staleTime: 'static',
    }),
  component: ProjectsPage,
})

الثمن هو حداثة الأداة. لدى Next.js سنوات من الإجابات في كل منتدى، أما TanStack Start فأحدث عهدًا، وفي مرات قليلة قرأت شيفرته المصدرية بدل مقال في مدوّنة. في موقع أصونه بنفسي، كان ضبط الأنواع يستحق ذلك.

لماذا Strapi

أردت للمحتوى واجهة تحرير حقيقية، وترجمات سليمة، واستضافة ذاتية. يلبّي Strapi 5 الثلاثة:

  • ترجمة لكل حقل. كل نص موجود بالإنجليزية والعربية والفرنسية، بينما تتشارك اللغات التواريخ والروابط والصور.
  • مسودة ثم نشر. أستطيع تجهيز مشروع أو مقال ونشره حين يكتمل، ولا يطلب الموقع إلا المحتوى المنشور.
  • مسارات مخصّصة حيث يلزم. التعليقات ورسائل التواصل تأتي من العموم، لذا تمرّ عبر مسارات مخصّصة صغيرة بحدود للطلبات بدل الواجهة الافتراضية.

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

ما الذي سأفعله بشكل مختلف

أن أحسم التصميم قبل بناء الصفحات. بنيت الصفحات الأولى ثم أعدت تصميمها مرتين. اليوم يعيش التصميم المعتمد في وثيقة داخل المستودع، وتُبنى كل صفحة انطلاقًا منه.

يتناول الجزء الثاني الجانب الذي يهمّني أكثر في هذا التصميم: أن تكون العربية لغة أصيلة في الموقع، لا نسخة معكوسة تُضاف في النهاية.

النشرة البريدية

تصلك المقالات الجديدة على بريدك

رسالة واحدة لكل مقال جديد. يمكنك إلغاء الاشتراك في أي وقت.

تفضّل قارئ الخلاصات؟ خلاصة RSS

الردود

لا يظهر على الموقع.

تظهر الردود بعد الموافقة عليها.

تواصل

لديك مشروع في ذهنك؟
لنبنِه معًا.

متاح لوظائف عن بُعد ولمشاريع العمل الحر.