→ العودة للمدونة
معمارية البرمجيات

التصدير الثابت في Next.js — الدليل الشامل للإنتاج

كل ما تحتاج معرفته لنشر موقع Next.js ثابت بالكامل: أوضاع التصدير، القيود، المسارات الديناميكية، تحسين الصور، والنشر.

بقلم يوسف محمود
2025-08-01
4 دقيقة قراءة
التصدير الثابت في Next.js — الدليل الشامل للإنتاج

لماذا التصدير الثابت؟

الاستضافة الحديثة تتغير. منصات مثل GitHub Pages وCloudflare Pages والاستضافة المشتركة التقليدية توفر خدمة ملفات ثابتة سريعة وبأسعار معقولة — لكنها لا تشغّل Node.js. إذا كان موقع Next.js الخاص بك لا يحتاج إلى خادم حي لجلب البيانات، فإن التحول الكامل للتصدير الثابت باستخدام output: "export" يمنحك:

  • بدون Cold Starts — HTML مُعالج مسبقاً، يُقدَّم فورياً.
  • قدرة تخزين CDN قصوى — كل مسار هو ملف ثابت.
  • نشر مُبسَّط — ارفع مجلد out/ في أي مكان.
  • كفاءة التكلفة — لا خادم للدفع عليه أو صيانته.

هذه هي المعمارية التي اخترتها لموقع معرضي الشخصي y0ussef.com، وهذا الدليل يلتقط كل ما تعلمته.


إعداد التصدير الثابت

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "export",       // ← هذا هو الإعداد الرئيسي
  trailingSlash: true,    // يُنشئ /about/index.html بدلاً من /about.html
  images: {
    unoptimized: true,    // مطلوب — تحسين الصور يحتاج لخادم
  },
};

export default nextConfig;

⚠️ unoptimized: true يُعطّل تغيير حجم الصور وتحويلها إلى WebP المدمج في Next.js. ستحتاج للتعامل مع هذا بنفسك — أنا أستخدم سكريبت ما قبل البناء المبني على sharp.


القيود الأساسية

فهم ما لا يمكنك فعله في التصدير الثابت أمر بالغ الأهمية:

الميزةالتصدير الثابتالسبب
getStaticProps✅ مدعوم بالكامليعمل وقت البناء
getStaticPaths✅ مدعوم بالكامليُنشئ المسارات الديناميكية مسبقاً
getServerSideProps❌ غير مدعوميحتاج خادماً حياً
مسارات API❌ غير مدعوميحتاج خادماً حياً
Middleware❌ غير مدعوميحتاج Edge runtime
Streaming / RSC❌ غير مدعومللخادم فقط
توليد صور OG❌ غير مدعوميحتاج مسار API

توجيه اللغة بدون i18n Plugin

توجيه i18n المدمج في Next.js لا يعمل مع التصدير الثابت. الحل الذي أستخدمه هو التوجيه القائم على المسار:

pages/
  en/
    index.tsx    →  /en/
    about.tsx    →  /en/about/
    blog/
      index.tsx  →  /en/blog/
      [slug].tsx →  /en/blog/[slug]/
  ar/
    index.tsx    →  /ar/
    about.tsx    →  /ar/about/

كل مجلد لغة هو فضاء مسار منفصل. الاتجاه واللغة يُعيَّنان عبر useEffect في _app.tsx:

// src/pages/_app.tsx
useEffect(() => {
  const isArabic = router.pathname.startsWith("/ar");
  document.documentElement.dir = isArabic ? "rtl" : "ltr";
  document.documentElement.lang = isArabic ? "ar" : "en";
}, [router.pathname]);

بسيط، بدون اعتماديات، ويعمل بشكل مثالي مع SSG.


المسارات الديناميكية مع getStaticPaths

لمقالات المدونة أو المشاريع أو أي محتوى ديناميكي:

// src/pages/en/blog/[slug].tsx
import { GetStaticProps, GetStaticPaths } from "next";

export const getStaticPaths: GetStaticPaths = () => {
  const slugs = getAllBlogSlugs(); // يقرأ من src/content/blog/
  return {
    paths: slugs.map((slug) => ({ params: { slug } })),
    fallback: false, // 404 للـ slugs غير المعروفة — مطلوب للتصدير الثابت
  };
};

export const getStaticProps: GetStaticProps = ({ params }) => {
  const post = getBlogPostBySlug(params!.slug as string, "en");
  if (!post) return { notFound: true };
  return { props: { post } };
};

fallback: false إلزامي للتصدير الثابت. كلاهما fallback: 'blocking' وfallback: true يحتاجان خادماً.


تحسين الصور بدون الخادم

بما أن images.unoptimized: true يُعطّل تحسين Next.js، كتبت سكريبت ما قبل البناء باستخدام sharp:

// scripts/convert-images.js
const sharp = require("sharp");
const fs = require("fs");
const path = require("path");

const IMAGES_DIR = path.join(__dirname, "..", "public", "images");

async function convertImages() {
  const files = fs.readdirSync(IMAGES_DIR);
  for (const file of files) {
    const ext = path.extname(file).toLowerCase();
    if (![".png", ".jpg", ".jpeg"].includes(ext)) continue;

    const source = path.join(IMAGES_DIR, file);
    const target = path.join(IMAGES_DIR, `${path.basename(file, ext)}.webp`);

    if (fs.existsSync(target)) continue;

    await sharp(source).webp({ quality: 85 }).toFile(target);
    console.log(`تم التحويل: ${file}${path.basename(target)}`);
  }
}

convertImages();

ربطه بعملية البناء:

{
  "scripts": {
    "convert-images": "node scripts/convert-images.js",
    "build": "npm run convert-images && next build"
  }
}

توليد sitemap.xml وقت البناء

بدون مسارات API، يجب أن يكون الـ sitemap ملفاً ثابتاً يُولَّد أثناء ما قبل البناء:

// scripts/generate-sitemap.js
const SITE_URL = "https://y0ussef.com";

function generateSitemap() {
  const blogSlugs = getBlogSlugs();
  const allRoutes = [...staticRoutes, ...blogSlugs.flatMap(slug => [
    `/en/blog/${slug}/`,
    `/ar/blog/${slug}/`,
  ])];
  
  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${allRoutes.map(route => `  <url><loc>${SITE_URL}${route}</loc></url>`).join("\n")}
</urlset>`;

  fs.writeFileSync("public/sitemap.xml", xml);
  console.log(`تم توليد الـ Sitemap: ${allRoutes.length} رابط`);
}

الملخص

التصدير الثابت مع Next.js Pages Router نهج قوي ومُقلَّل التقدير لمواقع المعارض الشخصية والصفحات التسويقية والمواقع الغنية بالمحتوى التي لا تحتاج ميزات الخادم الحي.

المبادئ الرئيسية التي أتبعها:

  1. كل البيانات وقت البناءgetStaticProps لكل شيء.
  2. fallback: false على جميع المسارات الديناميكية.
  3. تعامل مع تحسين الصور بنفسك عبر سكريبت sharp قبل البناء.
  4. توليد Sitemaps وقت البناء عبر سكريبت Node.js.
  5. توجيه اللغة القائم على المسار بدلاً من إعداد i18n.

نشر سعيد. 🚀

Y

يوسف محمود

Full-Stack Engineer & Project Engineer

هل لديك مشروع في ذهنك؟

دعنا نناقش متطلباتك ونبني شيئاً رائعاً معاً.

احجز استشارة فنية