Nested Layoutها

در درس قبل Root Layout را ساختیم که برای تمام صفحات سایت اعمال می‌شود. اما بخش‌های مختلف یک سایت اغلب ساختار مخصوص خودشان را دارند: مثلاً بخش نوشته‌ها یک سربرگ و چیدمان مشترک می‌خواهد و داشبورد مدیریت یک منوی کناری. Next.js برای این نیاز Layoutهای تودرتو (Nested Layouts) را فراهم کرده است. در این درس یاد می‌گیریم چطور برای هر بخش از مسیرها Layout جداگانه بسازیم و این Layoutها چگونه درون هم قرار می‌گیرند.

پیش‌نیاز

  • درس «Root Layout، تنظیم lang="fa" و dir="rtl"»
  • درس «ساخت صفحات با page.tsx»

ایده‌ی اصلی

هر پوشه‌ی داخل app می‌تواند فایل layout.tsx خودش را داشته باشد. چنین Layoutی برای همان پوشه و تمام مسیرهای زیرمجموعه‌ی آن اعمال می‌شود. وقتی چند Layout روی یک مسیر اثر بگذارند، از بیرونی‌ترین (Root Layout) تا درونی‌ترین، به‌ترتیب درون هم قرار می‌گیرند و هر Layout، بخش درونی‌تر را از طریق children می‌گیرد.

می‌توان آن را مثل عروسک‌های روسی دید: Root Layout بیرونی‌ترین عروسک است، Layout بخش نوشته‌ها داخل آن قرار می‌گیرد و صفحه‌ی نهایی در عمیق‌ترین نقطه نشسته است.

ساختار پیشنهادی برای DevNotes

text
app/
├── layout.tsx            ← Root Layout (کل سایت)
├── page.tsx              ← /
├── about/
│   └── page.tsx          ← /about
└── posts/
    ├── layout.tsx        ← Layout مخصوص بخش نوشته‌ها
    ├── page.tsx          ← /posts
    └── new/
        └── page.tsx      ← /posts/new

با این ساختار، مسیرهای /posts و /posts/new هر دو درون app/posts/layout.tsx رندر می‌شوند، ولی / و /about از آن بی‌خبرند.

آدرس Layoutهای اعمال‌شده (از بیرون به درون)
/ Root Layout ← صفحه
/about Root Layout ← صفحه
/posts Root Layout ← Layout نوشته‌ها ← صفحه
/posts/new Root Layout ← Layout نوشته‌ها ← صفحه

عملی: اضافه‌کردن هدر و فوتر به Root Layout

ابتدا بخش‌های مشترک کل سایت را در Root Layout می‌گذاریم. فایل app/layout.tsx را به این شکل به‌روزرسانی کنید (تغییر فقط داخل body است):

tsx
// app/layout.tsx
import type { Metadata } from "next";
import type { ReactNode } from "react";
import "./globals.css";

export const metadata: Metadata = {
  title: "DevNotes",
  description: "وب‌سایتی برای انتشار و مدیریت نوشته‌های فنی",
};

export default function RootLayout({
  children,
}: Readonly<{ children: ReactNode }>) {
  return (
    <html lang="fa" dir="rtl">
      <body>
        <header>
          <strong>DevNotes</strong>
        </header>
        {children}
        <footer>
          <small>پروژه‌ی آموزشی دوره‌ی Next.js 16</small>
        </footer>
      </body>
    </html>
  );
}

هدر و فوتر در تمام صفحات سایت دیده می‌شوند. لینک‌های ناوبری را بعداً، در درس «لینک‌سازی با Link»، به هدر اضافه می‌کنیم.

عملی: ساخت Layout بخش نوشته‌ها

حالا فایل app/posts/layout.tsx را بسازید:

tsx
// app/posts/layout.tsx
import type { ReactNode } from "react";

export default function PostsLayout({
  children,
}: Readonly<{ children: ReactNode }>) {
  return (
    <section>
      <p>
        <strong>بخش نوشته‌ها</strong> | مقاله‌های فنی DevNotes
      </p>
      {children}
    </section>
  );
}

عملی: صفحه‌ی ایجاد نوشته

برای اینکه ببینیم Layout روی مسیرهای تودرتو هم اعمال می‌شود، یک صفحه‌ی ساده در /posts/new می‌سازیم. فرم واقعی را در فصل «فرم‌ها و Server Actions» اضافه می‌کنیم:

tsx
// app/posts/new/page.tsx
export default function NewPostPage() {
  return (
    <main>
      <h1>نوشته‌ی جدید</h1>
      <p>فرم ایجاد نوشته به‌زودی در اینجا قرار می‌گیرد.</p>
    </main>
  );
}

صفحه‌ی app/posts/page.tsx را از درس «ساخت صفحات با page.tsx» تغییر نمی‌دهیم.

