App Router: адрес — это папка
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
В части II адрес не значил ничего: страница была одна, а все экраны жили в ней. В Next.js маршруты не описывают в конфиге — их создают папками: app/about/page.tsx и есть адрес /about. Дерево папок — это карта сайта, и другого источника правды у неё нет.
Корень маршрутизации — папка app. Правило внутри неё одно и короткое: папка — это сегмент адреса, а файл page.tsx внутри — сама страница. Папка без page.tsx адреса не даёт: она либо участок пути к тому, что лежит глубже, либо просто склад для соседних файлов.
Песочница учебника маршруты Next.js не запускает: там нет ни сервера, ни файловой системы, ни самого Next. Компоненты страниц и раскладок она вызывает по-настоящему, а вот дерево папок приходится изображать руками — списком путей, из которого примеры считают адреса.
Проверим правило на списке файлов — соберём из путей адреса сами, тем же способом, каким это делает Next при старте: он смотрит на дерево и строит карту.
Шесть файлов — четыре адреса. post-card.tsx и lib/format.ts не стали ничем: зарезервировано имя page, остальные имена Next не трогает. Поэтому компонент, стили и тест кладут в ту же папку, что и страницу, — они не «протекут» наружу адресом.
| Файл | Адрес |
|---|---|
app/page.tsx | / |
app/about/page.tsx | /about |
app/blog/posts/page.tsx | /blog/posts |
app/blog/post-card.tsx | адреса нет — обычный файл рядом со страницей |
app/dashboard/ без page.tsx | адреса нет: по /dashboard придёт 404 |
Второго источника правды у адреса не появляется. Переименовали папку — поменялся адрес; перенесли — тоже. Обратный ход так же прост: увидели в браузере /blog/posts — открываете app/blog/posts/page.tsx, искать нечего.
app можно держать внутри src: тогда путь — src/app/about/page.tsx, а адрес остаётся /about. У самого учебника app лежит рядом с src: в app только маршруты, компоненты — в src.
До App Router у Next был роутер на папке pages, и правило там было другим: адрес давал сам файл — pages/about.tsx превращался в /about, а pages/index.tsx — в корень. Отсюда путаница в старых статьях: «страница — это файл» и «страница — это папка с файлом page» описывают разные роутеры.
Оба умеют жить в одном проекте, поэтому большие приложения переезжают по частям. Новый код пишут в app: раскладки, серверные компоненты и всё, что будет дальше в этой части, есть только там.
Внутри page.tsx — обычный компонент из первой главы этой части: функция, возвращающая разметку. Особенных обстоятельств ровно два: имя файла и экспорт по умолчанию.
В настоящем page.tsx перед функцией стоит export default. Песочница выполняет один файл без модулей, поэтому в примерах это слово опущено — во всём остальном код тот же.
Имя функции Next не смотрит: AboutPage, Page или Whatever — всё равно. А вот экспорт обязан быть умолчательным: фреймворк импортирует файл сам и договариваться об имени ему не с кем. Именованный экспорт вместо export default — ошибка первого дня: Next пожалуется, что в файле страницы нет компонента по умолчанию.
Расширение годится любое из .tsx, .ts, .jsx, .js — важно имя, и совпадать оно должно целиком. page.module.css, page.test.tsx и page-header.tsx адреса не создают.
Компонент страницы получает пропсы от Next, а не от вас: кусок адреса и разобранную строку запроса. Пока достаточно знать, что они называются params и searchParams и приезжают промисами; статья про динамические сегменты разбирает и то, и другое.
Шапка, меню и подвал на каждой странице — старая задача: в части II их вписывали в разметку руками и следили, чтобы перерисовка списка их не снесла. В Next для этого есть layout.tsx — тоже обычный компонент, но с обязательным пропом children.
Порядок в выводе не случаен: раскладка вызывается первой, страница — внутри неё. При этом раскладка о странице не знает ничего: в children приезжает готовый элемент, как в любую вашу обёртку из главы про пропсы. Разница одна — пару <RootLayout> и <AboutPage /> здесь написали вы, а в Next её собирает фреймворк из двух файлов, лежащих в разных папках.
Тип children — React.ReactNode: внутрь может приехать что угодно, от одного элемента до списка. Объявить проп и не отрисовать его компилятор не мешает, и это стоит увидеть.
В выводе одна строка — от раскладки, а страница промолчала; в кадре — шапка и подвал без содержимого. Ошибки нет ни у компилятора, ни в браузере: элемент <AboutPage /> создали, но в разметку не вернули, и React не стал вызывать её функцию — вторую строку печатать было некому. Если в Next вместо страницы показан общий каркас, ищите пропавший {children}.
Корневая раскладка app/layout.tsx обязательна, и она единственная, где встречаются <html> и <body>: в Next нет файла-шаблона, эти теги возвращает компонент.
Этот файл дан листингом, без кнопки запуска: <html> внутрь страницы не смонтировать, а маршрутов в песочнице нет вовсе. Что он делает, видно только в работающем Next — там каждая страница сайта приезжает внутри этих <html> и <body>, и подделывать это запуском мы не станем.
Раскладка лежит не только в корне: app/dashboard/layout.tsx обернёт всё, что ниже /dashboard. Уровень раскладки задаётся папкой — ровно как адрес. Про вложенные раскладки, группы и шаблоны — следующая статья.
Для каждого адреса Next собирает цепочку файлов: от корневой раскладки вниз до страницы. Правило то же — идём по сегментам и берём то, что нашли.
Обратите внимание, чего в цепочке нет: у settings своей раскладки нет, и никто её не требует. Раскладку заводят там, где нужна общая обёртка, а не на каждом уровне.
| Адрес | Что соберёт Next |
|---|---|
/ | app/layout.tsx → app/page.tsx |
/dashboard | app/layout.tsx → app/dashboard/layout.tsx → app/dashboard/page.tsx |
/dashboard/settings | app/layout.tsx → app/dashboard/layout.tsx → app/dashboard/settings/page.tsx |
/dashboard/billing | ничего: страницы нет — 404 |
Цепочка — это вложенность: первый файл снаружи, страница внутри всех остальных. В компонентах она выглядит так.
Три вызова снаружи внутрь — тот же порядок, в каком файлы стояли в цепочке, и две рамки в кадре показывают то же самое. Разница только одна: в Next эту разметку никто не пишет руками — её форму задаёт дерево папок.
Общая часть цепочки при переходе не пересоздаётся. Между /dashboard и /dashboard/settings обе раскладки остаются смонтированными, меняется только различающийся хвост — то самое сохранение состояния, о котором была статья «Когда состояние сохраняется, а когда сбрасывается». Открытое меню и прокрутка внутри раскладки переживают переход; в части II ради этого приходилось руками запоминать и возвращать позицию.
Отсюда простое следствие для проектирования: то, что должно пережить переход, живёт в раскладке, а не на странице. Подробности — в статьях про вложенные раскладки и про переходы.
Зарезервированных имён немного, и почти каждое — тема отдельной статьи. Знать их стоит наперёд: файл с таким именем меняет поведение папки сразу, файл с любым другим именем — не меняет ничего.
| Имя файла | Что делает |
|---|---|
page.tsx | делает сегмент адресом; без него адреса нет |
layout.tsx | оборачивает страницу и всё, что ниже; в корне обязателен |
template.tsx | как раскладка, но пересоздаётся на каждом переходе (следующая статья) |
loading.tsx | что показать, пока сегмент грузится |
error.tsx | граница ошибок сегмента |
not-found.tsx | ответ на «такой страницы нет» |
global-error.tsx | запасная разметка на случай, когда упала сама корневая раскладка; бывает только в корне app |
route.ts | вместо страницы — обработчик запроса; с page.tsx в одном сегменте не уживается |
Файлы loading.tsx, error.tsx и not-found.tsx разбирает отдельная статья этой главы — там же видно, почему они лежат в папке сегмента, а не в одном месте на весь проект.
У имён папок тоже бывает смысл, и о нём стоит знать заранее — иначе случайные скобки в имени превращаются в загадку.
| Имя папки | Что значит |
|---|---|
about | обычный сегмент: /about |
[id] | динамический сегмент — подойдёт любое значение (статья про динамические сегменты) |
(marketing) | группа: в адрес не попадает, нужна ради общей раскладки (следующая статья) |
_drafts | приватная папка: ни она, ни то, что внутри, адресом не станет |
Всё остальное — ваши файлы, и лежать они могут прямо рядом со страницей: компонент, модуль стилей, тест, маленькая функция для этой одной страницы. Адрес создаётся именем, а не расположением, поэтому в app безопасно селиться чему угодно — а искать код становится проще, когда он лежит там же, где страница, которая его использует.
- Корень маршрутизации — папка
app: каждая папка внутри неё добавляет сегмент адреса, а имя папки и есть этот сегмент. - Адрес появляется только там, где лежит
page.tsx; папка без него — участок пути, и по такому адресу приходит 404. page.tsx— обычный компонент с экспортом по умолчанию: важно имя файла, а не имя функции.layout.tsxоборачивает свой сегмент и всё, что ниже; страница приезжает вchildren, и без него она просто не отрисуется.- Для каждого адреса Next собирает цепочку сверху вниз — корневая раскладка, раскладки сегментов, страница; цепочка и есть вложенность компонентов.
- Зарезервированы только особые имена (
page,layout,loading,error,route…) — остальные файлы спокойно живут рядом со страницей.
1/4В проекте есть файл app/dashboard/settings/page.tsx. По какому адресу откроется эта страница?
2/4В папке app/blog лежит только post-card.tsx — файла page.tsx там нет, но в app/blog/posts/page.tsx он есть. Что будет по адресу /blog?
3/4Раскладка объявила проп children, но в возвращаемой разметке его не использовала. Что произойдёт?
4/4Есть app/layout.tsx и app/dashboard/layout.tsx, а собственной раскладки у app/dashboard/settings нет. Что Next соберёт для адреса /dashboard/settings?
Карта сайта из дерева файлов
Напишите routes(list): из списка путей она должна вернуть адреса, которые действительно откроются. Учтите два правила — адрес даёт только файл page.tsx, а папка, чьё имя начинается с подчёркивания, выпадает из маршрутизации вместе со всем содержимым.
Два условия — два фильтра, а превращение пути в адрес то же, что в разделе про дерево папок.
Проверка на подчёркивание идёт по всем частям пути, а не только по последней папке: приватной может оказаться любая папка выше, и всё, что под ней, тоже теряет адрес.
Раскладка потеряла страницу
В кадре видны шапка и подвал, а корзины нет, и в выводе только строка раскладки. Почините раскладку так, чтобы страница появилась между шапкой и подвалом.
Проп children объявлен, но в разметку не попал — значит, элемент страницы никто не вернул. Достаточно отрисовать его там, где ему место.
После правки к строке раскладки добавится вторая — «страница отрисовалась»: React вызывает функцию компонента только тогда, когда его элемент оказался в отрисованном дереве.
Цепочка или 404
Функция chain честно собирает раскладки, но про страницу не спрашивает: для /dashboard она возвращает цепочку, хотя app/dashboard/page.tsx в наборе нет. Сделайте так, чтобы в этом случае возвращался null.
Раскладки собираются как раньше, но перед последним шагом появляется проверка: нет page.tsx — нет и адреса.
Это и есть поведение Next: сегмент без страницы существует в дереве файлов, но в браузере отвечает 404 — раскладки над ним ничего не меняют.