Файловая маршрутизация, page.js и layout.js

Главная механика App Router: структура папок = структура адресов сайта, плюс общий каркас через layout.

Файловая маршрутизация — принцип, при котором каждый подкаталог внутри app/ становится сегментом URL, файл page.tsx делает сегмент видимой страницей, а layout.tsx оборачивает страницы общим каркасом.

Папка = URL

Не нужно настраивать роутер вручную. Достаточно создать папку и положить в неё page.tsx:

app/
├─ page.tsx            → /
├─ about/
│  └─ page.tsx         → /about
└─ blog/
   └─ page.tsx         → /blog

Папка без page.tsx не создаёт доступный маршрут — она лишь группирует другие сегменты. Можно держать рядом вспомогательные файлы, не боясь «случайных» URL.

Минимальная страница

Страница — это просто компонент с экспортом по умолчанию. Никаких импортов роутера:

// app/about/page.tsx
export default function AboutPage() {
  return (
    <main>
      <h1>О компании</h1>
      <p>Мы делаем учебные курсы.</p>
    </main>
  );
}

Сохранили файл — адрес /about сразу работает. Если же создать папку app/contacts/ и забыть page.tsx, адрес /contacts вернёт 404: специальное имя именно page, а компонент с другим именем маршрутом не становится.

Корневой layout

Файл app/layout.tsx обязателен — он содержит теги html и body и оборачивает всё приложение. Дочерние страницы приходят в проп children:

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="ru">
      <body>
        <header>Мой сайт</header>
        {children}
        <footer>© 2024</footer>
      </body>
    </html>
  );
}

Вложенные layout

Layout можно положить на любом уровне. Тогда он применится только к страницам своего сегмента и обернётся внутрь родительского:

app/
├─ layout.tsx          ← общий для всего сайта
├─ page.tsx            → /
└─ dashboard/
   ├─ layout.tsx       ← общий только для /dashboard/*
   ├─ page.tsx         → /dashboard
   └─ settings/
      └─ page.tsx      → /dashboard/settings

Страница /dashboard/settings окажется обёрнута сразу двумя layout: сначала корневым, затем dashboard'овским. Важная деталь: при переходе между /dashboard и /dashboard/settings меняется только children, а сам layout остаётся смонтированным — поэтому состояние меню и позиция скролла не сбрасываются.

Логика маршрута и вложенности

Адрес собирается из имён папок, а layout вкладываются стеком. Смоделируем обе идеи на чистом JS:

function buildRoute(folders) {
  const path = "/" + folders.filter(Boolean).join("/");
  return path === "/" ? "/" : path;
}
function wrap(layers, page) {
  return layers.reduceRight((inner, layer) => layer + "(" + inner + ")", page);
}

console.log(buildRoute(["blog", "post-1"]));
console.log(wrap(["RootLayout", "DashboardLayout"], "SettingsPage"));

Вывод:

/blog/post-1
RootLayout(DashboardLayout(SettingsPage))

Итог

  • Каждая папка в app/ — сегмент URL; page.tsx делает его видимой страницей.
  • Папка без page.tsx только группирует и сама по себе даёт 404.
  • Корневой layout.tsx обязателен; вложенные layout оборачивают свой сегмент и сохраняются между переходами через проп children.
Проверьте себя
1. Какой адрес соответствует файлу app/blog/page.tsx?
A/app/blog
B/blog
C/blog/page
D/page/blog
2. Что произойдёт, если в папке app/contacts/ нет файла page.tsx?
ANext.js покажет пустую страницу
BАдрес /contacts вернёт 404 — маршрут не определён
CБудет использован layout вместо страницы
DПроизойдёт ошибка сборки
3. Через какой проп layout получает содержимое вложенной страницы?
Acontent
Bchildren
Cpage
Dslot
4. Что происходит с layout при навигации внутри его сегмента?
AОн полностью перерисовывается каждый раз
BОн сохраняется, меняется только содержимое (children)
CОн удаляется и создаётся заново
DОн превращается в страницу
Поддержать проект