Файловая маршрутизация, 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.