App Router: адрес — это папка

Next.js≈ 9 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

В части II адрес не значил ничего: страница была одна, а все экраны жили в ней. В Next.js маршруты не описывают в конфиге — их создают папками: app/about/page.tsx и есть адрес /about. Дерево папок — это карта сайта, и другого источника правды у неё нет.

Разделы темы:·

Корень маршрутизации — папка app. Правило внутри неё одно и короткое: папка — это сегмент адреса, а файл page.tsx внутри — сама страница. Папка без page.tsx адреса не даёт: она либо участок пути к тому, что лежит глубже, либо просто склад для соседних файлов.

Песочница учебника маршруты Next.js не запускает: там нет ни сервера, ни файловой системы, ни самого Next. Компоненты страниц и раскладок она вызывает по-настоящему, а вот дерево папок приходится изображать руками — списком путей, из которого примеры считают адреса.

Проверим правило на списке файлов — соберём из путей адреса сами, тем же способом, каким это делает Next при старте: он смотрит на дерево и строит карту.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Шесть файлов — четыре адреса. 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 — обычный компонент из первой главы этой части: функция, возвращающая разметку. Особенных обстоятельств ровно два: имя файла и экспорт по умолчанию.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

В настоящем 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.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Порядок в выводе не случаен: раскладка вызывается первой, страница — внутри неё. При этом раскладка о странице не знает ничего: в children приезжает готовый элемент, как в любую вашу обёртку из главы про пропсы. Разница одна — пару <RootLayout> и <AboutPage /> здесь написали вы, а в Next её собирает фреймворк из двух файлов, лежащих в разных папках.

Тип children — React.ReactNode: внутрь может приехать что угодно, от одного элемента до списка. Объявить проп и не отрисовать его компилятор не мешает, и это стоит увидеть.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

В выводе одна строка — от раскладки, а страница промолчала; в кадре — шапка и подвал без содержимого. Ошибки нет ни у компилятора, ни в браузере: элемент <AboutPage /> создали, но в разметку не вернули, и React не стал вызывать её функцию — вторую строку печатать было некому. Если в Next вместо страницы показан общий каркас, ищите пропавший {children}.

Корневая раскладка app/layout.tsx обязательна, и она единственная, где встречаются <html> и <body>: в Next нет файла-шаблона, эти теги возвращает компонент.

tsxлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Этот файл дан листингом, без кнопки запуска: <html> внутрь страницы не смонтировать, а маршрутов в песочнице нет вовсе. Что он делает, видно только в работающем Next — там каждая страница сайта приезжает внутри этих <html> и <body>, и подделывать это запуском мы не станем.

Раскладка лежит не только в корне: app/dashboard/layout.tsx обернёт всё, что ниже /dashboard. Уровень раскладки задаётся папкой — ровно как адрес. Про вложенные раскладки, группы и шаблоны — следующая статья.

Для каждого адреса Next собирает цепочку файлов: от корневой раскладки вниз до страницы. Правило то же — идём по сегментам и берём то, что нашли.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Обратите внимание, чего в цепочке нет: у settings своей раскладки нет, и никто её не требует. Раскладку заводят там, где нужна общая обёртка, а не на каждом уровне.

АдресЧто соберёт Next
/app/layout.tsx → app/page.tsx
/dashboardapp/layout.tsx → app/dashboard/layout.tsx → app/dashboard/page.tsx
/dashboard/settingsapp/layout.tsx → app/dashboard/layout.tsx → app/dashboard/settings/page.tsx
/dashboard/billingничего: страницы нет — 404

Цепочка — это вложенность: первый файл снаружи, страница внутри всех остальных. В компонентах она выглядит так.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Три вызова снаружи внутрь — тот же порядок, в каком файлы стояли в цепочке, и две рамки в кадре показывают то же самое. Разница только одна: в 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, а папка, чьё имя начинается с подчёркивания, выпадает из маршрутизации вместе со всем содержимым.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Два условия — два фильтра, а превращение пути в адрес то же, что в разделе про дерево папок.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Проверка на подчёркивание идёт по всем частям пути, а не только по последней папке: приватной может оказаться любая папка выше, и всё, что под ней, тоже теряет адрес.

Раскладка потеряла страницу

В кадре видны шапка и подвал, а корзины нет, и в выводе только строка раскладки. Почините раскладку так, чтобы страница появилась между шапкой и подвалом.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Проп children объявлен, но в разметку не попал — значит, элемент страницы никто не вернул. Достаточно отрисовать его там, где ему место.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

После правки к строке раскладки добавится вторая — «страница отрисовалась»: React вызывает функцию компонента только тогда, когда его элемент оказался в отрисованном дереве.

Цепочка или 404

Функция chain честно собирает раскладки, но про страницу не спрашивает: для /dashboard она возвращает цепочку, хотя app/dashboard/page.tsx в наборе нет. Сделайте так, чтобы в этом случае возвращался null.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Раскладки собираются как раньше, но перед последним шагом появляется проверка: нет page.tsx — нет и адреса.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Это и есть поведение Next: сегмент без страницы существует в дереве файлов, но в браузере отвечает 404 — раскладки над ним ничего не меняют.