مقالWeb Development

Next.js 15 App Router: ٥ دروس اتعلمتها من بناء موقع production ثنائي اللغة

دروس حقيقية من بناء yousifdev.com — الـ i18n routing، الـ Server vs Client Components، عرض TipTap، مشاكل Framer Motion، والنشر على Vercel مع Cloudflare.

Yousif MohamedApril 1, 20268 دقيقة قراءة0 مشاهدة
TypeScriptReactNext.jsTailwind CSSBest Practices
Next.js 15 App Router: ٥ دروس اتعلمتها من بناء موقع production ثنائي اللغة

الدروس دي ما اتعلمتهاش من tutorial. اتعلمتها من إني عملت deploy.

منصتي — yousifdev.com — منصة محتوى ثنائية اللغة (إنجليزي/عربي) مبنية بـ Next.js 15 App Router، Supabase، Tailwind CSS 4، وFramer Motion. منشورة على Vercel مع Cloudflare DNS.

في الطريق، قابلت مشاكل مفيش documentation حذّرني منها. هنا ٥ دروس كنت أتمنى حد قالهملي قبل ما أبدأ.


الدرس ١: الـ i18n Routing أبسط مما تتخيّل

كنت متوقع إن تعدد اللغات هيكون صداع. طلع لا — لما فهمت الـ pattern.

الإعداد هو [locale] dynamic segment في root الـ app directory. كل route بيعيش جوا app/[locale]/(public)/.... الـ Middleware بيكشف لغة المستخدم وبيعمل redirect لـ /en/... أو /ar/... أوتوماتيك.

استخدمت next-intl واللي بيتعامل مع ملفات الرسائل وكشف اللغة والـ useTranslations() hook بشكل ممتاز. ملفين JSON — messages/en.json وmessages/ar.json — والواجهة بتتكلم اللغتين.

المطب: كل لينك داخلي لازم يتضمن الـ locale prefix. لو نسيت وربطت بـ /blog بدل /en/blog، الـ middleware بيعمل redirect وبتحصل flash. استخدم navigation helpers بتاعة next-intl والمشكلة بتروح.

ضيف الـ i18n من أول يوم. تركيبها بعد ما التطبيق اتبنى معناه إنك هتلمس كل لينك، كل layout، وكل data-fetching function.


الدرس ٢: Server Components بالـ Default — Client Components بالاستثناء

ده أكبر تغيير في طريقة التفكير في App Router. في Pages Router القديم، كل حاجة كانت client-side بالـ default. دلوقتي العكس — كل حاجة Server Component إلا لو إنت قلت غير كده.

عملياً، ده معناه إن صفحاتك بتجيب البيانات مباشرة على السيرفر. مفيش useEffect، مفيش loading spinners للبيانات الأولية، مفيش client-side fetch waterfalls. الـ HTML بيوصل مرندر بالكامل.

بتضيف 'use client' بس لما تحتاج browser APIs: حالة الفورم، click handlers، أنيميشن Framer Motion، أو محرر TipTap.

المطب: مش بتقدر تمرّر props مش serializable من Server Component لـ Client Component. اتعلمت ده بالطريقة الصعبة لما حاولت أمرّر Lucide icon components كـ props. Functions وclass instances وReact components مش بيعدّوا حدود الـ RSC. مرّر strings أو plain objects بدلاً، وخلّي الـ Client Component يعمل import للـ icons اللي محتاجها.

فكّر في الـ Server Components كطبقة البيانات والـ Client Components كطبقة التفاعل. خلّي الحدود نضيفة وتطبيقك هيفضل سريع.


الدرس ٣: TipTap JSON لـ HTML مش بسيط زي ما بيبان

بخزّن كل محتوى المقالات كـ TipTap JSON في أعمدة Supabase JSONB. الأدمن بيكتب في محرر TipTap، الـ JSON بيتحفظ، والصفحة العامة بترندره لـ HTML باستخدام generateHTML() من @tiptap/html.

بيبان بسيط. بس هنا بينكسر:

الـ extensions بتاعة الـ renderer لازم تطابق الـ editor بالظبط. لو المحرر بيستخدم CodeBlockLowlight بس الـ renderer بيستخدم الـ CodeBlock الأساسي من StarterKit، الـ JSON فيه attributes الـ renderer مش فاهمها — وبيفشل بهدوء أو بيرندر كلام فاضي.

