Доки по разработке
This project is maintained by teniryte
В этом файле — базовый «конспект по умолчанию» по структуре проектов на Next.js с App Router, опирающийся на ваш код в src/app.
appПринцип: в режиме App Router всё приложение строится вокруг папки app.
Каждая подпапка в app — это маршрут, а специальные файлы в этих папках определяют поведение страницы.
page.tsx: «точка входа» для маршрута (рендерит контент страницы).layout.tsx: общий макет (layout) для всех дочерних маршрутов.loading.tsx: состояние загрузки для маршрута (fallback).error.tsx: обработка ошибок на уровне маршрута.not-found.tsx: страница 404 для конкретной ветки маршрутов.В вашем проекте есть:
src/app/layout.tsx — корневой layout.src/app/page.tsx — главная страница /.13-forms, 14-isr, 18-lazy-loading, 20-mdx и т.д. — отдельные учебные модули/маршруты.Минимальный пример маршрута (аналог вашего src/app/page.tsx):
'use client';
export default function HomePage() {
return <div>HOME</div>;
}
Здесь используется директива 'use client', поэтому компонент рендерится как клиентский (о различиях — ниже).
Файл src/app/layout.tsx определяет HTML-каркас всего приложения:
<html> и <body>,metadata (SEO‑метаданные по умолчанию).Ключевые идеи, которые важно запомнить:
RootLayout: он живёт в app/layout.tsx и оборачивает всё приложение.layout.tsx, они оборачивают только свою ветку маршрутов.page.tsx.Упрощённая структура root layout:
import type { Metadata } from 'next';
import { SomeFont } from 'next/font/google';
import '@/styles/globals.scss';
const someFont = SomeFont({
variable: '--font-some-font',
subsets: ['latin'],
});
export const metadata: Metadata = {
title: 'Название приложения',
description: 'Описание приложения',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={someFont.variable}>
{/* Здесь можно подключить любые глобальные компоненты */}
{children}
</body>
</html>
);
}
Next.js по умолчанию делает компоненты серверными.
Чтобы компонент стал клиентским, в начале файла нужно написать директиву:
'use client';
Серверные компоненты:
Prisma, секреты и т.п.;window, document);useState, useEffect.Клиентские компоненты:
useState), эффекты (useEffect), браузерные API;form action={...}.Типичный пример клиентского компонента:
'use client';
import { useState, useEffect } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Count changed', count);
}, [count]);
return (
<button onClick={() => setCount((c) => c + 1)}>
Count: {count}
</button>
);
}
Типичный пример серверного компонента:
// файл БЕЗ 'use client'
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default async function UsersPage() {
const users = await prisma.user.findMany();
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.email}</li>
))}
</ul>
);
}
Принцип: путь к файлу page.tsx внутри app напрямую определяет URL.
app/page.tsx → /app/13-forms/page.tsx → /13-formsapp/13-forms/01-create-user-useActionState/page.tsx → /13-forms/01-create-user-useActionStateapp/14-isr/byslug/[slug]/page.tsx → /14-isr/byslug/:slugДинамические сегменты:
[slug] создаёт маршрут с параметром slug.params.Пример:
// app/blog/[slug]/page.tsx
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
// Например, загрузка поста по slug
const post = await fetch(`https://example.com/posts/${slug}`).then((res) =>
res.json(),
);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
Вариант с синхронными params (чаще используемый в актуальных примерах):
export default async function BlogPostPage({
params,
}: {
params: { slug: string };
}) {
const { slug } = params;
// ...
}
Каждая страница/маршрут может определять свои метаданные через экспорт metadata или функцию generateMetadata.
Статические метаданные:
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Моя страница',
description: 'Описание страницы',
};
export default function Page() {
return <div>Контент</div>;
}
Динамические метаданные (пример — ISR-модуль с использованием параметра slug):
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return {
title: `Пост ${slug}`,
};
}
Серверные компоненты — идеальное место для:
Prisma, ORM, SQL и т.д.);fetch к сторонним сервисам);Пример: загрузка данных и передача их в дочерний компонент:
// Серверный компонент
import { PostsList } from './PostsList'; // допустим, это клиентский компонент
export default async function PostsPage() {
const posts = await fetch('https://example.com/posts').then((res) =>
res.json(),
);
return (
<section>
<h1>Posts</h1>
<PostsList posts={posts} />
</section>
);
}
// Клиентский компонент
'use client';
export function PostsList({ posts }: { posts: any[] }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
В RootLayout удобно подключать:
globals.scss);next/font.Паттерн подключения глобальных стилей:
// layout.tsx
import '@/styles/globals.scss';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Паттерн подключения шрифтов:
import { Roboto } from 'next/font/google';
const roboto = Roboto({
subsets: ['latin'],
variable: '--font-roboto',
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={roboto.variable}>{children}</body>
</html>
);
}
Подводя итог базовой структуре приложения:
13-forms/, 14-isr/, 18-lazy-loading/ и т.п.)._components или features:
app/.../_components/..., src/features/...) хорошо масштабируется.Эти принципы — фундамент для следующих глав: формы и server actions, ISR/данные, lazy loading, MDX, SEO и т.д.