پوشه‌های خصوصی و ساختاردهی فایل‌ها

در درس‌های قبل دیدیم که فقط پوشه‌هایی که فایل page.tsx داشته باشند مسیر قابل مشاهده می‌سازند. این یعنی می‌توانیم فایل‌های دیگری، مثل کامپوننت‌ها و توابع کمکی، کنار صفحات بگذاریم بدون اینکه ناخواسته آدرس عمومی بسازند. در این درس سه ابزار برای مرتب نگه‌داشتن پروژه را می‌بینیم: ایده‌ی Colocation، پوشه‌های خصوصی (Private Folders) و استراتژی‌های ساختاردهی فایل‌ها. سپس ساختار DevNotes را بر اساس آن‌ها مرتب می‌کنیم.

پیش‌نیاز

  • درس «Route Groups و سازمان‌دهی مسیرها»
  • درس «ساخت صفحات با page.tsx»

Colocation: فایل‌ها را نزدیک جایی بگذارید که استفاده می‌شوند

در App Router هیچ فایلی فقط به‌خاطر اینکه داخل app است مسیر عمومی نمی‌شود. فقط فایل‌های با نام‌های قراردادی مثل page.tsx و route.ts محتوایی را از آن مسیر عمومی می‌کنند. بنابراین می‌توان فایل‌های وابسته به یک صفحه را کنار همان صفحه نگه داشت:

text
app/(public)/posts/
├── page.tsx              ← /posts (مسیر عمومی)
├── PostsIntro.tsx        ← مسیر عمومی نمی‌سازد
└── helpers.ts            ← مسیر عمومی نمی‌سازد

این قابلیت «Colocation» نام دارد و باعث می‌شود وقتی صفحه‌ای را تغییر می‌دهید، همه‌ی فایل‌های مرتبط جلوی چشم باشند.

پوشه‌های خصوصی (Private Folders)

اگر فایل‌های کمکی هم‌سطح با پوشه‌های مسیر باشند، تشخیص اینکه کدام پوشه «مسیر» و کدام «کمکی» است سخت می‌شود. برای حل این مشکل می‌توان نام پوشه را با زیرخط شروع کرد: _components. چنین پوشه‌ای:

  • به‌طور کامل از مسیریابی کنار گذاشته می‌شود؛ حتی اگر داخلش page.tsx بگذارید، آدرسی ساخته نمی‌شود.
  • برای هر کسی که پروژه را می‌خواند نشان می‌دهد که این پوشه «داخلی» است.
  • فایل‌های داخلش مثل فایل‌های معمولی import می‌شوند.
مسیر فایل آیا آدرس می‌سازد؟
app/posts/page.tsx بله، /posts
app/posts/_components/PostsIntro.tsx خیر
app/posts/_components/page.tsx خیر، چون کل پوشه‌ی _components از مسیریابی خارج است

اگر واقعاً نیاز دارید یک بخش از آدرس با زیرخط شروع شود، نام پوشه را با %5F (کد URL زیرخط) بنویسید، مثلاً %5Fadmin برای آدرس /_admin. این مورد در DevNotes پیش نمی‌آید.

تفاوت پوشه‌ی خصوصی و Route Group

ویژگی Route Group مثل (public) پوشه‌ی خصوصی مثل _components
نقش گروه‌بندی مسیرها و Layoutها نگهداری فایل‌های غیرمسیری
مسیر داخلش ساخته می‌شود؟ بله، فقط نام گروه از آدرس حذف می‌شود خیر، هیچ مسیری ساخته نمی‌شود
می‌تواند layout.tsx داشته باشد؟ بله معنی ندارد

استراتژی‌های ساختاردهی پروژه

Next.js ساختار خاصی را اجبار نمی‌کند. سه الگوی رایج:

الگو شکل کار مناسب برای
۱. همه‌چیز خارج از app app فقط برای مسیریابی است؛ کامپوننت‌ها و منطق در پوشه‌های ریشه مثل components و lib هستند پروژه‌های کوچک تا متوسط با کدهای مشترک زیاد
۲. همه‌چیز داخل app فایل‌های مشترک و غیرمسیری در پوشه‌های خصوصی داخل app نگه داشته می‌شوند کسانی که می‌خواهند همه‌چیز یکجا باشد
۳. تقسیم بر اساس ویژگی یا مسیر کدهای مشترک در بیرون یا ریشه‌ی app و کدهای اختصاصی هر بخش کنار همان بخش پروژه‌های بزرگ‌تر با چند بخش مستقل

الگوی درست یکی نیست؛ مهم این است که یک قاعده انتخاب کنید و در کل پروژه رعایتش کنید. در DevNotes الگوی سوم را به شکل ساده به کار می‌بریم:

  • کدهای مشترک بین چند صفحه (مثل هدر و فوتر) در پوشه‌های ریشه‌ی پروژه: components و بعداً lib.
  • کدهایی که فقط یک بخش استفاده می‌کند در پوشه‌ی خصوصی _components کنار همان بخش.

چون پروژه را بدون پوشه‌ی src ساخته‌ایم، پوشه‌های components و lib مستقیماً در کنار app قرار می‌گیرند. در فصل‌های بعد، بخصوص درس «معماری و سازمان‌دهی کد در پروژه‌ی واقعی»، دوباره سراغ ساختار پروژه می‌رویم.

عملی: مرتب‌سازی ساختار DevNotes

در درس قبل هدر و فوتر را مستقیماً داخل app/(public)/layout.tsx نوشتیم. حالا آن‌ها را به کامپوننت‌های مشترک تبدیل می‌کنیم و برای صفحه‌ی نوشته‌ها یک کامپوننت اختصاصی در پوشه‌ی خصوصی می‌سازیم.

فایل‌های متأثر: app/(public)/layout.tsx (ساده‌تر می‌شود)، app/(public)/posts/page.tsx (یک کامپوننت را استفاده می‌کند) و سه فایل جدید. آدرس‌های سایت تغییر نمی‌کنند.

گام ۱: کامپوننت‌های مشترک

پوشه‌ی components را در ریشه‌ی پروژه (هم‌سطح با app) بسازید:

tsx
// components/SiteHeader.tsx
export default function SiteHeader() {
  return (
    <header>
      <strong>DevNotes</strong>
    </header>
  );
}
tsx
// components/SiteFooter.tsx
export default function SiteFooter() {
  return (
    <footer>
      <small>پروژه‌ی آموزشی دوره‌ی Next.js 16</small>
    </footer>
  );
}

گام ۲: استفاده در Layout گروه عمومی

tsx
// app/(public)/layout.tsx
import type { ReactNode } from "react";
import SiteHeader from "@/components/SiteHeader";
import SiteFooter from "@/components/SiteFooter";

export default function PublicLayout({
  children,
}: Readonly<{ children: ReactNode }>) {
  return (
    <>
      <SiteHeader />
      {children}
      <SiteFooter />
    </>
  );
}

گام ۳: کامپوننت اختصاصی در پوشه‌ی خصوصی

پوشه‌ی _components را داخل app/(public)/posts بسازید:

tsx
// app/(public)/posts/_components/PostsIntro.tsx
export default function PostsIntro() {
  return (
    <p>
      در این بخش نوشته‌های فنی DevNotes فهرست می‌شوند. فهرست واقعی
      بعد از اتصال به داده اضافه می‌شود.
    </p>
  );
}

گام ۴: استفاده در صفحه‌ی نوشته‌ها

tsx
// app/(public)/posts/page.tsx
import PostsIntro from "./_components/PostsIntro";

export default function PostsPage() {
  return (
    <main>
      <h1>نوشته‌ها</h1>
      <PostsIntro />
    </main>
  );
}

