3. Файловая маршрутизация
🛣️ Файловая маршрутизация в Next.js
Заголовок раздела «🛣️ Файловая маршрутизация в Next.js»Забудь о конфигах роутов. В Next.js маршрут определяется структурой папок в директории app/. Это одна из самых элегантных идей фреймворка: код организован так же, как URL. 📁
Файловая структура → URL─────────────────────────────────────────────────app/page.tsx → /app/about/page.tsx → /aboutapp/blog/page.tsx → /blogapp/blog/[slug]/page.tsx → /blog/:slugapp/shop/[...path]/page.tsx → /shop/a/b/c/...📌 Статические сегменты
Заголовок раздела «📌 Статические сегменты»Самый простой случай — папка с именем сегмента:
app/├── page.tsx → /├── about/│ └── page.tsx → /about├── contact/│ └── page.tsx → /contact└── services/ ├── page.tsx → /services ├── design/ │ └── page.tsx → /services/design └── development/ └── page.tsx → /services/developmentexport default function AboutPage() { return ( <main> <h1>О нас</h1> <p>Мы команда разработчиков...</p> </main> );}
// Можно добавить метаданные прямо в файле!export const metadata = { title: 'О нас | Мой сайт', description: 'Узнайте о нашей команде',};🔀 Динамические сегменты [slug]
Заголовок раздела «🔀 Динамические сегменты [slug]»Квадратные скобки в имени папки = динамический параметр:
app/blog/[slug]/page.tsx → /blog/hello-world → /blog/next-js-guide → /blog/anything-here// params содержит { slug: 'hello-world' }
interface BlogPostProps { params: Promise<{ slug: string }>; // Next.js 15: Promise!}
export default async function BlogPost({ params }: BlogPostProps) { const { slug } = await params; // Ожидаем Promise
const post = await getPostBySlug(slug);
if (!post) { notFound(); // Показывает not-found.tsx }
return ( <article> <h1>{post.title}</h1> <p>Слаг: {slug}</p> <div dangerouslySetInnerHTML={{ __html: post.content }} /> </article> );}
// Генерация статических путей при сборкеexport async function generateStaticParams() { const posts = await getAllPosts();
return posts.map(post => ({ slug: post.slug, }));}Несколько динамических сегментов:
app/[lang]/[country]/page.tsx → /en/us, /ru/ru, /de/deinterface Props { params: Promise<{ lang: string; country: string }>;}
export default async function LocalePage({ params }: Props) { const { lang, country } = await params;
return <p>Язык: {lang}, Страна: {country}</p>;}📡 Catch-all сегменты [...slug]
Заголовок раздела «📡 Catch-all сегменты [...slug]»Три точки захватывают один или более сегментов пути:
app/docs/[...slug]/page.tsx → /docs/intro → /docs/api/users → /docs/api/users/create → НЕ /docs (нужен хотя бы один сегмент!)interface Props { params: Promise<{ slug: string[] }>; // Массив сегментов!}
export default async function DocsPage({ params }: Props) { const { slug } = await params; // /docs/api/users → slug = ['api', 'users'] // /docs/intro → slug = ['intro']
const path = slug.join('/'); // 'api/users' const doc = await getDoc(path);
return ( <div> <Breadcrumbs segments={slug} /> <article>{doc.content}</article> </div> );}
// Генерация всех возможных путейexport async function generateStaticParams() { const docs = await getAllDocs();
return docs.map(doc => ({ slug: doc.path.split('/'), // ['api', 'users'] }));}🎯 Опциональные catch-all [[...slug]]
Заголовок раздела «🎯 Опциональные catch-all [[...slug]]»Двойные скобки захватывают ноль или более сегментов:
app/[[...slug]]/page.tsx → / (slug = undefined!) → /blog (slug = ['blog']) → /blog/post (slug = ['blog', 'post'])interface Props { params: Promise<{ filters?: string[] }>; // Может быть undefined!}
export default async function ShopPage({ params }: Props) { const { filters } = await params;
// /shop → filters = undefined // /shop/clothing → filters = ['clothing'] // /shop/clothing/summer → filters = ['clothing', 'summer']
const category = filters?.[0]; const subcategory = filters?.[1];
const products = await getProducts({ category, subcategory });
return ( <div> <h1> {!filters ? 'Все товары' : filters.join(' → ')} </h1> <ProductGrid products={products} /> </div> );}🗂️ Группы роутов (group)
Заголовок раздела «🗂️ Группы роутов (group)»Папки в круглых скобках не влияют на URL — только организуют код:
app/├── (marketing)/ ← НЕ добавляет /marketing к URL│ ├── layout.tsx ← Layout только для маркетинговых страниц│ ├── page.tsx → /│ ├── about/│ │ └── page.tsx → /about│ └── pricing/│ └── page.tsx → /pricing│└── (dashboard)/ ← НЕ добавляет /dashboard к URL ├── layout.tsx ← Layout только для дашборда ├── analytics/ │ └── page.tsx → /analytics └── settings/ └── page.tsx → /settings// app/(marketing)/layout.tsx// Layout применяется только к marketing страницамexport default function MarketingLayout({ children }) { return ( <> <MarketingNav /> {/* Только для маркетинга */} {children} <MarketingFooter /> </> );}
// app/(dashboard)/layout.tsxexport default function DashboardLayout({ children }) { return ( <div className="dashboard"> <DashboardSidebar /> {children} </div> );}Несколько root layouts:
// app/(marketing)/layout.tsxexport default function MarketingLayout({ children }) { return ( <html lang="ru"> <body className="marketing-theme">{children}</body> </html> );}
// app/(dashboard)/layout.tsxexport default function DashboardLayout({ children }) { return ( <html lang="ru"> <body className="dashboard-theme">{children}</body> </html> );}// Каждый layout может иметь свой <html> и <body>!⚡ Параллельные роуты @slot
Заголовок раздела «⚡ Параллельные роуты @slot»Папки с символом @ создают именованные слоты для одновременного рендеринга нескольких страниц:
app/└── dashboard/ ├── layout.tsx ← Получает @analytics и @team ├── page.tsx → /dashboard ├── @analytics/ │ ├── page.tsx → Слот аналитики │ └── revenue/ │ └── page.tsx └── @team/ ├── page.tsx → Слот команды └── members/ └── page.tsxinterface DashboardLayoutProps { children: React.ReactNode; analytics: React.ReactNode; // Слот @analytics team: React.ReactNode; // Слот @team}
export default function DashboardLayout({ children, analytics, team,}: DashboardLayoutProps) { return ( <div className="dashboard-grid"> <div className="main">{children}</div> <div className="analytics">{analytics}</div> <div className="team">{team}</div> </div> );}// /dashboard → рендерит все три слота одновременно!🎭 Перехватывающие роуты (.), (..), (...)
Заголовок раздела «🎭 Перехватывающие роуты (.), (..), (...)»Перехватывают навигацию для показа контента в модальном окне (без потери контекста):
app/├── photos/│ ├── page.tsx → /photos (галерея)│ └── [id]/│ └── page.tsx → /photos/123 (полный экран)│└── @modal/ └── (.)photos/ └── [id]/ └── page.tsx → Перехватывает /photos/123 → показывает модалку!// app/photos/[id]/page.tsx — полная страницаexport default async function PhotoPage({ params }) { const { id } = await params; const photo = await getPhoto(id);
return ( <div> <img src={photo.url} alt={photo.title} /> <h1>{photo.title}</h1> <p>{photo.description}</p> </div> );}
// app/@modal/(.)photos/[id]/page.tsx — модальное окноexport default async function PhotoModal({ params }) { const { id } = await params; const photo = await getPhoto(id);
return ( <Modal> <img src={photo.url} alt={photo.title} /> </Modal> );}// При клике из /photos → показывает модалку// При прямом переходе → показывает полную страницуСинтаксис перехвата:
(.)— перехватить сегмент на том же уровне(..)— перехватить сегмент на уровень выше(..)(..)— на два уровня выше(...)— перехватить из корня app
🔗 Компонент Link
Заголовок раздела «🔗 Компонент Link»next/link — основной способ навигации в Next.js:
import Link from 'next/link';
// Базовое использование<Link href="/about">О нас</Link>
// Динамический роут<Link href={`/blog/${post.slug}`}> {post.title}</Link>
// С объектом href<Link href={{ pathname: '/blog/[slug]', query: { slug: post.slug } }}> {post.title}</Link>
// Prefetch (по умолчанию true для видимых ссылок)<Link href="/about" prefetch={false}> О нас (без prefetch)</Link>
// replace — не добавляет в history<Link href="/login" replace> Войти</Link>
// scroll — скролл в начало страницы (по умолчанию true)<Link href="/about" scroll={false}> О нас</Link>
// Программная навигацияimport { useRouter } from 'next/navigation';
function MyComponent() { const router = useRouter();
const handleClick = () => { router.push('/about'); // router.replace('/about'); // router.back(); // router.forward(); // router.refresh(); // обновить текущую страницу // router.prefetch('/about'); // предзагрузить };}📊 Таблица всех типов роутов
Заголовок раздела «📊 Таблица всех типов роутов»| Синтаксис | Пример | URL |
|---|---|---|
segment/ | app/about/ | /about |
[param]/ | app/blog/[slug]/ | /blog/:slug |
[...slug]/ | app/docs/[...slug]/ | /docs/a/b/c |
[[...slug]]/ | app/[[...all]]/ | / или /a/b |
(group)/ | app/(marketing)/ | без влияния на URL |
@slot/ | app/@modal/ | параллельный слот |
(.)segment/ | app/@modal/(.)photo/ | перехват |
🎯 Резюме урока
Заголовок раздела «🎯 Резюме урока»Файловая маршрутизация — это сердце Next.js App Router. Запомни:
- Папка = сегмент URL
[param]= динамический параметр[...slug]= catch-all (1+)[[...slug]]= optional catch-all (0+)(group)= организация без URL@slot= параллельный рендеринг(.)= перехват навигации