Next.js: توجيه App Router بنظام الملفات
آخر تحديث: 2026-08-26
توجيه نظام الملفات يشبه تصنيف الكتب في المكتبة — أسماء الملفات تحدد مسار URL، وهيكل المجلدات يعمل كجدول توجيه، دون الحاجة إلى أي تكوين يدوي.
1. ما ستتعلمه
- المبدأ الأساسي لتوجيه نظام الملفات: اسم الملف هو URL
- اصطلاحات الملفات الستة: page و layout و loading و error و not-found و route
- التوجيه الديناميكي
[id]بنمط المعامل الواحد - مسارات الالتقاط الشامل
[...slug]والالتقاط الشامل الاختياري[[...catchAll]] - تعريف واستخدام المسارات مع Route Handlers API
2. قصة حقيقية لمدير تقني
(1) نقطة الألم: التكوين اليدوي للتوجيه فوضوي
Bob هو المدير التقني في شركة تجارة إلكترونية، وفريقه يحتفظ بتطبيق React SPA يحتوي على 50 صفحة. في كل مرة تُضاف صفحة جديدة، يضطر المطورون إلى تكوين ثلاثة ملفات يدويًا:
"جدول التوجيه
routes.jsلدينا يحتوي على 300 سطر. الأسبوع الماضي، أضاف Xiao Ming صفحة تفاصيل منتج جديدة لكنه نسي تسجيلpath: '/products/:id'في جدول التوجيه، مما تسبب في خطأ 404 لم يُلاحظ لمدة ساعتين كاملتين بعد نشر الصفحة."
واجه فريق Bob مشاكل إدارة التوجيه التالية:
| المشكلة | التأثير | التكرار |
|---|---|---|
| نسيان تكوين التوجيه | خطأ 404 بعد النشر | 1-2 مرات شهريًا |
| معالجة المسارات المتداخلة يدويًا | كود معقد، تسلسل هرمي مربك للمسارات | كل صفحة جديدة |
| صفحات خطأ 404/500 متفرقة | غير متسقة، تجربة مستخدم سيئة | صفحات متعددة |
| صيانة منفصلة لمسارات API | مجموعة مسارات للواجهة الأمامية وأخرى للخلفية | كل API |
(2) حلول توجيه نظام الملفات في Next.js
اسم الملف هو URL، وهيكل المجلدات هو جدول التوجيه — لم تعد هناك حاجة لتكوين توجيه
react-router-dom.
src/app/
├── page.tsx # → /
├── about/
│ └── page.tsx # → /about
├── products/
│ ├── page.tsx # → /products
│ └── [id]/
│ ├── page.tsx # → /products/1, /products/2
│ └── reviews/
│ └── page.tsx # → /products/1/reviews
└── api/
└── products/
└── route.ts # → /api/products (GET/POST)
(3) العائد
| البُعد | قبل (React Router) | بعد (توجيه نظام الملفات) |
|---|---|---|
| خطوات إضافة صفحة | 3 خطوات (إنشاء مكون + تكوين توجيه + استيراد) | خطوة واحدة (إنشاء ملف) |
| صيانة تكوين التوجيه | جدول توجيه بـ 300 سطر | صفر أسطر، يُحدد تلقائيًا من المجلد |
| أخطاء 404 عند الإطلاق | 1-2 مرات شهريًا | 0 مرات |
| إدارة موحدة لأخطاء 404/500 | استيراد يدوي | اصطلاحات الملفات تفعّل تلقائيًا |
3. مبادئ توجيه نظام الملفات
(1) اسم الملف هو URL
graph TB
subgraph "src/app/ هيكل المجلدات"
A[page.tsx] --> B[/]
C[about/page.tsx] --> D[/about]
E[products/page.tsx] --> F[/products]
G[products/promo/page.tsx] --> H[/products/promo]
I[products/id/page.tsx] --> J[/products/:id]
K[dashboard/settings/page.tsx] --> L[/dashboard/settings]
end
style A fill:#d4edda
style C fill:#d4edda
style E fill:#d4edda
| اسم الملف | URL المقابل | الوصف |
|---|---|---|
app/page.tsx |
/ |
الصفحة الرئيسية |
app/about/page.tsx |
/about |
صفحة ثابتة |
app/blog/page.tsx |
/blog |
قائمة المدونة |
app/blog/[id]/page.tsx |
/blog/1 |
توجيه ديناميكي |
app/dashboard/settings/page.tsx |
/dashboard/settings |
تداخل متعدد المستويات |
(2) اصطلاحات الملفات الستة الرئيسية
| اسم الملف | الغرض | مطلوب؟ | سلوك التصيير |
|---|---|---|---|
page.tsx |
مكون الصفحة (محتوى UI) | ✅ | Server Component (افتراضيًا) |
layout.tsx |
حاوية التخطيط (حالة مستمرة) | ✅ تخطيط الجذر | لا يُعاد تحميله أثناء التنقل |
loading.tsx |
شاشة هيكلية للتحميل | اختياري | Suspense fallback |
error.tsx |
واجهة Error Boundary | اختياري | يلتقط أخطاء المكونات الفرعية |
not-found.tsx |
صفحة 404 | اختياري | يُفعّل بـ notFound() |
route.ts |
نقطة نهاية API | اختياري | تنفيذ من جانب الخادم |
▶ مثال: إنشاء صفحتك الأولى
المخرجات:
مخطط: page.tsx; /; about/page.tsx; /about; products/page.tsx; /products.
// ============================================
// إنشاء صفحة /about
// الملف:src/app/about/page.tsx
// فقط قم بإنشاء ملف، وسيتم تسجيل التوجيه تلقائيًا
// ============================================
export default function AboutPage() {
return (
<div className="max-w-2xl mx-auto p-8">
<h1 className="text-3xl font-bold">حول TaskFlow</h1>
<p className="mt-4 text-gray-600 leading-relaxed">
TaskFlow هي منصة تعاونية لإدارة المشاريع تساعد
الفرق على التخطيط والتتبع وتسليم المشاريع بكفاءة. مبنية
بـ Next.js 16 و React 19، وتوفر تحديثات فورية وتعاون
سلس وأمان على مستوى المؤسسات.
</p>
<div className="mt-8 grid grid-cols-3 gap-4">
<div className="p-4 bg-blue-50 rounded-lg text-center">
<div className="text-2xl font-bold text-blue-600">10K+</div>
<div className="text-sm text-gray-500">مستخدم نشط</div>
</div>
<div className="p-4 bg-green-50 rounded-lg text-center">
<div className="text-2xl font-bold text-green-600">50K+</div>
<div className="text-sm text-gray-500">مشروع</div>
</div>
<div className="p-4 bg-purple-50 rounded-lg text-center">
<div className="text-2xl font-bold text-purple-600">99.9%</div>
<div className="text-sm text-gray-500">وقت التشغيل</div>
</div>
</div>
</div>
);
}
المخرجات:
المحتوى: حول TaskFlow | TaskFlow هي منصة تعاونية | 10K+ | مستخدم نشط
المخرجات:
زر http://localhost:3000/about لترى:
حول TaskFlow
TaskFlow هي منصة تعاونية لإدارة المشاريع...
10K+ مستخدم نشط | 50K+ مشروع | 99.9% وقت التشغيل
4. التوجيه الديناميكي
(1) التوجيه الديناميكي بمعامل واحد [id]
graph LR
A[app/products] --> B[page.tsx → /products]
A --> C[[id]]
C --> D[page.tsx → /products/1]
C --> E[/products/2]
style B fill:#d4edda
style D fill:#cce5ff
| نمط الملف | مثال URL | قيمة params |
|---|---|---|
app/blog/[slug]/page.tsx |
/blog/hello-world |
{ slug: 'hello-world' } |
app/products/[id]/page.tsx |
/products/42 |
{ id: '42' } |
app/users/[userId]/settings/page.tsx |
/users/123/settings |
{ userId: '123' } |
▶ مثال: صفحة تفاصيل منتج ديناميكية
المخرجات:
مخطط: app/products; page.tsx → /products; [id; page.tsx → /products/1; /products/2.
// ============================================
// صفحة تفاصيل المنتج — توجيه ديناميكي [id]
// الملف:src/app/products/[id]/page.tsx
// ============================================
// في Next.js 16، params هو Promise (غير متزامن)
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await fetch(`https://fakestoreapi.com/products/${id}`);
return (
<div className="container mx-auto p-8">
<div className="flex gap-8">
<img
src={product.image}
alt={product.title}
className="w-80 h-80 object-contain"
/>
<div>
<h1 className="text-2xl font-bold">{product.title}</h1>
<p className="text-xl text-green-600 font-bold mt-2">
${product.price}
</p>
<p className="text-sm text-gray-500 mt-1">
الفئة: {product.category}
</p>
<p className="mt-4 text-gray-700">{product.description}</p>
</div>
</div>
</div>
);
}
المخرجات:
مكون الخادم يجلب البيانات ويعرضها.
المخرجات:
زر /products/1 لترى:
Fjallraven - Foldsack No. 1 Backpack
$109.95
الفئة: men's clothing
حقيبتك المثالية للرحلات اليومية والمشي...
(2) مسارات الالتقاط الشامل [...slug]
graph TB
A[app/docs] --> B[[...slug]]
B --> C[page.tsx]
C --> D[/docs/getting-started]
C --> E[/docs/guides/installation]
C --> F[/docs/api/authentication/overview]
style C fill:#cce5ff
| نمط الملف | URL | params.slug |
|---|---|---|
app/docs/[...slug]/page.tsx |
/docs |
404 (المستوى 1 مطلوب) |
app/docs/[...slug]/page.tsx |
/docs/getting-started |
['getting-started'] |
app/docs/[...slug]/page.tsx |
/docs/guides/installation |
['guides', 'installation'] |
app/docs/[...slug]/page.tsx |
/docs/a/b/c |
['a', 'b', 'c'] |
▶ مثال: صفحة توثيق بالالتقاط الشامل
المخرجات:
مخطط: app/docs; [...slug; page.tsx; /docs/getting-started; /docs/guides/installation; /docs/api/authentication/overview.
// ============================================
// توجيه الالتقاط الشامل — صفحات توثيق متعددة المستويات
// الملف:src/app/docs/[...slug]/page.tsx
// ============================================
export default async function DocsPage({
params,
}: {
params: Promise<{ slug: string[] }>;
}) {
const { slug } = await params;
return (
<div className="container mx-auto p-8">
<nav className="text-sm text-gray-500 mb-4">
الرئيسية / Docs / {slug.join(" / ")}
</nav>
<h1 className="text-3xl font-bold">Docs: {slug.join(" > ")}</h1>
<div className="mt-8 p-6 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="font-medium">أنت تشاهد توثيق:</p>
<ul className="mt-2 list-disc list-inside">
{slug.map((segment, index) => (
<li key={index}>
المستوى {index + 1}: <code className="bg-gray-100 px-2 py-0.5 rounded">{segment}</code>
</li>
))}
</ul>
</div>
<p className="mt-4 text-gray-600">
إجمالي العمق: {slug.length} مستويات
</p>
</div>
);
}
المخرجات:
DocsPage يعرض واجهته.
المخرجات:
زر /docs/getting-started/installation لترى:
الرئيسية / Docs / getting-started / installation
Docs: getting-started > installation
أنت تشاهد توثيق:
- المستوى 1: getting-started
- المستوى 2: installation
إجمالي العمق: 2 مستويات
(3) الالتقاط الشامل الاختياري [[...catchAll]]
graph TB
A[app/categories] --> B[[[...catchAll]]]
B --> C[page.tsx]
C --> D[/categories]
C --> E[/categories/electronics]
C --> F[/categories/electronics/phones]
style C fill:#d4edda
style D fill:#d4edda
| نمط الملف | URL | params.catchAll |
|---|---|---|
app/categories/[[...catchAll]]/page.tsx |
/categories |
undefined |
app/categories/[[...catchAll]]/page.tsx |
/categories/electronics |
['electronics'] |
app/categories/[[...catchAll]]/page.tsx |
/categories/electronics/phones |
['electronics', 'phones'] |
▶ مثال: صفحة فئات بالالتقاط الشامل الاختياري
المخرجات:
مخطط: app/categories; [[...catchAll; page.tsx; /categories; /categories/electronics; /categories/electronics/phones.
// ============================================
// الالتقاط الشامل الاختياري — صفحة تصفح الفئات
// الملف:src/app/categories/[[...categories]]/page.tsx
// كل من /categories و /categories/electronics يعملان
// ============================================
export default async function CategoriesPage({
params,
}: {
params: Promise<{ categories?: string[] }>;
}) {
const { categories } = await params;
const allProducts = await fetch("https://fakestoreapi.com/products");
const categoriesList = [...new Set(allProducts.map(p => p.category))];
const filteredProducts = categories
? allProducts.filter(p => p.category === categories[0])
: allProducts;
return (
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold mb-6">
{categories ? `الفئة: ${categories[0]}` : "جميع الفئات"}
</h1>
{!categories && (
<div className="flex gap-2 mb-8 flex-wrap">
{categoriesList.map(cat => (
<a
key={cat}
href={`/categories/${encodeURIComponent(cat)}`}
className="px-4 py-2 bg-gray-100 rounded-full hover:bg-blue-100"
>
{cat}
</a>
))}
</div>
)}
<div className="grid grid-cols-4 gap-6">
{filteredProducts.slice(0, 8).map(p => (
<div key={p.id} className="border rounded-lg p-4 hover:shadow-lg">
<img src={p.image} alt={p.title} className="h-40 mx-auto" />
<p className="mt-2 font-medium text-sm truncate">{p.title}</p>
<p className="text-green-600 font-bold">${p.price}</p>
</div>
))}
</div>
</div>
);
}
المخرجات:
جلب البيانات من جانب الخادم يعرض قائمة العناصر.
المخرجات:
زر /categories لترى:
جميع أزرار الفئات(electronics, jewelery, men's clothing, women's clothing)
زر /categories/electronics لترى:
الفئة: electronics
عرض منتجات electronics فقط(8 بطاقات منتج)
5. loading.tsx و error.tsx
(1) loading.tsx — شاشة هيكلية للتحميل
graph LR
A[تنقل المستخدم] --> B[loading.tsx<br/>عرض هيكلي]
B --> C[page.tsx<br/>البيانات جاهزة]
C --> D[صفحة كاملة]
style B fill:#f8d7da
style C fill:#d4edda
▶ مثال: شاشة هيكلية للتحميل
المخرجات:
مخطط: تنقل المستخدم; loading.tsx عرض هيكلي; page.tsx البيانات جاهزة; صفحة كاملة.
// ============================================
// شاشة هيكلية للتحميل — تُعرض أثناء جلب بيانات الصفحة
// الملف:src/app/products/loading.tsx
// ============================================
export default function ProductsLoading() {
return (
<div className="container mx-auto p-8">
<div className="h-8 w-48 bg-gray-200 rounded animate-pulse mb-6" />
<div className="grid grid-cols-4 gap-6">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="border rounded-lg p-4">
<div className="h-40 bg-gray-200 rounded animate-pulse" />
<div className="h-4 bg-gray-200 rounded mt-2 animate-pulse" />
<div className="h-4 w-16 bg-gray-200 rounded mt-2 animate-pulse" />
</div>
))}
</div>
</div>
);
}
المخرجات:
يعرض قائمة عناصر باستخدام .map().
المخرجات:
عند زيارة /products، يُعرض حتى اكتمال تحميل البيانات:
8 بطاقات رمادية وهمية(متحركة بنبضات)
بعد انتهاء تحميل البيانات، يُستبدل المحتوى الوهمي تلقائيًا بالمحتوى الفعلي
بدون وميض، بدون شاشة بيضاء، انتقال سلس
(2) error.tsx — Error Boundary
▶ مثال: صفحة خطأ
المخرجات:
تُعرض الصفحة كما هو موصوف أعلاه، مع تحديث الواجهة بناءً على السلوك الموصوف.
// ============================================
// Error Boundary — التقاط أخطاء المكونات الفرعية
// الملف:src/app/products/error.tsx
// ============================================
'use client';
export default function ProductsError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="container mx-auto p-8 text-center">
<div className="max-w-md mx-auto">
<h2 className="text-2xl font-bold text-red-600 mb-4">
حدث خطأ ما!
</h2>
<p className="text-gray-600 mb-6">
{error.message || "فشل تحميل المنتجات. يرجى المحاولة مرة أخرى."}
</p>
<button
onClick={reset}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
حاول مرة أخرى
</button>
</div>
</div>
);
}
المخرجات:
المحتوى: حدث خطأ ما! | حاول مرة أخرى
المخرجات:
عند فشل جلب البيانات من صفحة المنتج، يُعرض:
حدث خطأ ما!
فشل تحميل المنتجات. يرجى المحاولة مرة أخرى.
[حاول مرة أخرى] زر → اضغط لإعادة المحاولة تلقائيًا
(3) not-found.tsx — صفحة 404
6. Route Handlers (مسارات API)
(1) الاستخدام الأساسي لـ route.ts
graph LR
A[طلب العميل] --> B[route.ts]
B --> C[GET /api/products]
B --> D[POST /api/products]
B --> E[PUT /api/products/:id]
B --> F[DELETE /api/products/:id]
style B fill:#cce5ff
| طريقة HTTP | الدالة المُصدّرة | الغرض |
|---|---|---|
GET |
export async function GET() |
قراءة البيانات |
POST |
export async function POST() |
إنشاء بيانات |
PUT |
export async function PUT() |
تحديث البيانات |
DELETE |
export async function DELETE() |
حذف البيانات |
PATCH |
export async function PATCH() |
تحديث جزئي |
7. مثال كامل: نظام تصفح منتجات التجارة الإلكترونية
// ============================================
// مثال شامل: نظام تصفح منتجات التجارة الإلكترونية
// يغطي الصفحات والتخطيط والتحميل والخطأ وتوجيه API
// ============================================
// src/app/layout.tsx — تخطيط الجذر
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="bg-gray-50">
<nav className="bg-white shadow-sm p-4">
<a href="/" className="text-xl font-bold text-blue-600">
ShopHub
</a>
</nav>
<main>{children}</main>
</body>
</html>
);
}
// src/app/page.tsx — الصفحة الرئيسية
export default function HomePage() {
return (
<div className="container mx-auto p-8 text-center">
<h1 className="text-4xl font-bold">مرحبًا بك في ShopHub</h1>
<p className="mt-4 text-gray-600">
تصفح مجموعتنا من المنتجات الرائعة.
</p>
<a
href="/products"
className="inline-block mt-6 px-8 py-3 bg-blue-600 text-white rounded-lg"
>
تصفح المنتجات
</a>
</div>
);
}
// src/app/products/page.tsx — قائمة المنتجات
export default async function ProductsPage() {
const products = await fetch(
"https://fakestoreapi.com/products"
);
return (
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold mb-6">جميع المنتجات</h1>
<div className="grid grid-cols-4 gap-6">
{products.map(p => (
<a
key={p.id}
href={`/products/${p.id}`}
className="border rounded-lg p-4 bg-white hover:shadow-lg"
>
<img
src={p.image}
alt={p.title}
className="h-40 mx-auto"
/>
<p className="mt-2 font-medium text-sm">{p.title}</p>
<p className="text-green-600 font-bold mt-1">${p.price}</p>
</a>
))}
</div>
</div>
);
}
// src/app/products/[id]/page.tsx — تفاصيل المنتج
export default async function ProductDetailPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await fetch(
`https://fakestoreapi.com/products/${id}`
);
if (!product) {
notFound();
}
return (
<div className="container mx-auto p-8">
<a
href="/products"
className="text-blue-600 hover:underline mb-4 inline-block"
>
← العودة إلى المنتجات
</a>
<div className="flex gap-8 bg-white p-8 rounded-lg shadow">
<img
src={product.image}
alt={product.title}
className="w-96 h-96 object-contain"
/>
<div>
<h1 className="text-3xl font-bold">{product.title}</h1>
<p className="text-2xl text-green-600 font-bold mt-4">
${product.price}
</p>
<p className="mt-6 text-gray-700 leading-relaxed">
{product.description}
</p>
<button className="mt-6 px-8 py-3 bg-blue-600 text-white rounded-lg">
أضف إلى السلة
</button>
</div>
</div>
</div>
);
}
// src/app/products/loading.tsx — شاشة هيكلية للتحميل
export default function ProductsLoading() {
return (
<div className="container mx-auto p-8">
<div className="h-8 w-48 bg-gray-200 rounded animate-pulse mb-6" />
<div className="grid grid-cols-4 gap-6">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="border rounded-lg p-4 bg-white">
<div className="h-40 bg-gray-200 rounded animate-pulse" />
<div className="h-4 bg-gray-200 rounded mt-2 animate-pulse" />
<div className="h-4 w-16 bg-gray-200 rounded mt-2 animate-pulse" />
</div>
))}
</div>
</div>
);
}
// src/app/products/error.tsx — Error Boundary
'use client';
export default function ProductsError({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="container mx-auto p-8 text-center">
<h2 className="text-2xl font-bold text-red-600">
فشل تحميل المنتجات
</h2>
<button
onClick={reset}
className="mt-4 px-6 py-2 bg-blue-600 text-white rounded-lg"
>
حاول مرة أخرى
</button>
</div>
);
}
المخرجات المتوقعة:
الصفحة الرئيسية(/):
مرحبًا بك في ShopHub → [تصفح المنتجات]
قائمة المنتجات(/products):
جارٍ التحميل... → 8 بطاقات هيكلية رمادية
اكتمل التحميل → شبكة بطاقات المنتج(تشمل الصور والعنوان والسعر)
تفاصيل المنتج(/products/1):
صورة المنتج (يسار) + العنوان/السعر/الوصف/أضف إلى السلة (يمين)
← العودة إلى المنتجات رابط العودة إلى القائمة
حالة الخطأ:
فشل تحميل المنتجات
[حاول مرة أخرى] زر
حالة 404:
تُعالج تلقائيًا من not-found.tsx الجذر
❓ أسئلة شائعة
params هو Promise في Next.js 16؟params و searchParams والمتغيرات المشابهة إلى Promises لدعم إنشاء الصفحات غير المتزامن. إذا استخدمتها مباشرة params.id، فستواجه خطأ TypeScript؛ يجب عليك await params لاسترداد قيمها.loading.tsx و Suspense؟loading.tsx هو Suspense fallback على مستوى الصفحة. يقوم Next.js تلقائيًا بتغليف loading.tsx داخل <Suspense>. إذا كنت بحاجة إلى حالة تحميل على مستوى المكون، يجب عليك استخدام <Suspense fallback={...}> يدويًا.error.tsx إلى use client؟error.tsx مكون عميل (Client Component) لأنه يحتاج إلى معالجة التفاعلات (مثل زر إعادة المحاولة). لا يمكن لمكونات الخادم (Server Components) أن تحتوي على معالجات أحداث أو hooks، لذا فإن توجيه 'use client' مطلوب.route.ts و page.tsx معًا؟page.tsx و route.ts متنافيين. يمكن أن يحتوي المجلد على نوع واحد فقط من معالجي المسارات — إما صفحة (page.tsx) أو API (route.ts).📖 ملخص
- توجيه نظام الملفات: اسم الملف يعمل كمسار URL، مما يلغي الحاجة إلى التكوين اليدوي لجداول التوجيه
page.tsxيعرّف محتوى الصفحة؛layout.tsxيعرّف حاوية التخطيط- التوجيه الديناميكي:
[id]يطابق معاملًا واحدًا،[...slug]يطابق مسارًا متعدد المستويات [[...catchAll]]يسمح بصفر وسائط (اختياري)؛[...slug]يتطلب وسيطًا واحدًا على الأقلloading.tsxيُغلّف تلقائيًا بـSuspenseلعرض شاشة هيكلية للتحميلerror.tsxهو مكون عميل يوفر Error Boundary وزر إعادة المحاولةnot-found.tsxيعالج صفحات 404؛ تُفعّل دالةnotFound()يدويًاroute.tsيعرّف نقاط نهاية API تدعم GET/POST/PUT/DELETE
📝 تمارين
-
تمرين أساسي (⭐): أنشئ صفحة باسم
app/blog/[slug]/page.tsxفي المشروع بحيث عند زيارة/blog/hello-world، يُخرج console القيمةslug: hello-world. -
تمرين متقدم (⭐⭐): أنشئ
app/api/todos/route.tsلتنفيذ TODO API بسيط (GET يعيد قائمة، POST ينشئ TODO)، وتحقق منه بزيارته في المتصفح. -
تحدٍّ (⭐⭐⭐): أنشئ تطبيقين،
app/docs/[...slug]/page.tsxوapp/docs/[[...slug]]/page.tsx، لاختبار سلوك الوصول إلى/docs، واستخدم جدولًا لمقارنة الفروق بينهما.