توضیح کد

  • @/components/SiteHeader: alias مسیر @/* از درس «ساختار فایل‌ها و پوشه‌ها» است و به ریشه‌ی پروژه اشاره می‌کند. برای کدهای مشترک از آن استفاده می‌کنیم تا مسیرهای نسبی طولانی (مثل ../../../components) نداشته باشیم.
  • ./_components/PostsIntro: چون فایل کنار صفحه است، مسیر نسبی کوتاه و گویا است؛ همین نزدیکی به معنی «فقط این بخش از آن استفاده می‌کند».
  • هیچ‌کدام از این کامپوننت‌ها "use client" ندارند، پس Server Component هستند. اگر در آینده یکی از آن‌ها به State یا رویداد نیاز پیدا کند، فقط همان را Client Component می‌کنیم.
  • خروجی HTML تغییری نکرده است؛ فقط کد مرتب‌تر شده است. این کار «بازسازی» (Refactoring) نام دارد.

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

text
devnotes/
├── app/
│   ├── layout.tsx
│   ├── globals.css
│   ├── (public)/
│   │   ├── layout.tsx
│   │   ├── page.tsx
│   │   ├── about/
│   │   │   └── page.tsx
│   │   └── posts/
│   │       ├── layout.tsx
│   │       ├── page.tsx
│   │       ├── _components/
│   │       │   └── PostsIntro.tsx
│   │       └── new/
│   │           └── page.tsx
│   └── (dashboard)/
│       ├── layout.tsx
│       └── dashboard/
│           └── page.tsx
└── components/
    ├── SiteHeader.tsx
    └── SiteFooter.tsx

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

  • صفحات /، /about، /posts، /posts/new و /dashboard دقیقاً مثل قبل کار می‌کنند.
  • در /posts علاوه بر عنوان، متن توضیحی کامپوننت PostsIntro هم دیده می‌شود.
  • آدرس /posts/_components صفحه‌ی ۴۰۴ می‌دهد؛ چون پوشه‌ی خصوصی هیچ مسیری نمی‌سازد.

نکته‌های مهم

  • یک قاعده انتخاب کنید و به آن وفادار بمانید. مهم‌تر از «بهترین ساختار» این است که ساختار در کل پروژه یکدست باشد.
  • زیرخط فقط برای مسیریابی است. اگر پوشه‌ی کمکی‌تان را با _ شروع نکنید هم مسیر ساخته نمی‌شود (چون page.tsx ندارد)، اما با پیشوند زیرخط خوانندگان کد فوراً تشخیص می‌دهند که پوشه‌ی داخلی است. همچنین از تداخل ناخواسته با نام‌های قراردادی آینده‌ی Next.js جلوگیری می‌کند.
  • فایل‌های ویژه نام‌های قراردادی ثابت دارند. page، layout، loading، error، not-found و route نام‌هایی هستند که Next.js می‌شناسد. نام کامپوننت‌های خودتان نباید با این‌ها اشتباه گرفته شود. فهرست کامل را در درس بعد، «فایل‌های ویژه‌ی App Router»، می‌بینیم.
  • کد حساس سرور را در مسیر درست نگه دارید. قرار دادن یک فایل کنار صفحه آن را برای کاربر قابل دسترسی نمی‌کند، اما اگر یک Client Component آن را import کند، کدش به مرورگر می‌رود. محافظت از کد سرور را در درس «server-only، Context و اشتباهات رایج» می‌بینیم.

جمع‌بندی

  • در App Router فقط فایل‌های قراردادی مثل page.tsx و route.ts محتوای عمومی می‌سازند و بقیه‌ی فایل‌ها را می‌توان کنار صفحات گذاشت (Colocation).
  • پوشه‌ای که نامش با زیرخط شروع شود (_components) یک پوشه‌ی خصوصی است و کاملاً از مسیریابی خارج می‌شود.
  • Route Group برای دسته‌بندی مسیرها و Layoutها است و پوشه‌ی خصوصی برای نگه‌داری فایل‌های غیرمسیری؛ این دو یکی نیستند.
  • سه الگوی رایج ساختاردهی وجود دارد و ما در DevNotes کدهای مشترک را در components (و بعداً lib) و کدهای اختصاصی را در _components کنار صفحه نگه می‌داریم.
  • در این درس هدر و فوتر را به components منتقل کردیم و PostsIntro را در پوشه‌ی خصوصی ساختیم.
  • در درس بعد، «فایل‌های ویژه‌ی App Router»، با مهم‌ترین فایل‌های قراردادی مثل loading.tsx، error.tsx و not-found.tsx آشنا می‌شویم.