Пошук уроків, статей та іншого контенту
Створюйте сторінки під час збірки та визначайте межі статичної генерації в App Router.
Статична генерація — це створення HTML сторінки під час виконання next build, а не під час кожного запиту користувача.
Переваги:
сторінка швидко віддається з кешу або CDN;
серверу не потрібно виконувати код сторінки для кожного запиту;
помилки отримання даних можна виявити під час збірки;
сторінка добре підходить для SEO.
В App Router статична генерація є стандартним варіантом для маршрутів, які не використовують динамічні дані запиту, наприклад cookies або HTTP-заголовки.
Розглянемо сторінку товару:
app/
└── products/
└── [id]/
└── page.tsxФайл page.tsx може отримувати дані з локального модуля:
// app/products/[id]/page.tsx
import { products } from "@/lib/products";
type ProductPageProps = {
params: Promise<{
id: string;
}>;
};
export default async function ProductPage({
params,
}: ProductPageProps) {
const { id } = await params;
const product = products.find((item) => item.id === id);
if (!product) {
return <h1>Товар не знайдено</h1>;
}
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<strong>{product.price} грн</strong>
</main>
);
}Дані:
// lib/products.ts
export const products = [
{
id: "keyboard",
name: "Механічна клавіатура",
description: "Клавіатура з механічними перемикачами.",
price: 3200,
},
{
id: "mouse",
name: "Бездротова миша",
description: "Миша з точним сенсором і тихими кнопками.",
price: 1800,
},
];Такий маршрут може бути статичним, але Next.js ще потрібно повідомити, які значення id потрібно створити під час збірки.
generateStaticParamsДля статичної генерації динамічних маршрутів використовується функція generateStaticParams.
// app/products/[id]/page.tsx
import { products } from "@/lib/products";
type ProductPageProps = {
params: Promise<{
id: string;
}>;
};
export function generateStaticParams() {
return products.map((product) => ({
id: product.id,
}));
}
export default async function ProductPage({
params,
}: ProductPageProps) {
const { id } = await params;
const product = products.find((item) => item.id === id);
if (!product) {
return <h1>Товар не знайдено</h1>;
}
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<strong>{product.price} грн</strong>
</main>
);
}Під час виконання next build Next.js викличе generateStaticParams і створить сторінки:
/products/keyboard
/products/mouseФактично в результаті збірки будуть підготовлені дві окремі сторінки.
generateStaticParamsФункція повинна повертати масив об’єктів. Назви властивостей мають відповідати динамічним сегментам маршруту.
Для маршруту:
app/blog/[slug]/page.tsxпотрібно повернути об’єкти з властивістю slug:
export function generateStaticParams() {
return [
{ slug: "nextjs-basics" },
{ slug: "app-router" },
];
}Для вкладених сегментів:
app/shop/[category]/[productId]/page.tsxпотрібно повернути обидва параметри:
export function generateStaticParams() {
return [
{
category: "books",
productId: "clean-code",
},
];
}За замовчуванням параметр, якого немає в результаті generateStaticParams, не обов’язково призводить до помилки. Next.js може створити таку сторінку під час першого запиту, якщо маршрут дозволяє динамічну генерацію.
За потреби можна заборонити всі параметри, яких немає у списку:
// app/products/[id]/page.tsx
export const dynamicParams = false;
export function generateStaticParams() {
return [
{ id: "keyboard" },
{ id: "mouse" },
];
}Тепер:
/products/keyboard буде доступним;
/products/mouse буде доступним;
/products/phone поверне 404.
Цей варіант корисний, коли список сторінок повністю відомий під час збірки й нові параметри не повинні оброблятися після неї.
Статична генерація діє на рівні сегментів маршруту. Розглянемо структуру:
app/
├── layout.tsx
├── products/
│ ├── layout.tsx
│ └── [id]/
│ └── page.tsxКожен layout і page є частиною дерева маршруту. Якщо сегмент використовує дані, які доступні лише під час запиту, статична генерація цього сегмента або всього маршруту може бути неможливою.
До динамічних API належать, зокрема:
cookies();
headers();
дані поточного запиту;
параметри пошуку, які читаються серверним компонентом;
інші API, значення яких відрізняються для різних запитів.
Приклад динамічного маршруту:
// app/account/page.tsx
import { cookies } from "next/headers";
export default async function AccountPage() {
const cookieStore = await cookies();
const session = cookieStore.get("session");
return (
<main>
<h1>Особистий кабінет</h1>
<p>
Статус сесії: {session ? "авторизовано" : "не авторизовано"}
</p>
</main>
);
}Вміст цієї сторінки залежить від cookies конкретного користувача. Одна загальна HTML-версія для всіх відвідувачів тут не підходить, тому сторінка не є звичайною статичною сторінкою.
Якщо layout не використовує динамічні API, він може бути статичним, навіть якщо дочірня сторінка є динамічною.
Наприклад:
// app/layout.tsx
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="uk">
<body>
<header>Мій магазин</header>
{children}
</body>
</html>
);
}А сторінка нижче використовує cookies:
// app/account/page.tsx
import { cookies } from "next/headers";
export default async function AccountPage() {
const cookieStore = await cookies();
const theme = cookieStore.get("theme")?.value ?? "light";
return (
<main data-theme={theme}>
<h1>Особистий кабінет</h1>
</main>
);
}Статична частина дерева може залишатися спільною, але сторінка облікового запису повинна враховувати дані поточного запиту.
У сегменті маршруту можна явно вказати бажаний режим:
export const dynamic = "force-static";Це вказує Next.js примусово обробляти маршрут як статичний. Такий режим доречний лише тоді, коли сторінка справді не залежить від cookies, заголовків та інших даних запиту.
// app/about/page.tsx
export const dynamic = "force-static";
export default function AboutPage() {
return (
<main>
<h1>Про компанію</h1>
<p>Цей текст однаковий для всіх користувачів.</p>
</main>
);
}Не варто використовувати force-static, щоб приховати залежність від даних запиту. Якщо сторінка повинна показувати персоналізований результат, її не слід примусово робити статичною.
output: "export"Якщо застосунок потрібно розгорнути як набір статичних файлів, у конфігурації можна вказати:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
};
export default nextConfig;У цьому режимі Next.js створює статичні файли під час збірки. Для динамічних маршрутів потрібно явно згенерувати всі можливі параметри через generateStaticParams.
Наприклад, маршрут:
app/products/[id]/page.tsxповинен мати повний список товарів:
export function generateStaticParams() {
return products.map((product) => ({
id: product.id,
}));
}Серверна обробка запиту після збірки недоступна в режимі статичного експорту. Тому сторінки, які залежать від cookies, заголовків або серверної логіки під час запиту, не підходять для такого розгортання.
Для перевірки статичної генерації виконайте:
npm run buildПід час збірки Next.js аналізує маршрути та показує інформацію про їхню генерацію. Після цього можна запустити production-сервер:
npm run startВажливо перевіряти саме production-збірку. Режим розробки має іншу поведінку та не є точним показником того, які сторінки були створені під час next build.
Для [id] потрібно повертати id, а не productId:
// Правильно
return [{ id: "keyboard" }];
// Неправильно
return [{ productId: "keyboard" }];Динамічний сегмент [id] сам по собі не створює сторінки для всіх товарів. Список потрібно отримати в generateStaticParams.
Якщо результат залежить від cookies, він не може бути однією статичною версією для всіх користувачів.
import { cookies } from "next/headers";
export default async function Page() {
const cookieStore = await cookies();
// Сторінка залежить від конкретного запиту.
const language = cookieStore.get("language")?.value;
return <p>{language}</p>;
}Якщо generateStaticParams повертає сотні тисяч або мільйони записів, збірка може стати повільною та вимагати багато пам’яті. Для таких даних потрібно обмежити кількість сторінок, які створюються наперед, або вибрати іншу стратегію генерації.
Навіть для статично згенерованого маршруту варто передбачити випадок, коли запис не знайдено:
import { notFound } from "next/navigation";
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = products.find((item) => item.id === id);
if (!product) {
notFound();
}
return <h1>{product.name}</h1>;
}notFound() завершує обробку сторінки та показує найближчий файл not-found.tsx або стандартну сторінку 404.
Статична генерація створює сторінки під час next build.
Динамічні маршрути описуються сегментами на кшталт [id].
generateStaticParams визначає параметри, для яких сторінки потрібно створити під час збірки.
dynamicParams = false забороняє невідомі параметри та повертає 404.
Cookies, HTTP-заголовки й інші дані поточного запиту визначають межі статичної генерації.
output: "export" вимагає, щоб усі сторінки можна було підготувати як статичні файли.
Результат потрібно перевіряти через production-команди next build і next start.