Практика: типичный роутинг
Собрали структуру роутинга для реального проекта.
Структура
app/
├── layout.tsx
├── page.tsx → /
├── globals.css
├── not-found.tsx
│
├── (auth)/
│ ├── layout.tsx ← без шапки
│ ├── login/page.tsx → /login
│ └── register/page.tsx → /register
│
├── (main)/
│ ├── layout.tsx ← с шапкой
│ ├── about/page.tsx → /about
│ ├── pricing/page.tsx → /pricing
│ └── contact/page.tsx → /contact
│
├── (app)/
│ ├── layout.tsx ← с сайдбаром, защита
│ ├── dashboard/
│ │ ├── page.tsx → /dashboard
│ │ └── loading.tsx
│ ├── posts/
│ │ ├── page.tsx → /posts
│ │ ├── loading.tsx
│ │ ├── new/page.tsx → /posts/new
│ │ └── [id]/
│ │ ├── page.tsx → /posts/1
│ │ ├── loading.tsx
│ │ ├── not-found.tsx
│ │ └── edit/page.tsx → /posts/1/edit
│ └── settings/
│ └── page.tsx → /settings
│
├── blog/
│ ├── page.tsx → /blog
│ ├── [slug]/
│ │ └── page.tsx → /blog/my-post
│ └── category/
│ └── [category]/
│ └── page.tsx → /blog/category/tech
│
└── api/
├── posts/route.ts → /api/posts
└── posts/[id]/route.ts → /api/posts/1
Root layout
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html lang="ru">
<body>{children}</body>
</html>
);
}
Layout аутентификации
// app/(auth)/layout.tsx
export default function AuthLayout({ children }) {
return (
<div className="auth-layout">
<div className="auth-card">{children}</div>
</div>
);
}
Layout приложения
// app/(app)/layout.tsx
import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';
export default async function AppLayout({ children }) {
const session = await auth();
if (!session) redirect('/login');
return (
<div className="app-layout">
<Sidebar user={session.user} />
<main>{children}</main>
</div>
);
}
Список постов
// app/(app)/posts/page.tsx
export default async function Posts({ searchParams }) {
const { page = '1', search = '' } = await searchParams;
const { data: posts, meta } = await getPosts({
page: Number(page),
search,
});
return (
<>
<PostFilters search={search} />
<PostList posts={posts} />
<Pagination meta={meta} />
</>
);
}
Детальная страница
// app/(app)/posts/[id]/page.tsx
import { notFound } from 'next/navigation';
export async function generateMetadata({ params }) {
const { id } = await params;
const post = await getPost(id);
return post ? { title: post.title } : {};
}
export default async function Post({ params }) {
const { id } = await params;
const post = await getPost(id);
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
<Link href={`/posts/${id}/edit`}>Редактировать</Link>
</article>
);
}
Блог со slug
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((p) => ({ slug: p.slug }));
}
export default async function Post({ params }) {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) notFound();
return <article>{post.content}</article>;
}
Категории блога
app/blog/category/[category]/page.tsx
/blog/category/tech/blog/category/life
API-роут
// app/api/posts/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get('page') ?? 1);
const posts = await db.post.findMany({
skip: (page - 1) * 10,
take: 10,
});
return Response.json(posts);
}
export async function POST(request: Request) {
const body = await request.json();
const post = await db.post.create({ data: body });
return Response.json(post, { status: 201 });
}
Middleware
// middleware.ts
import { NextResponse } from 'next/server';
export function middleware(request) {
const token = request.cookies.get('token')?.value;
if (request.nextUrl.pathname.startsWith('/dashboard') && !token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/posts/new'],
};
Итоги
- Группы для разных layouts
(auth)без шапки,(app)с сайдбаром- Динамические
[id]и[slug] generateStaticParamsдля блога- API-роуты в
app/api/ - Middleware для защиты