الـ styling معركة لوحدها. في البداية اعتمدت على prose-* modifier classes بتاعة Tailwind عشان أنسّق الـ HTML المرندر. في Tailwind CSS 4، كتير من الـ modifiers دي مش بتتـ compile صح. الحل كان إني أضيف CSS classes مباشرة عن طريق خيار HTMLAttributes بتاع TipTap على كل extension — فالـ HTML المُنتَج بيوصل منسّق جاهز.

خلّي المحرر والـ renderer متزامنين. لما تضيف extension للمحرر، ضيفها للـ renderer في نفس الوقت — وإلا الـ output هينكسر بهدوء.


الدرس ٤: Framer Motion والـ App Router علاقتهم معقّدة

Framer Motion مكتبة Client Component. كل component بيستخدم motion.div أو AnimatePresence محتاج 'use client' في أوله. ده عادي — بس له عواقب.

لو الـ page component بتاعك بيستخدم Framer Motion مباشرة، الصفحة كلها بتتحوّل Client Component — وبتخسر server-side data fetching. الصفحة دلوقتي محتاجة useEffect وloading states بدل ما كانت ببساطة بترجع البيانات من السيرفر.

الحل: اعمل wrapper components صغيرة. صفحتك تفضل Server Component بتجيب البيانات. بتمرّر البيانات لـ Client Component wrapper بيتعامل مع الأنيميشن. الصفحة سريعة، البيانات server-rendered، والأنيميشن لسه شغّال.

الـ layout animations عبر تغيير الـ routes — النوع اللي كان بيشتغل ممتاز في Pages Router — مش بيشتغل بنفس الطريقة في App Router. كل route هو React tree منفصل. لو محتاج page transitions، هتحتاج template component أو حل custom.

خلّي الأنيميشن في Client Components صغيرة ومركّزة. ما تخلّيش Framer Motion يصيب الـ Server Components بتاعة صفحاتك.


الدرس ٥: Vercel + Cloudflare DNS = إعداد واحد لازم تظبطه

بستخدم Cloudflare كـ domain registrar ومزوّد DNS، مع Vercel بيستضيف تطبيق Next.js. ده setup شائع — بس فيه إعداد واحد حرج لو غلطت فيه كل حاجة بتتكسر.

اضبط سجلات الـ DNS على وضع DNS-only، مش Proxied. في Cloudflare، الـ default للسجلات الجديدة هو "Proxied" (أيقونة السحابة البرتقالية). ده بيوجّه الترافيك عبر شبكة Cloudflare — واللي بيتعارض مع SSL وشبكة edge بتاعة Vercel. النتيجة: أخطاء SSL handshake، redirect loops، أو موقعك ببساطة مش بيحمّل.

اضغط على السحابة البرتقالية عشان تتحوّل رمادي (DNS-only). خلاص. Vercel بيتعامل مع SSL والتخزين المؤقت والـ edge delivery لوحده.

حاجتين كمان لازم تعملهم update:

  • Environment variables — اضبط NEXT_PUBLIC_SITE_URL على الدومين بتاعك في Vercel. لو لسه بيشاور على localhost:3000 أو preview URL، الـ OG images والـ canonical URLs والـ sitemap كلهم هيكونوا غلط.

  • Supabase Auth URLs — عدّل الـ redirect URLs في Supabase dashboard عشان تتضمّن الدومين بتاعك. لو عندك localhost بس، تسجيل الدخول للأدمن هيفشل في production بخطأ redirect mismatch.

DNS-only mode في Cloudflare. URL الـ production في الـ environment variables. الـ Auth redirects متحدّثة. انسى أي واحدة فيهم والـ deployment بينكسر بهدوء.


إيه اللي كنت هعمله غير كده

لو بدأت المشروع ده من الأول، حاجة واحدة كانت هتتغيّر: كنت هضيف الـ i18n من أول commit. إضافة دعم اللغتين بعد ما التطبيق اتبنى خلاص معناها إنك هتلمس كل layout، كل لينك navigation، كل data query، وكل SEO tag. اشتغلت، بس كانت أسبوع refactoring كان ممكن يبقى ساعتين setup في البداية.

كل حاجة تانية — Supabase، TipTap، Framer Motion، Vercel — كنت هختارهم تاني. الـ stack صلب. الدروس فوق دي هي الحواف الخشنة، مش dealbreakers.

انزّل حاجة حقيقية. الدروس عايشة هناك.

مشاركة

مقالات ذات صلة