Маршрутизация

Введение

В Boilerplate используется система маршрутизации Next.js App Router, которая основана на файловой системе. Это означает, что структура папок и файлов в директории app напрямую определяет URL-маршруты вашего приложения.

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

Основы файловой системы маршрутизации

Структура папок

В App Router каждая папка представляет собой сегмент URL-пути, а файл page.tsx внутри папки определяет содержимое страницы. Например:

text
app/
├── page.tsx              // Главная страница сайта (/)
├── about/
│   └── page.tsx          // Страница "О нас" (/about)
└── blog/
    └── page.tsx          // Страница блога (/blog)

Компоненты страниц

Файл page.tsx должен экспортировать React-компонент по умолчанию:

tsx
// app/about/page.tsx
export default function AboutPage() {
  return (
    <div>
      <h1>О нас</h1>
      <p>Это страница с информацией о нашем проекте.</p>
    </div>
  );
}

Вложенные маршруты

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

text
app/
├── dashboard/
│   ├── page.tsx          // Главная страница дашборда (/dashboard)
│   ├── settings/
│   │   └── page.tsx      // Настройки дашборда (/dashboard/settings)
│   └── analytics/
│       └── page.tsx      // Аналитика (/dashboard/analytics)

Группировка маршрутов

В Next.js App Router вы можете группировать маршруты с помощью круглых скобок, не влияя на фактический URL. Это полезно для организационных целей или для разделения разных частей приложения.

text
app/
├── (landing)/            // Группа "landing" не влияет на URL
│   ├── page.tsx          // Доступно по URL: /
│   └── about/
│       └── page.tsx      // Доступно по URL: /about
└── (dashboard)/          // Группа "dashboard" не влияет на URL
    └── admin/
        └── page.tsx      // Доступно по URL: /admin

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

text
app/
├── (landing)/            // Секция лендинга
│   ├── page.tsx          // Главная страница
│   ├── _components/      // Компоненты лендинга
│   └── layout.tsx        // Макет для лендинга
└── dashboard/            // Секция дашборда
    ├── page.tsx
    ├── _components/
    └── layout.tsx        // Отдельный макет для дашборда

Динамические маршруты

Для создания динамических маршрутов, которые принимают параметры из URL, используются папки с именами в квадратных скобках:

text
app/
├── blog/
│   ├── page.tsx           // Список статей блога (/blog)
│   └── [slug]/
│       └── page.tsx       // Страница отдельной статьи (/blog/article-name)

Пример компонента для динамического маршрута:

tsx
// 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>
  );
}

Вложенные динамические маршруты

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

text
app/
├── shop/
│   ├── page.tsx           // Страница магазина (/shop)
│   └── [category]/
│       ├── page.tsx       // Категория товаров (/shop/electronics)
│       └── [product]/
│           └── page.tsx   // Страница товара (/shop/electronics/smartphone)

Catch-all маршруты

Для маршрутов, которые принимают множество сегментов пути, используются троеточия перед именем параметра:

text
app/
├── docs/
│   └── [...slug]/
│       └── page.tsx       // Будет соответствовать /docs/a, /docs/a/b, /docs/a/b/c и т.д.

Пример компонента для catch-all маршрута:

tsx
// 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 определяет общий макет для группы страниц. Макеты вложенных маршрутов будут вкладываться в родительские макеты.

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>
  );
}

В нашем проекте есть отдельные макеты для разных разделов сайта:

text
app/
├── layout.tsx            // Корневой макет для всего сайта
├── (landing)/
│   └── layout.tsx        // Макет для лендинга
└── dashboard/
    └── layout.tsx        // Макет для дашборда

Файлы loading.tsx и error.tsx

Next.js поддерживает специальные файлы для обработки состояний загрузки и ошибок:

text
app/
├── dashboard/
│   ├── page.tsx
│   ├── loading.tsx       // Показывается во время загрузки
│   └── error.tsx         // Показывается при ошибках

Пример компонента загрузки:

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>
  );
}

Пример компонента обработки ошибок:

tsx
// 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:

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:

tsx
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:

tsx
"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:

tsx
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:

tsx
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:

text
app/
├── api/
│   ├── hello/
│   │   └── route.ts      // Доступно по URL: /api/hello
│   └── users/
│       ├── route.ts      // Доступно по URL: /api/users
│       └── [id]/
│           └── route.ts  // Доступно по URL: /api/users/123

Пример простого API маршрута:

tsx
// 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 маршрута с параметрами

tsx
// 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 методы, экспортируя соответствующие функции:

tsx
// 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:

tsx
// 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:

tsx
// 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:

tsx
// 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также работает с вложенными динамическими маршрутами:

tsx
// 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,
    }))
  );
}