Next.js

App Router في Next.js — دليل شامل 2026

📅 2026-11-13⏱ 8 دقائق قراءة
في المقال السابق، بنيت أول تطبيق Next.js وتعلمت الأساسيات. الآن سنتعمق في **App Router** — وهو **نظام التوجيه الحديث** في Next.js 15. في هذا الدليل العملي، سنأخذك خطوة بخطوة لفهم App Router بالكامل، مع تمارين وحلول. ## ما هو App Router؟ **App Router** هو نظام التوجيه **الجديد** في Next.js (منذ الإصدار 13). **يعتمد على:** **نظام الملفات هو الراوتر**. <table> <thead> <tr> <th>الملف</th> <th>الرابط</th> </tr> </thead> <tbody> <tr> <td><code>app/page.tsx</code></td> <td><code>/</code></td> </tr> <tr> <td><code>app/about/page.tsx</code></td> <td><code>/about</code></td> </tr> <tr> <td><code>app/blog/page.tsx</code></td> <td><code>/blog</code></td> </tr> <tr> <td><code>app/blog/[slug]/page.tsx</code></td> <td><code>/blog/any-post</code></td> </tr> <tr> <td><code>app/(shop)/cart/page.tsx</code></td> <td><code>/cart</code></td> </tr> <tr> <td><code>app/dashboard/settings/page.tsx</code></td> <td><code>/dashboard/settings</code></td> </tr> </tbody> </table> ## أنواع الملفات في App Router <table> <thead> <tr> <th>الملف</th> <th>الوظيفة</th> </tr> </thead> <tbody> <tr> <td><code>page.tsx</code></td> <td>صفحة (URL)</td> </tr> <tr> <td><code>layout.tsx</code></td> <td>تخطيط مشترك</td> </tr> <tr> <td><code>loading.tsx</code></td> <td>شاشة التحميل</td> </tr> <tr> <td><code>error.tsx</code></td> <td>معالج الأخطاء</td> </tr> <tr> <td><code>not-found.tsx</code></td> <td>صفحة 404</td> </tr> <tr> <td><code>route.ts</code></td> <td>API Endpoint</td> </tr> <tr> <td><code>template.tsx</code></td> <td>قالب (يعاد عند التنقل)</td> </tr> <tr> <td><code>default.tsx</code></td> <td>للمسارات المتوازية</td> </tr> </tbody> </table> ## المسارات الأساسية ### 1. الصفحة الرئيسية **`app/page.tsx`** → `/` ### 2. مسار عادي **`app/about/page.tsx`** → `/about` ### 3. مسار متداخل **`app/blog/tech/page.tsx`** → `/blog/tech` ### 4. مسار ديناميكي **`app/blog/[slug]/page.tsx`** → `/blog/أي-شيء` **`app/blog/[slug]/page.tsx`:** ```tsx export default function BlogPost({ params, }: { params: Promise<{ slug: string }>; }) { return <h1>المقال: {params.slug}</h1>; } ``` **⚠️ لاحظ:** في Next.js 15، `params` هو **Promise** — يجب استخدام `async/await`. ### 5. مسار ديناميكي شامل **`app/docs/[...slug]/page.tsx`** → `/docs/a/b/c` ```tsx export default async function Docs({ params, }: { params: Promise<{ slug: string[] }>; }) { const { slug } = await params; return <h1>المسار: {slug.join("/")}</h1>; } ``` ### 6. مسار اختياري **`app/shop/[[...slug]]/page.tsx`** → `/shop` أو `/shop/a/b` ## مجموعات المسارات (Route Groups) استخدم `( )` لتنظيم المسارات **بدون التأثير على URL**. ``` app/ ├── (marketing)/ │ ├── about/ │ │ └── page.tsx → /about │ └── contact/ │ └── page.tsx → /contact ├── (shop)/ │ ├── products/ │ │ └── page.tsx → /products │ └── cart/ │ └── page.tsx → /cart └── layout.tsx ``` **الفائدة:** تخطيط مختلف لكل مجموعة: ``` app/ ├── (marketing)/ │ ├── layout.tsx # تخطيط التسويق │ └── about/page.tsx └── (shop)/ ├── layout.tsx # تخطيط المتجر └── cart/page.tsx ``` ## Layouts المتداخلة ``` app/ ├── layout.tsx # التخطيط الجذري ├── page.tsx └── dashboard/ ├── layout.tsx # تخطيط لوحة التحكم ├── page.tsx └── settings/ └── page.tsx ``` **`app/dashboard/layout.tsx`:** ```tsx export default function DashboardLayout({ children, }: { children: React.ReactNode; }) { return ( <div className="flex"> <aside className="w-64 bg-gray-900 text-white p-4"> <nav> <a href="/dashboard">لوحة التحكم</a> <a href="/dashboard/settings">الإعدادات</a> </nav> </aside> <main className="flex-1 p-8">{children}</main> </div> ); } ``` **النتيجة:** كل صفحات `/dashboard/*` ستحتوي على الشريط الجانبي. ## Loading — شاشة التحميل **`app/dashboard/loading.tsx`:** ```tsx export default function Loading() { return ( <div className="flex items-center justify-center min-h-screen"> <div className="animate-spin rounded-full h-12 w-12 border-4 border-blue-600 border-t-transparent" /> </div> ); } ``` **النتيجة:** عند تحميل أي صفحة في `/dashboard`، تظهر شاشة التحميل. ## Error — معالج الأخطاء **`app/dashboard/error.tsx`:** ```tsx "use client"; export default function Error({ error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { return ( <div className="text-center p-8"> <h1 className="text-4xl font-bold text-red-600 mb-4"> ❌ حدث خطأ </h1> <p className="text-gray-600 mb-6">{error.message}</p> <button onClick={reset} className="bg-blue-600 text-white px-6 py-3 rounded-lg" > حاول مرة أخرى </button> </div> ); } ``` **⚠️ مهم:** `error.tsx` يجب أن يكون **Client Component** (`"use client"`). ## Not Found — صفحة 404 **`app/not-found.tsx`:** ```tsx import Link from "next/link"; export default function NotFound() { return ( <div className="min-h-screen flex items-center justify-center text-center"> <div> <h1 className="text-9xl font-bold text-gray-900">404</h1> <p className="text-2xl text-gray-600 mb-8">الصفحة غير موجودة</p> <Link href="/" className="bg-blue-600 text-white px-8 py-3 rounded-lg" > العودة للرئيسية </Link> </div> </div> ); } ``` ## التنقل بين الصفحات ### 1. `<Link>` — الطريقة الأساسية ```tsx import Link from "next/link"; <Link href="/about">من نحن</Link> ``` ### 2. `useRouter` — التنقل برمجياً ```tsx "use client"; import { useRouter } from "next/navigation"; export default function LoginButton() { const router = useRouter(); const handleLogin = () => { // ... منطق تسجيل الدخول router.push("/dashboard"); }; return <button onClick={handleLogin}>تسجيل الدخول</button>; } ``` ### 3. `redirect` — إعادة التوجيه ```tsx import { redirect } from "next/navigation"; export default async function Page() { const isLoggedIn = false; if (!isLoggedIn) { redirect("/login"); } return <h1>مرحباً</h1>; } ``` ## useRouter و usePathname **`app/components/NavLink.tsx`:** ```tsx "use client"; import Link from "next/link"; import { usePathname } from "next/navigation"; interface NavLinkProps { href: string; children: React.ReactNode; } export default function NavLink({ href, children }: NavLinkProps) { const pathname = usePathname(); const isActive = pathname === href; return ( <Link href={href} className={`px-4 py-2 rounded-lg transition ${ isActive ? "bg-blue-600 text-white" : "text-gray-700 hover:bg-gray-100" }`} > {children} </Link> ); } ``` ## generateStaticParams **للمسارات الديناميكية في التصدير الثابت:** ```tsx export function generateStaticParams() { const posts = [ { slug: "post-1" }, { slug: "post-2" }, ]; return posts.map((post) => ({ slug: post.slug, })); } ``` ## مثال عملي: مدونة ### 1. الصفحة الرئيسية `app/page.tsx` ```tsx import Link from "next/link"; export default function HomePage() { return ( <main className="p-8"> <h1 className="text-4xl font-bold mb-6">📝 مدونتي</h1> <Link href="/blog" className="text-blue-600 hover:underline"> تصفح المقالات → </Link> </main> ); } ``` ### 2. قائمة المقالات `app/blog/page.tsx` ```tsx import Link from "next/link"; const posts = [ { slug: "learn-nextjs", title: "تعلم Next.js" }, { slug: "react-hooks", title: "React Hooks" }, { slug: "typescript-tips", title: "نصائح TypeScript" }, ]; export default function BlogPage() { return ( <main className="p-8 max-w-4xl mx-auto"> <h1 className="text-4xl font-bold mb-8">المقالات</h1> <ul className="space-y-4"> {posts.map((post) => ( <li key={post.slug}> <Link href={`/blog/${post.slug}`} className="text-xl text-blue-600 hover:underline" > {post.title} </Link> </li> ))} </ul> </main> ); } ``` ### 3. المقال الفردي `app/blog/[slug]/page.tsx` ```tsx import Link from "next/link"; import { notFound } from "next/navigation"; const posts = { "learn-nextjs": { title: "تعلم Next.js", content: "..." }, "react-hooks": { title: "React Hooks", content: "..." }, "typescript-tips": { title: "نصائح TypeScript", content: "..." }, }; export function generateStaticParams() { return Object.keys(posts).map((slug) => ({ slug })); } export default async function BlogPost({ params, }: { params: Promise<{ slug: string }>; }) { const { slug } = await params; const post = posts[slug as keyof typeof posts]; if (!post) notFound(); return ( <main className="p-8 max-w-4xl mx-auto"> <Link href="/blog" className="text-blue-600 hover:underline mb-8 inline-block"> ← العودة </Link> <h1 className="text-4xl font-bold mb-4">{post.title}</h1> <p>{post.content}</p> </main> ); } ``` ## تمارين عملية ### تمرين 1: 3 صفحات متداخلة أنشئ `/dashboard/profile` و `/dashboard/settings`. **الحل:** ``` app/dashboard/profile/page.tsx app/dashboard/settings/page.tsx ``` ### تمرين 2: مسار ديناميكي أنشئ `/users/[id]` يعرض ID المستخدم. **الحل:** ```tsx export default async function UserPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; return <h1>المستخدم: {id}</h1>; } ``` ### تمرين 3: Route Group نظّم الصفحات في مجموعتين. **الحل:** ``` app/(marketing)/about/page.tsx app/(shop)/products/page.tsx ``` ### تمرين 4: Layout مخصص أنشئ Layout للوحة التحكم. **الحل:** ```tsx // app/dashboard/layout.tsx export default function Layout({ children }: { children: React.ReactNode }) { return ( <div className="flex"> <aside>Sidebar</aside> <main>{children}</main> </div> ); } ``` ### تمرين 5: Loading أضف شاشة تحميل. **الحل:** ```tsx // app/loading.tsx export default function Loading() { return <div>جاري التحميل...</div>; } ``` ### تمرين 6: Error أضف معالج أخطاء. **الحل:** ```tsx "use client"; export default function Error({ reset }: { reset: () => void }) { return ( <div> <h1>خطأ!</h1> <button onClick={reset}>حاول مرة أخرى</button> </div> ); } ``` ### تمرين 7: NavLink نشط أنشئ NavLink يظهر الرابط النشط. **الحل:** (راجع `NavLink.tsx` أعلاه) ### تمرين 8: مدونة كاملة ابنِ مدونة بكل ما تعلمته. **الحل:** (راجع المثال العملي أعلاه) ## حل المشاكل الشائعة ### 🔴 المشكلة 1: `params` غير متاح مباشرة **السبب:** في Next.js 15، `params` هو Promise. **الحل:** ```tsx export default async function Page({ params, }: { params: Promise<{ slug: string }>; }) { const { slug } = await params; } ``` ### 🔴 المشكلة 2: Link لا يعمل **السبب:** نسيت الاستيراد. **الحل:** ```tsx import Link from "next/link"; ``` ### 🔴 المشكلة 3: useRouter لا يعمل **السبب:** لم تُضف `"use client"`. **الحل:** أضف في أعلى الملف. ### 🔴 المشكلة 4: generateStaticParams مفقود **السبب:** مع `output: 'export'`، تحتاج تعريف كل المسارات. **الحل:** ```tsx export function generateStaticParams() { return [{ slug: "post-1" }, { slug: "post-2" }]; } ``` ### 🔴 المشكلة 5: 404 في الإنتاج **السبب:** السيرفر لا يعرف المسارات. **الحل:** أضف `rewrites` في `firebase.json`. ## جدول الملفات الخاصة <table> <thead> <tr> <th>الملف</th> <th>الوظيفة</th> <th>ملاحظة</th> </tr> </thead> <tbody> <tr> <td><code>page.tsx</code></td> <td>صفحة</td> <td>مطلوب للعرض</td> </tr> <tr> <td><code>layout.tsx</code></td> <td>تخطيط</td> <td>لا يُعاد عند التنقل</td> </tr> <tr> <td><code>template.tsx</code></td> <td>قالب</td> <td>يُعاد عند التنقل</td> </tr> <tr> <td><code>loading.tsx</code></td> <td>تحميل</td> <td>يظهر أثناء التحميل</td> </tr> <tr> <td><code>error.tsx</code></td> <td>خطأ</td> <td>Client Component</td> </tr> <tr> <td><code>not-found.tsx</code></td> <td>404</td> <td>عند عدم وجود الصفحة</td> </tr> <tr> <td><code>route.ts</code></td> <td>API</td> <td>لا يعرض UI</td> </tr> </tbody> </table> ## قائمة تحقق نهائية <table> <thead> <tr> <th>المهمة</th> <th>الحالة</th> </tr> </thead> <tbody> <tr> <td>فهم نظام الملفات هو الراوتر</td> <td>⬜</td> </tr> <tr> <td>إنشاء مسارات متداخلة</td> <td>⬜</td> </tr> <tr> <td>المسارات الديناميكية</td> <td>⬜</td> </tr> <tr> <td>Route Groups</td> <td>⬜</td> </tr> <tr> <td>Layouts المتداخلة</td> <td>⬜</td> </tr> <tr> <td>Loading و Error</td> <td>⬜</td> </tr> <tr> <td>حل التمارين الثمانية</td> <td>⬜</td> </tr> </tbody> </table> ## ماذا بعد هذا المقال؟ الآن بعد أن أتقنت App Router، أنت جاهز للمقال التالي: 1. **Server Components** — تعمق. 2. **Data Fetching** — جلب البيانات. 3. **API Routes** — بناء APIs. ## الخلاصة في هذا المقال، تعلمت: - ✅ ما هو App Router. - ✅ نظام الملفات هو الراوتر. - ✅ المسارات الديناميكية. - ✅ Route Groups. - ✅ Layouts المتداخلة. - ✅ Loading و Error. - ✅ التنقل بين الصفحات. - ✅ حل 8 تمارين عملية. **تذكر:** App Router هو **المستقبل** — أتقنه جيداً.