متغيرات البيئة في Next.js: التضمين وقت البناء، مسارات التسريب الحقيقية، وإفشال البناء باستخدام Zod
جدول المحتويات
عن الكاتب

Ahmed Mahmoud
كاتب ومطوّر
مهندس برمجيات شغوف بتطوير الويب وتصميم تجربة المستخدم.
مؤسّس ديفيا · eng-ahmed.com ↗المتغير البيئي الذي يبدأ بالبادئة NEXT_PUBLIC_ لا يُقرأ وقت التشغيل — إذ يستبدل Next.js كل تعبير process.env.NEXT_PUBLIC_* بقيمة نصية حرفية أثناء تنفيذ next build. هذه الحقيقة الواحدة تفسر القيمة التي لا تتحدث إلا بعد إعادة البناء، والوصول الديناميكي الذي يعيد undefined في المتصفح، ولماذا لا يمكن لبناء واحد أن يخدم بيئتين مختلفتين. في Devya واجهنا كل واحدة من هذه الحالات في مشاريع Next.js حقيقية، وهذه هي القائمة التي يراجعها فريقنا قبل أول نشر.
الخلاصات الرئيسية
• تُضمَّن متغيرات NEXT_PUBLIC_ في حزمة المتصفح وقت البناء؛ تغيير القيمة في لوحة التحكم لا يفعل شيئًا حتى البناء التالي.
• التضمين استبدال نصي ساكن: الوصول بمفتاح ديناميكي مثل process.env[name] أو عبر التفكيك من process.env يعيد undefined في المتصفح.
• المتغيرات الخادمية تُحذف من حزم المتصفح، فقراءة process.env.SECRET داخل مكوّن عميل تعيد undefined. التسريب الحقيقي يحدث عبر تمرير السر كخاصية تُسلسل في حمولة RSC، أو عبر إعادة تسمية المتغير بالبادئة NEXT_PUBLIC_.
• حزمة server-only تحوّل استيراد ملف الأسرار من مكوّن عميل إلى خطأ بناء واضح بدل خطر صامت.
• التحقق من المتغيرات بمخطط Zod عند تحميل الوحدة، مع استيرادها في next.config.ts، يجعل غياب متغير يُفشل next build بدلًا من أول طلب في الإنتاج.
لماذا لم يتغير شيء بعد تعديل متغير NEXT_PUBLIC_؟
لأن القيمة التي يراها المتصفح جرى تجميدها داخل JavaScript في آخر بناء. خلال next build ينفذ المجمّع عملية بحث واستبدال: كل تعبير ساكن مثل process.env.NEXT_PUBLIC_API_URL يتحول إلى النص الذي كان يحمله المتغير على جهاز البناء في تلك اللحظة. الحزمة المنشورة تحتوي على النص الحرفي لا على عملية قراءة، لذلك تعديل القيمة في لوحة التحكم أو في خيارات Docker لا يغيّر إلا ما سيراه البناء التالي.
الاستبدال نصي، وهذا ينتج أعطالًا تبدو كأخطاء في Next.js وليست كذلك: الوصول بمفتاح ديناميكي يعيد undefined، والتفكيك من process.env يعيد undefined أيضًا، لأن أيًا منهما ليس تعبيرًا حرفيًا يستطيع المجمّع مطابقته. أما كود الخادم فلا يخضع لهذه القيود — داخل مكوّنات الخادم ومعالجات المسارات يبقى process.env كائن Node.js حيًا يُقرأ وقت الطلب.
كيف يصل سر خاص بالخادم إلى المتصفح فعلًا؟
ليس عبر process.env — فالمتغيرات غير المسبوقة بالبادئة تُحذف من حزم المتصفح. التسريبات التي رأيناها فعلًا تسلك طريقين آخرين. الأول هو التسلسل: يقرأ مكوّن خادم سرًا ويمرره كخاصية إلى مكوّن عميل، فتُضمَّن القيمة في حمولة RSC داخل استجابة HTML وتظهر في مصدر الصفحة. والثاني ردة فعل «الإصلاح»: يرى المطور undefined في المتصفح فيعيد تسمية المتغير بالبادئة NEXT_PUBLIC_، فيختفي الخطأ ويتحول السر إلى نص حرفي في ملف JavaScript عام.
الدفاع الرخيص هو حزمة server-only: وحدة فارغة يفشل البناء إذا انتهى استيرادها إلى شجرة استيرادات العميل. نضعها في أعلى أي ملف يلمس الأسرار، فإذا استورده مكوّن عميل يومًا — مباشرة أو عبر سلسلة استيرادات — فشل البناء برسالة واضحة بدل أن يُشحن السر.
كيف نُفشل عملية البناء عند غياب متغير مطلوب؟
نمرر البيئة عبر مخطط Zod في وحدة تُنفذ أثناء البناء، ونستورد كل متغير من خلالها. عندها يوقف المتغير الغائب أو المشوه عملية next build بخطأ مسمّى بدل أن يظهر undefined في أول كود يقرؤه.
تفصيلان مهمان: نذكر المفاتيح صراحة بدل تمرير process.env كاملًا، لأن متغيرات العميل يجب أن تظهر كتعابير حرفية ليتمكن المجمّع من تضمينها. ونستورد الوحدة من next.config.ts ليُنفَّذ المخطط وقت البناء حتى لو لم يلمسه أي مسار. حزمة @t3-oss/env-nextjs تغلف الفكرة نفسها مع فصل جاهز بين متغيرات الخادم والعميل.
أي ملف .env هو الذي يفوز؟
يحمّل Next.js ملفات .env بنفسه دون حاجة إلى حزمة dotenv، وبأسبقية ثابتة: المتغير المضبوط مسبقًا في الصدفة يتغلب على كل الملفات، والملف الأكثر تخصيصًا يتغلب على الأعم.
• ملف .env: يُحمَّل في كل البيئات ويُرفع إلى المستودع كقيم افتراضية مشتركة.
• ملف .env.local: يُحمَّل في كل البيئات عدا NODE_ENV=test ولا يُرفع أبدًا — فهو لأسرار الجهاز المحلي.
• ملفا .env.development و.env.production: يُحمَّلان عند تطابق NODE_ENV ويُرفعان كافتراضات لكل بيئة.
• الملفات .env.development.local و.env.production.local: الأعلى أسبقية بين الملفات ولا تُرفع أبدًا.
قيمة NODE_ENV ليست حرة: يعرف Next.js القيم development وproduction وtest فقط؛ يفرض next dev الأولى ويفرض next build وnext start الثانية. بيئة staging تُمثَّل بـ NODE_ENV=production مع متغير خاص مثل APP_ENV=staging.
هل يمكن لبناء واحد أن يخدم بيئتي staging والإنتاج معًا؟
للمتغيرات الخادمية نعم — فهي تُقرأ وقت الطلب، وصورة Docker نفسها يمكن أن تُقلع بقيم DATABASE_URL مختلفة. لمتغيرات NEXT_PUBLIC_ لا — فقيمها موجودة داخل JavaScript سلفًا. على Vercel تبقى المشكلة غير مرئية لأن كل بيئة تحصل على بنائها الخاص بمتغيراتها الخاصة، أما نشر Docker بوضع standalone تُرقّى فيه الصورة نفسها بين البيئات فيكشفها فورًا.
مخارجنا بالترتيب: إبقاء الإعدادات في جهة الخادم وجعل كود العميل ينادي مسارات same-origin عبر rewrite، أو قراءة القيمة في مكوّن خادم وقت الطلب وتمريرها كخاصية، أو القبول ببناء مستقل لكل بيئة. أما تعديل المتغير وإعادة نشر الأثر نفسه فلا يعمل، ويفشل بصمت — القيمة القديمة تستمر في الظهور دون أي خطأ في أي مكان.
الأسئلة الشائعة
س: هل تصل المتغيرات الخادمية إلى المتصفح؟
ج: لا. يحذف Next.js المتغيرات غير المسبوقة بـ NEXT_PUBLIC_ من حزم المتصفح فتُقرأ كـ undefined. التسرب يحدث فقط عند تمرير السر كخاصية إلى مكوّن عميل أو إعادة تسميته بالبادئة العامة.
س: هل نحتاج حزمة dotenv في مشروع Next.js؟
ج: لا. يحمّل Next.js ملفات .env و.env.local والمتغيرات الخاصة بكل NODE_ENV بنفسه وفق ترتيب موثق، وإضافة dotenv فوقه تخلق ترتيب تحميل ثانيًا متعارضًا.
س: هل يمكن ضبط NODE_ENV على قيمة staging؟
ج: لا. يعرف Next.js ثلاث قيم فقط: development وproduction وtest. تُمثَّل بيئة staging بقيمة NODE_ENV=production مع متغير خاص مثل APP_ENV=staging.
س: لماذا يعيد process.env[name] قيمة undefined في المتصفح؟
ج: لأن التضمين استبدال ساكن وقت البناء للتعابير الحرفية فقط، ولا يوجد كائن process.env في المتصفح للوصول إليه بمفتاح ديناميكي.
س: كيف نحصل على متغيرات بيئة آمنة الأنواع في TypeScript؟
ج: نصدّر كائن env متحققًا منه عبر Zod من وحدة واحدة ونستورده في كل مكان بدل لمس process.env مباشرة؛ فالمخطط يمنح ضمان وقت التشغيل والإكمال التلقائي معًا.
قراءات إضافية
هندسة الواجهات
الوضع الداكن دون وميض في Next.js 16: كيف نستخدم سكربت سمة حاجبًا وcolor-scheme وزر تبديل آمنًا مع الـ Hydration
وميض الوضع الداكن عند إعادة تحميل الصفحة مشكلة توقيت: الخادم لا يستطيع قراءة localStorage. نشرح سكربت السمة الحاجب الذي نستخدمه في Next.js 16، ولماذا لا نضع السمة في cookie حفاظًا على الرسم الثابت، وكيف نبني زر تبديل يمر بالـ hydration دون أي عدم تطابق.
هندسة الواجهات
صور Open Graph الديناميكية في Next.js 16: كيف نبني opengraph-image.tsx ونتحقق من إصدار Satori الذي تضمّنه next/og ونحافظ على صحة معاينات الروابط
واجهت بطاقات المشاركة المولّدة لدينا ثلاث مفاجآت: خط WOFF2 لم يستطع المحرّك قراءته، وملف README لـ Satori يصف محرّكًا أحدث من المضمّن في Next.js، وصفحة ظلّت تعرض صورة الـ layout. هذه ملاحظاتنا الميدانية حول opengraph-image.tsx في Next.js 16 وأولوية البيانات الوصفية وفحص og:image قبل مشاركة أي رابط.