Маршрутизация
Введение
В Boilerplate используется система маршрутизации Next.js App Router, которая основана на файловой системе. Это означает, что структура папок и файлов в директории app
напрямую определяет URL-маршруты вашего приложения.
Эта документация поможет вам понять, как работает маршрутизация в проекте, и как добавлять новые страницы и маршруты.
Основы файловой системы маршрутизации
Структура папок
В App Router каждая папка представляет собой сегмент URL-пути, а файл page.tsx
внутри папки определяет содержимое страницы. Например:
app/
├── page.tsx // Главная страница сайта (/)
├── about/
│ └── page.tsx // Страница "О нас" (/about)
└── blog/
└── page.tsx // Страница блога (/blog)
Компоненты страниц
Файл page.tsx
должен экспортировать React-компонент по умолчанию:
// app/about/page.tsx
export default function AboutPage() {
return (
<div>
<h1>О нас</h1>
<p>Это страница с информацией о нашем проекте.</p>
</div>
);
}
Вложенные маршруты
Вы можете создавать вложенные маршруты, добавляя дополнительные уровни папок:
app/
├── dashboard/
│ ├── page.tsx // Главная страница дашборда (/dashboard)
│ ├── settings/
│ │ └── page.tsx // Настройки дашборда (/dashboard/settings)
│ └── analytics/
│ └── page.tsx // Аналитика (/dashboard/analytics)
Группировка маршрутов
В Next.js App Router вы можете группировать маршруты с помощью круглых скобок, не влияя на фактический URL. Это полезно для организационных целей или для разделения разных частей приложения.
app/
├── (landing)/ // Группа "landing" не влияет на URL
│ ├── page.tsx // Доступно по URL: /
│ └── about/
│ └── page.tsx // Доступно по URL: /about
└── (dashboard)/ // Группа "dashboard" не влияет на URL
└── admin/
└── page.tsx // Доступно по URL: /admin
В нашем проекте используется группировка для разделения разных частей сайта, например, лендинга и дашборда, которые имеют разные макеты и структуру.
app/
├── (landing)/ // Секция лендинга
│ ├── page.tsx // Главная страница
│ ├── _components/ // Компоненты лендинга
│ └── layout.tsx // Макет для лендинга
└── dashboard/ // Секция дашборда
├── page.tsx
├── _components/
└── layout.tsx // Отдельный макет для дашборда
Динамические маршруты
Для создания динамических маршрутов, которые принимают параметры из URL, используются папки с именами в квадратных скобках:
app/
├── blog/
│ ├── page.tsx // Список статей блога (/blog)
│ └── [slug]/
│ └── page.tsx // Страница отдельной статьи (/blog/article-name)
Пример компонента для динамического маршрута:
// app/blog/[slug]/page.tsx
interface BlogPostPageProps {
params: {
slug: string; // Параметр из URL
};
}
export default function BlogPostPage({ params }: BlogPostPageProps) {
const { slug } = params;
return (
<div>
<h1>Статья: {slug}</h1>
{/* Содержимое статьи */}
</div>
);
}
Вложенные динамические маршруты
Вы можете создавать вложенные динамические маршруты, комбинируя несколько параметров:
app/
├── shop/
│ ├── page.tsx // Страница магазина (/shop)
│ └── [category]/
│ ├── page.tsx // Категория товаров (/shop/electronics)
│ └── [product]/
│ └── page.tsx // Страница товара (/shop/electronics/smartphone)
Catch-all маршруты
Для маршрутов, которые принимают множество сегментов пути, используются троеточия перед именем параметра:
app/
├── docs/
│ └── [...slug]/
│ └── page.tsx // Будет соответствовать /docs/a, /docs/a/b, /docs/a/b/c и т.д.
Пример компонента для catch-all маршрута:
// app/docs/[...slug]/page.tsx
interface DocsPageProps {
params: {
slug: string[]; // Массив сегментов пути
};
}
export default function DocsPage({ params }: DocsPageProps) {
const { slug } = params; // например, ['getting-started', 'installation']
return (
<div>
<h1>Документация: {slug.join(' > ')}</h1>
{/* Содержимое документации */}
</div>
);
}
Макеты и шаблоны
Файлы layout.tsx
Файл layout.tsx
определяет общий макет для группы страниц. Макеты вложенных маршрутов будут вкладываться в родительские макеты.
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ru">
<body>
<header>Общий заголовок сайта</header>
<main>{children}</main>
<footer>Общий футер сайта</footer>
</body>
</html>
);
}
В нашем проекте есть отдельные макеты для разных разделов сайта:
app/
├── layout.tsx // Корневой макет для всего сайта
├── (landing)/
│ └── layout.tsx // Макет для лендинга
└── dashboard/
└── layout.tsx // Макет для дашборда
Файлы loading.tsx и error.tsx
Next.js поддерживает специальные файлы для обработки состояний загрузки и ошибок:
app/
├── dashboard/
│ ├── page.tsx
│ ├── loading.tsx // Показывается во время загрузки
│ └── error.tsx // Показывается при ошибках
Пример компонента загрузки:
// app/dashboard/loading.tsx
export default function DashboardLoading() {
return (
<div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
</div>
);
}
Пример компонента обработки ошибок:
// app/dashboard/error.tsx
"use client"; // Обязательно для error.tsx
import { useEffect } from "react";
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Лог ошибки на сервер
console.error(error);
}, [error]);
return (
<div className="text-center py-10">
<h2 className="text-xl font-semibold mb-4">Что-то пошло не так!</h2>
<button
onClick={reset}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
Попробовать снова
</button>
</div>
);
}
Файл not-found.tsx
Для обработки страниц "404 Not Found" используется файлnot-found.tsx
:
// app/not-found.tsx
import Link from "next/link";
export default function NotFound() {
return (
<div className="container py-20 text-center">
<h1 className="text-5xl font-bold mb-6">404</h1>
<h2 className="text-2xl font-semibold mb-3">Страница не найдена</h2>
<p className="text-gray-400 mb-8">
Запрашиваемая страница не существует или была перемещена.
</p>
<Link
href="/"
className="bg-blue-600 hover:bg-blue-700 text-white font-medium px-5 py-2 rounded-lg"
>
Вернуться на главную
</Link>
</div>
);
}
Навигация между страницами
Компонент Link
Для навигации между страницами рекомендуется использовать компонентLink
изnext/link
:
import Link from "next/link";
export default function Navigation() {
return (
<nav>
<ul>
<li>
<Link href="/">Главная</Link>
</li>
<li>
<Link href="/blog">Блог</Link>
</li>
<li>
<Link href="/dashboard">Личный кабинет</Link>
</li>
<li>
<Link href="/blog/my-article">Статья блога</Link>
</li>
<li>
<Link
href={{
pathname: "/blog/[slug]",
query: { slug: "my-article" },
}}
>
Статья блога (объект)
</Link>
</li>
</ul>
</nav>
);
}
Программная навигация
Для программной навигации используется хукuseRouter
изnext/navigation
:
"use client";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
export default function LoginForm() {
const router = useRouter();
const handleSuccess = () => {
// Перенаправление после успешного входа
router.push("/dashboard");
};
return (
<form onSubmit={handleSuccess}>
{/* Форма входа */}
<Button type="submit">Войти</Button>
{/* Кнопка возврата назад */}
<Button type="button" onClick={() => router.back()}>
Назад
</Button>
{/* Обновление текущей страницы */}
<Button type="button" onClick={() => router.refresh()}>
Обновить
</Button>
</form>
);
}
Функция redirect
Для перенаправления на серверной стороне используется функцияredirect
:
import { redirect } from "next/navigation";
import { getCookie } from "@/lib/cookie-utils";
export default function DashboardPage() {
const isAuthenticated = getCookie("access_token");
if (!isAuthenticated) {
redirect("/login"); // Перенаправление, если пользователь не авторизован
}
return (
<div>
<h1>Панель управления</h1>
{/* Содержимое дашборда */}
</div>
);
}
Функция notFound
Для отображения страницы 404 используется функцияnotFound
:
import { notFound } from "next/navigation";
import { getPostBySlug } from "@/lib/blog";
interface BlogPostPageProps {
params: {
slug: string;
};
}
export default function BlogPostPage({ params }: BlogPostPageProps) {
const post = getPostBySlug(params.slug);
if (!post) {
notFound(); // Показывает страницу 404
}
return (
<div>
<h1>{post.title}</h1>
{/* Содержимое поста */}
</div>
);
}
API маршруты
В Next.js App Router, API маршруты создаются внутри директорииapp/api
:
app/
├── api/
│ ├── hello/
│ │ └── route.ts // Доступно по URL: /api/hello
│ └── users/
│ ├── route.ts // Доступно по URL: /api/users
│ └── [id]/
│ └── route.ts // Доступно по URL: /api/users/123
Пример простого API маршрута:
// app/api/hello/route.ts
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ message: "Hello World!" });
}
export async function POST(request: Request) {
const body = await request.json();
return NextResponse.json({
message: "Данные получены",
data: body
});
}
Пример API маршрута с параметрами
// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
interface Params {
params: {
id: string;
};
}
export async function GET(request: NextRequest, { params }: Params) {
const userId = params.id;
// Получение данных пользователя по ID
const user = await fetchUserById(userId);
if (!user) {
return NextResponse.json(
{ error: "Пользователь не найден" },
{ status: 404 }
);
}
return NextResponse.json(user);
}
Обработка HTTP методов
В API маршрутах можно обрабатывать различные HTTP методы, экспортируя соответствующие функции:
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
// Получение списка пользователей
export async function GET() {
const users = await fetchUsers();
return NextResponse.json(users);
}
// Создание нового пользователя
export async function POST(request: NextRequest) {
const userData = await request.json();
const newUser = await createUser(userData);
return NextResponse.json(newUser, { status: 201 });
}
// Обновление пользователя
export async function PUT(request: NextRequest) {
const userData = await request.json();
const updatedUser = await updateUser(userData);
return NextResponse.json(updatedUser);
}
// Удаление пользователя
export async function DELETE(request: NextRequest) {
const { id } = await request.json();
await deleteUser(id);
return NextResponse.json({ success: true });
}
Генерация метаданных
Next.js App Router позволяет генерировать метаданные для страниц с помощью объекта metadata
или функции generateMetadata
:
// app/blog/page.tsx
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Блог | Мой сайт",
description: "Статьи о веб-разработке и технологиях",
openGraph: {
title: "Блог | Мой сайт",
description: "Статьи о веб-разработке и технологиях",
url: "https://mysite.com/blog",
siteName: "Мой сайт",
locale: "ru_RU",
type: "website",
},
};
export default function BlogPage() {
return (
<div>
<h1>Блог</h1>
{/* Список статей */}
</div>
);
}
Для динамических страниц используется функцияgenerateMetadata
:
// app/blog/[slug]/page.tsx
import { Metadata } from "next";
import { getPostBySlug } from "@/lib/blog";
interface BlogPostPageProps {
params: {
slug: string;
};
}
export async function generateMetadata({
params,
}: BlogPostPageProps): Promise<Metadata> {
const post = getPostBySlug(params.slug);
if (!post) {
return {
title: "Статья не найдена | Блог",
description: "Запрашиваемая статья не найдена",
};
}
return {
title: `${post.title} | Блог`,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
type: "article",
publishedTime: post.createdAt.toISOString(),
authors: ["Автор"],
images: post.image ? [post.image] : [],
},
};
}
export default function BlogPostPage({ params }: BlogPostPageProps) {
const post = getPostBySlug(params.slug);
if (!post) {
return null; // Обработка notFound() происходит в компоненте
}
return (
<article>
<h1>{post.title}</h1>
{/* Содержимое статьи */}
</article>
);
}
Статическая генерация путей
Next.js позволяет предварительно генерировать пути для динамических страниц с помощью функции generateStaticParams
:
// app/blog/[slug]/page.tsx
import { getAllPosts, getPostBySlug } from "@/lib/blog";
interface BlogPostPageProps {
params: {
slug: string;
};
}
export async function generateStaticParams() {
const posts = getAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
export default function BlogPostPage({ params }: BlogPostPageProps) {
const post = getPostBySlug(params.slug);
if (!post) {
return null;
}
return (
<article>
<h1>{post.title}</h1>
{/* Содержимое статьи */}
</article>
);
}
Функция generateStaticParams
также работает с вложенными динамическими маршрутами:
// app/shop/[category]/[product]/page.tsx
export async function generateStaticParams() {
// Получаем все категории и их продукты
const categories = await getCategories();
// Возвращаем массив с комбинациями параметров
return categories.flatMap(category =>
category.products.map(product => ({
category: category.slug,
product: product.slug,
}))
);
}