Пошук уроків, статей та іншого контенту
Оновлюйте статичний контент після збірки через Incremental Static Regeneration і стратегії повторної валідації.
Incremental Static Regeneration (ISR) — це стратегія, за якої Next.js:
генерує сторінку як статичний HTML;
віддає згенеровану сторінку з кешу;
після завершення заданого часу повторно отримує актуальні дані;
створює оновлену версію сторінки без повної повторної збірки застосунку.
ISR поєднує переваги статичної генерації та динамічних даних:
статичні сторінки швидко віддаються користувачам;
серверне навантаження нижче, ніж у повністю динамічному рендерингу;
контент може змінюватися після next build;
оновлення можна запускати автоматично або вручну.
ISR особливо корисний для:
каталогів товарів;
новин;
документації;
сторінок блогових публікацій;
профілів або сторінок, які змінюються не після кожного запиту.
У сучасному Next.js ISR зазвичай налаштовують через:
revalidate для часової повторної валідації;
опцію
fetchrevalidatePath для оновлення конкретного маршруту;
revalidateTag для оновлення групи пов’язаних даних.
Сторінка може експортувати значення revalidate:
// app/news/page.js
export const revalidate = 60;
export default async function NewsPage() {
const response = await fetch("https://api.example.com/news");
const articles = await response.json();
return (
<main>
<h1>Новини</h1>
<ul>
{articles.map((article) => (
<li key={article.id}>
<a href={`/news/${article.slug}`}>{article.title}</a>
</li>
))}
</ul>
</main>
);
}У цьому прикладі:
перший запит генерує сторінку;
сторінка кешується на 60 секунд;
протягом 60 секунд користувачі отримують кешовану версію;
після завершення цього часу Next.js вважає кеш застарілим;
наступний запит запускає повторну валідацію.
Значення revalidate вказується в секундах.
export const revalidate = 3600;Це означає, що сторінка може залишатися актуальною протягом однієї години.
Замість налаштування всієї сторінки можна задати час кешування для конкретного запиту:
const response = await fetch("https://api.example.com/products", {
next: {
revalidate: 300,
},
});Тут відповідь цього fetch кешується на 300 секунд.
Це зручно, коли одна сторінка використовує кілька джерел даних із різною частотою оновлення:
// app/dashboard/page.js
export default async function DashboardPage() {
const [productsResponse, settingsResponse] = await Promise.all([
fetch("https://api.example.com/products", {
next: {
revalidate: 300,
},
}),
fetch("https://api.example.com/settings", {
next: {
revalidate: 3600,
},
}),
]);
const products = await productsResponse.json();
const settings = await settingsResponse.json();
return (
<main>
<h1>{settings.title}</h1>
<p>Кількість товарів: {products.length}</p>
</main>
);
}У цьому випадку:
список товарів оновлюється кожні 5 хвилин;
налаштування оновлюються щогодини;
сторінка використовує найменший відповідний інтервал для залежних даних.
ISR працює і для динамічних маршрутів, наприклад:
app/
└── posts/
└── [slug]/
└── page.jsФайл page.js може мати такий вигляд:
// app/posts/[slug]/page.js
export const revalidate = 300;
async function getPost(slug) {
const response = await fetch(`https://api.example.com/posts/${slug}`, {
next: {
revalidate: 300,
},
});
if (!response.ok) {
return null;
}
return response.json();
}
export default async function PostPage({ params }) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
return <h1>Публікацію не знайдено</h1>;
}
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}Маршрут може бути статично згенерований під час збірки за допомогою generateStaticParams:
// app/posts/[slug]/page.js
export async function generateStaticParams() {
const response = await fetch("https://api.example.com/posts", {
next: {
revalidate: 300,
},
});
const posts = await response.json();
return posts.map((post) => ({
slug: post.slug,
}));
}
export const revalidate = 300;
export default async function PostPage({ params }) {
const { slug } = await params;
const response = await fetch(`https://api.example.com/posts/${slug}`, {
next: {
revalidate: 300,
},
});
if (!response.ok) {
return <h1>Публікацію не знайдено</h1>;
}
const post = await response.json();
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}За замовчуванням Next.js може генерувати сторінки для параметрів, які не були повернуті generateStaticParams, під час першого запиту. Після цього така сторінка також може кешуватися та повторно валідуватися.
Якщо список параметрів відомий під час збірки, їх можна повернути з generateStaticParams:
export async function generateStaticParams() {
return [
{ slug: "first-post" },
{ slug: "second-post" },
];
}Ці сторінки будуть підготовлені під час next build.
Для великих або постійно змінних наборів даних не обов’язково генерувати всі сторінки під час збірки.
Наприклад, сторінка товару може бути створена під час першого запиту, а потім збережена в кеші. Для цього не потрібно додавати її параметр до generateStaticParams.
Так можна уникнути довгої збірки, якщо застосунок має тисячі або мільйони сторінок.
Якщо потрібно дозволити лише параметри, повернуті generateStaticParams, можна використати dynamicParams = false:
// app/posts/[slug]/page.js
export const dynamicParams = false;
export function generateStaticParams() {
return [
{ slug: "first-post" },
{ slug: "second-post" },
];
}
export default async function PostPage({ params }) {
const { slug } = await params;
return <h1>Публікація: {slug}</h1>;
}У такому випадку маршрут із невідомим slug не буде створений під час запиту.
revalidatePathЧасова стратегія не завжди достатня. Наприклад, адміністратор змінив статтю, і потрібно оновити сторінку одразу, не чекаючи завершення п’ятихвилинного інтервалу.
Для цього використовується revalidatePath.
// app/admin/actions.js
"use server";
import { revalidatePath } from "next/cache";
export async function updatePost(slug, title, content) {
const response = await fetch(`https://api.example.com/posts/${slug}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title,
content,
}),
});
if (!response.ok) {
throw new Error("Не вдалося оновити публікацію");
}
revalidatePath(`/posts/${slug}`);
revalidatePath("/posts");
}Після оновлення:
revalidatePath(/posts/${slug}) позначає сторінку публікації як застарілу;
revalidatePath("/posts") оновлює список публікацій;
наступний запит отримає актуальні дані та сформує нову кешовану версію.
revalidatePath можна викликати в Server Action або Route Handler.
// app/api/revalidate/route.js
import { revalidatePath } from "next/cache";
import { NextResponse } from "next/server";
export async function POST(request) {
const body = await request.json();
if (body.secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json(
{ message: "Недійсний секретний ключ" },
{ status: 401 },
);
}
if (typeof body.slug !== "string") {
return NextResponse.json(
{ message: "Параметр slug є обов’язковим" },
{ status: 400 },
);
}
revalidatePath(`/posts/${body.slug}`);
revalidatePath("/posts");
return NextResponse.json({ revalidated: true });
}Такий endpoint може викликати CMS після публікації або редагування контенту.
Приклад запиту:
curl -X POST http://localhost:3000/api/revalidate \
-H "Content-Type: application/json" \
-d '{"secret":"my-secret","slug":"nextjs-isr"}'Секрет потрібно зберігати в змінній середовища:
REVALIDATION_SECRET=my-secretНе слід дозволяти довільним користувачам викликати endpoint повторної валідації без перевірки автентифікації.
revalidatePath прив’язаний до URL. Якщо однакові дані використовуються на багатьох сторінках, зручніше додати до запиту тег.
const response = await fetch("https://api.example.com/posts", {
next: {
tags: ["posts"],
},
});Тепер цей кешований запит пов’язаний із тегом posts.
Після зміни будь-якої публікації можна оновити всі запити з цим тегом:
"use server";
import { revalidateTag } from "next/cache";
export async function publishPost() {
// Оновлення публікації в зовнішньому API
revalidateTag("posts", "max");
}Теги корисні, коли:
одні й ті самі дані відображаються на багатьох маршрутах;
неможливо або незручно перелічити всі URL;
потрібно централізовано інвалідувати групу кешованих запитів.
Повний приклад із тегами:
// app/posts/page.js
async function getPosts() {
const response = await fetch("https://api.example.com/posts", {
next: {
tags: ["posts"],
revalidate: 600,
},
});
if (!response.ok) {
throw new Error("Не вдалося завантажити публікації");
}
return response.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<main>
<h1>Публікації</h1>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</main>
);
}// app/admin/actions.js
"use server";
import { revalidateTag } from "next/cache";
export async function publishPost(postId) {
const response = await fetch(
`https://api.example.com/posts/${postId}/publish`,
{
method: "POST",
},
);
if (!response.ok) {
throw new Error("Не вдалося опублікувати публікацію");
}
revalidateTag("posts", "max");
}revalidatePath і revalidateTagrevalidatePath, колизмінився конкретний маршрут;
потрібно оновити сторінку та її залежні компоненти;
URL є природним ідентифікатором кешу;
після зміни відомо, які сторінки треба інвалідувати.
revalidatePath("/posts/nextjs-isr");revalidateTag, колиодні дані використовуються на багатьох сторінках;
потрібно оновити цілу групу запитів;
логіка кешування побудована навколо сутності або колекції.
revalidateTag("posts", "max");У реальному застосунку ці стратегії можна комбінувати:
revalidatePath(`/posts/${slug}`);
revalidatePath("/posts");
revalidateTag("posts", "max");Водночас не варто без потреби інвалідовувати весь кеш застосунку. Чим точніше визначена область оновлення, тим менше зайвої роботи виконує сервер.
Під час часової повторної валідації зазвичай використовується підхід stale-while-revalidate:
користувач отримує наявну кешовану версію;
Next.js запускає оновлення застарілих даних;
майбутні запити отримують нову версію після завершення оновлення.
Це дозволяє не блокувати кожного користувача очікуванням зовнішнього API.
Якщо оновлення не вдалося, Next.js може продовжити віддавати попередню успішну версію, доки нове оновлення не завершиться успішно. Тому зовнішні API мають обробляти помилки, тайм-аути та тимчасову недоступність.
ISR працює лише для даних, які можна кешувати.
Запит без кешування робить маршрут динамічним:
const response = await fetch("https://api.example.com/profile", {
cache: "no-store",
});Також динамічна поведінка потрібна для даних, які залежать від конкретного запиту, користувача або сесії.
Наприклад, персональну сторінку користувача не слід кешувати як спільну статичну сторінку:
// app/account/page.js
export const dynamic = "force-dynamic";
export default async function AccountPage() {
const response = await fetch("https://api.example.com/account", {
cache: "no-store",
});
const account = await response.json();
return <h1>Вітаємо, {account.name}</h1>;
}ISR підходить для загальнодоступного контенту, однакового для всіх користувачів. Для приватних даних потрібно свідомо обирати динамічний рендеринг і відсутність кешування.
Нижче наведено узгоджений приклад структури:
app/
├── posts/
│ ├── page.js
│ └── [slug]/
│ └── page.js
└── api/
└── revalidate/
└── route.jsСторінка списку:
// app/posts/page.js
export const revalidate = 600;
async function getPosts() {
const response = await fetch("https://api.example.com/posts", {
next: {
tags: ["posts"],
},
});
if (!response.ok) {
throw new Error("Не вдалося завантажити публікації");
}
return response.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<main>
<h1>Публікації</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<a href={`/posts/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
</main>
);
}Сторінка окремої публікації:
// app/posts/[slug]/page.js
export const revalidate = 600;
export async function generateStaticParams() {
const response = await fetch("https://api.example.com/posts", {
next: {
tags: ["posts"],
},
});
const posts = await response.json();
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function PostPage({ params }) {
const { slug } = await params;
const response = await fetch(`https://api.example.com/posts/${slug}`, {
next: {
tags: [`post:${slug}`],
},
});
if (!response.ok) {
return <h1>Публікацію не знайдено</h1>;
}
const post = await response.json();
return (
<main>
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
</main>
);
}Endpoint для webhook від CMS:
// app/api/revalidate/route.js
import { revalidatePath, revalidateTag } from "next/cache";
import { NextResponse } from "next/server";
export async function POST(request) {
const body = await request.json();
if (body.secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json(
{ message: "Недійсний секретний ключ" },
{ status: 401 },
);
}
const { slug } = body;
if (typeof slug !== "string") {
return NextResponse.json(
{ message: "Потрібно передати slug" },
{ status: 400 },
);
}
revalidatePath("/posts");
revalidatePath(`/posts/${slug}`);
revalidateTag("posts", "max");
revalidateTag(`post:${slug}`, "max");
return NextResponse.json({
revalidated: true,
slug,
});
}Тепер контент можна оновлювати двома способами:
автоматично після завершення інтервалу revalidate;
негайно після webhook від CMS.
У старішій архітектурі Pages Router ISR налаштовується через поле revalidate у getStaticProps:
// pages/news.js
export async function getStaticProps() {
const response = await fetch("https://api.example.com/news");
const articles = await response.json();
return {
props: {
articles,
},
revalidate: 60,
};
}
export default function NewsPage({ articles }) {
return (
<main>
<h1>Новини</h1>
<ul>
{articles.map((article) => (
<li key={article.id}>{article.title}</li>
))}
</ul>
</main>
);
}Логіка така сама: сторінка генерується статично, а після завершення інтервалу Next.js створює оновлену версію.
Для нових частин застосунку бажано використовувати App Router, але в проєктах із Pages Router цей API все ще може бути основним способом реалізації ISR.
cache: "no-store" разом із очікуванням ISRfetch("https://api.example.com/posts", {
cache: "no-store",
});Такий запит не кешується, тому часовий ISR для нього не працює.
Якщо дані можна кешувати, використовуйте:
fetch("https://api.example.com/posts", {
next: {
revalidate: 300,
},
});Не можна кешувати відповідь, яка залежить від cookie, токена або поточного користувача, як загальнодоступну ISR-сторінку.
Для таких даних використовуйте динамічний рендеринг і cache: "no-store".
Повторна валідація позначає кеш як застарілий, але користувач, який уже отримав стару відповідь, не побачить її автоматично оновленою. Оновлені дані з’являться під час наступного запиту або наступного рендерингу.
Якщо сторінка доступна за адресою /posts/example, виклик:
revalidatePath("/post/example");не оновить потрібний кеш. Шлях у revalidatePath має точно відповідати маршруту.
Публічний endpoint повторної валідації без секрету або автентифікації може дозволити стороннім користувачам постійно очищати кеш.
Завжди перевіряйте:
секретний ключ;
типи вхідних параметрів;
допустимий формат slug;
права доступу до операції.
Якщо новини мають оновлюватися щохвилини, значення revalidate = 86400 зробить дані застарілими майже на добу.
Інтервал потрібно обирати відповідно до вимог до свіжості даних:
рідко змінювані налаштування — години або дні;
каталог — хвилини;
термінові новини — короткий інтервал або webhook;
приватні дані — без кешування.
ISR дозволяє оновлювати статично згенерований контент після збірки.
export const revalidate = N задає інтервал повторної валідації для маршруту.
next.revalidate задає інтервал для конкретного fetch.
generateStaticParams генерує відомі динамічні маршрути під час збірки.
revalidatePath інвалідує кеш конкретного маршруту.
revalidateTag інвалідує групу кешованих запитів.
Webhook від CMS дозволяє оновлювати контент одразу після публікації.
cache: "no-store" вимикає кешування та переводить запит у динамічний режим.
Приватні дані не можна кешувати як спільний статичний контент.
Найкраща стратегія залежить від частоти змін, вартості запиту та вимог до актуальності даних.