توضیح کد

  • هر Layout یک کامپوننت با پراپ children است و باید آن را در جای مناسب رندر کند. اگر children را رندر نکنید، صفحه نمایش داده نمی‌شود.
  • Layout بخش نوشته‌ها تگ‌های html و body ندارد؛ آن‌ها فقط در Root Layout هستند.
  • نام تابع (PostsLayout) دلخواه است، اما به‌صورت قرارداد با Layout تمام می‌شود.
  • این Layoutها بدون "use client" هستند، پس Server Component‌اند و روی سرور اجرا می‌شوند.

خروجی و رفتار مورد انتظار

آدرس آنچه روی صفحه می‌بینید
/about هدر ← محتوای صفحه ← فوتر
/posts هدر ← خط «بخش نوشته‌ها» ← محتوای صفحه ← فوتر
/posts/new هدر ← خط «بخش نوشته‌ها» ← «نوشته‌ی جدید» ← فوتر

ساختار HTML نهایی برای /posts تقریباً به این شکل درمی‌آید:

html
<body>
  <header>...</header>
  <section>               <!-- Layout نوشته‌ها -->
    <p>...</p>
    <main>...</main>      <!-- محتوای page.tsx -->
  </section>
  <footer>...</footer>
</body>

رفتار Layout هنگام جابه‌جایی بین صفحات

یکی از مهم‌ترین مزیت‌های Layout این است که هنگام ناوبری، فقط بخشی که تغییر کرده دوباره رندر می‌شود:

  • اگر از /posts به /posts/new بروید، Layout نوشته‌ها و Root Layout باقی می‌مانند و فقط صفحه عوض می‌شود.
  • اگر از /posts به /about بروید، Layout نوشته‌ها حذف می‌شود، ولی Root Layout همچنان باقی است.
  • چون Layout بین ناوبری‌ها دوباره ساخته نمی‌شود، State کامپوننت‌های کلاینتی داخل آن حفظ می‌شود. این نکته وقتی به Client Component رسیدیم کاربرد پیدا می‌کند (فصل «Server Components و Client Components»).

این رفتار با ایده‌ی «رندر جزئی» (Partial Rendering) شناخته می‌شود و دلیل سریع‌بودن ناوبری در App Router است.

چند نکته‌ی مهم

  • ارتباط Layout و صفحه از طریق props: Layout نمی‌تواند داده را از طریق props مستقیماً به children بدهد؛ children صرفاً یک جایگاه رندر است. برای اشتراک داده بین Layout و صفحات از دریافت داده در هرکدام، Context (در Client Componentها) یا توابع مشترک دسترسی به داده استفاده می‌کنیم. این موضوع را در فصل «Rendering و Data Fetching» می‌بینیم.
  • پارامترهای مسیر پویا: Layout در مسیرهای پویا params را به‌صورت یک Promise دریافت می‌کند، دقیقاً مثل صفحات. جزئیات را در درس «مسیرهای پویا، params ناهمگام و مسیرهای تودرتو» می‌بینیم.
  • Layout فقط روی ریشه‌ی زیرمسیرها اثر می‌گذارد: Layout در app/posts روی /about اثری ندارد. اگر بخواهید یک Layout روی چند مسیرِ هم‌سطح اعمال شود ولی در آدرس ظاهر نشود، از Route Groups استفاده می‌کنیم؛ موضوع درس «Route Groups و سازمان‌دهی مسیرها».
  • Layout با Metadata: هر Layout هم می‌تواند Metadata خودش را تعریف کند و Metadata درونی‌تر مقدار بیرونی‌تر را بازنویسی می‌کند. جزئیات در درس «Metadata ثابت و پویا» است.

ساختار پروژه پس از این درس

text
app/
├── layout.tsx            ← هدر و فوتر کل سایت
├── page.tsx              ← /
├── globals.css
├── about/
│   └── page.tsx          ← /about
└── posts/
    ├── layout.tsx        ← Layout بخش نوشته‌ها
    ├── page.tsx          ← /posts
    └── new/
        └── page.tsx      ← /posts/new

جمع‌بندی

  • هر پوشه‌ی داخل app می‌تواند فایل layout.tsx داشته باشد که برای آن پوشه و تمام مسیرهای زیرمجموعه‌اش اعمال می‌شود.
  • Layoutها به‌ترتیب از Root Layout تا درونی‌ترین Layout درون هم قرار می‌گیرند و هر کدام بخش درونی را با children رندر می‌کند.
  • فقط Root Layout تگ‌های html و body دارد.
  • هنگام جابه‌جایی بین صفحاتی که Layout مشترک دارند، Layout دوباره ساخته نمی‌شود و فقط بخش تغییر‌کرده به‌روز می‌شود.
  • در DevNotes اکنون Root Layout هدر و فوتر دارد و بخش /posts یک Layout اختصاصی دارد.
  • در درس بعد، «Route Groups و سازمان‌دهی مسیرها»، یاد می‌گیریم چطور مسیرها را بدون تغییر در آدرس‌ها دسته‌بندی کنیم و به هر دسته Layout جدا بدهیم.