Пошук уроків, статей та іншого контенту
Розберете вплив пагінації на індексацію та налаштуєте URL, canonical і посилання для сторінок списків.
Пагінація розділяє великий список на окремі URL:
/catalog
/catalog?page=2
/catalog?page=3Кожна сторінка списку може бути окремим документом, який пошуковий робот:
знаходить через HTML-посилання;
обходить;
індексує;
показує у результатах пошуку, якщо сторінка має самостійну цінність.
Пагінація сама по собі не є проблемою для SEO. Проблеми виникають, коли:
усі сторінки мають однаковий canonical на першу сторінку;
наступні сторінки доступні лише після виконання JavaScript;
сторінки списку не містять звичайних HTML-посилань;
різні URL показують однаковий контент;
сторінки з неіснуючими номерами успішно відповідають статусом 200.
URL має однозначно описувати стан списку. Для пагінації зручно використовувати query-параметр:
/catalog
/catalog?page=2
/catalog?page=3Першу сторінку краще представляти одним URL. Тобто такі адреси не повинні дублювати одна одну:
/catalog
/catalog?page=1У застосунку можна дозволити обидва варіанти як вхідні URL, але canonical для них має вести на /catalog.
Якщо список має фільтри, вони також повинні входити в URL:
/catalog?category=books&page=2
/catalog?category=books&sort=price&page=3Порядок query-параметрів бажано нормалізувати. Якщо ці URL показують однаковий список:
/catalog?category=books&page=2
/catalog?page=2&category=booksвони повинні мати однаковий canonical.
Обидва варіанти коректні:
/catalog?page=2
/catalog/page/2Важлива не форма URL, а послідовність:
URL повинен бути стабільним.
URL повинен відтворювати конкретну сторінку.
URL повинен бути доступним для звичайного переходу.
Одна й та сама сторінка не повинна мати багато непотрібних URL.
У прикладах далі використовується /catalog?page=2.
Для сторінок пагінації зазвичай використовується self-referencing canonical:
/catalog → canonical /catalog
/catalog?page=2 → canonical /catalog?page=2
/catalog?page=3 → canonical /catalog?page=3Це важливо, тому що сторінки /catalog?page=2 і /catalog?page=3 містять різні товари. Якщо canonical усіх сторінок вести на /catalog, пошукова система може не індексувати вміст наступних сторінок.
Canonical першої сторінки слід нормалізувати:
/catalog?page=1 → canonical /catalogДля URL із фільтром canonical має зберігати фільтр:
/catalog?category=books&page=2не повинен мати canonical лише на:
/catalogякщо сторінка з фільтром містить окремий корисний список.
Canonical є підказкою для пошукової системи, а не заміною правильній структурі посилань. Наступні сторінки все одно потрібно зв’язати між собою звичайними HTML-посиланнями.
rel="next" і rel="prev"Раніше для пагінації часто використовували:
<link rel="prev" href="/catalog">
<link rel="next" href="/catalog?page=3">Пошукова система Google більше не використовує rel="next" і rel="prev" як спеціальний сигнал пагінації. Тому SEO-основа пагінації — це:
унікальні URL;
self-referencing canonical;
звичайні посилання між сторінками;
серверний HTML-контент;
коректні статуси відповідей.
Атрибути rel="prev" і rel="next" можна додати як додаткову семантику для інших інструментів, але покладатися на них не слід.
У Next.js App Router query-параметри сторінки доступні через searchParams. Для SEO-метаданих використовується generateMetadata.
Нижче наведено приклад сторінки каталогу, яка:
читає номер сторінки з URL;
генерує self-referencing canonical;
нормалізує page=1;
створює серверні посилання для пагінації;
повертає 404 для неіснуючої сторінки;
додає метадані для кожної сторінки.
metadataBaseФайл app/layout.tsx повинен містити базову адресу сайту:
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://example.com"),
title: {
default: "Каталог",
template: "%s | Example",
},
description: "Каталог товарів Example",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="uk">
<body>{children}</body>
</html>
);
}У реальному проєкті https://example.com потрібно замінити на канонічний домен сайту. Не слід будувати canonical на основі довільного заголовка Host, якщо домен не перевіряється сервером.
Файл app/catalog/page.tsx:
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
type SearchParams = Promise<{
page?: string;
category?: string;
}>;
type Product = {
id: number;
name: string;
category: string;
};
const products: Product[] = [
{ id: 1, name: "TypeScript для розробників", category: "books" },
{ id: 2, name: "Архітектура вебзастосунків", category: "books" },
{ id: 3, name: "Механічна клавіатура", category: "devices" },
{ id: 4, name: "Монітор 27 дюймів", category: "devices" },
{ id: 5, name: "Практика Next.js", category: "books" },
{ id: 6, name: "USB-C док-станція", category: "devices" },
{ id: 7, name: "Курс із вебдоступності", category: "books" },
];
const ITEMS_PER_PAGE = 3;
function getPage(value: string | undefined): number {
const page = Number(value);
if (!Number.isInteger(page) || page < 1) {
return 1;
}
return page;
}
function buildCatalogUrl({
page,
category,
}: {
page?: number;
category?: string;
}): string {
const params = new URLSearchParams();
if (category) {
params.set("category", category);
}
// Першу сторінку представляємо URL без параметра page.
if (page && page > 1) {
params.set("page", String(page));
}
const query = params.toString();
return query ? `/catalog?${query}` : "/catalog";
}
async function getCatalogData(category: string | undefined) {
const filteredProducts = category
? products.filter((product) => product.category === category)
: products;
const totalPages = Math.ceil(filteredProducts.length / ITEMS_PER_PAGE);
return {
products: filteredProducts,
totalPages,
};
}
export async function generateMetadata({
searchParams,
}: {
searchParams: SearchParams;
}): Promise<Metadata> {
const params = await searchParams;
const page = getPage(params.page);
const category = params.category;
const { totalPages } = await getCatalogData(category);
// Некоректні сторінки не повинні отримувати індексовані метадані.
if (page > totalPages && totalPages > 0) {
return {
title: "Сторінку не знайдено",
robots: {
index: false,
follow: false,
},
};
}
const canonical = buildCatalogUrl({
page,
category,
});
const pageTitle = category
? `Каталог: ${category}${page > 1 ? ` — сторінка ${page}` : ""}`
: `Каталог${page > 1 ? ` — сторінка ${page}` : ""}`;
return {
title: pageTitle,
alternates: {
canonical,
},
robots: {
index: true,
follow: true,
},
};
}
export default async function CatalogPage({
searchParams,
}: {
searchParams: SearchParams;
}) {
const params = await searchParams;
const page = getPage(params.page);
const category = params.category;
const { products: filteredProducts, totalPages } =
await getCatalogData(category);
if (page > totalPages && totalPages > 0) {
notFound();
}
const startIndex = (page - 1) * ITEMS_PER_PAGE;
const visibleProducts = filteredProducts.slice(
startIndex,
startIndex + ITEMS_PER_PAGE,
);
const previousUrl =
page > 1
? buildCatalogUrl({
page: page - 1,
category,
})
: null;
const nextUrl =
page < totalPages
? buildCatalogUrl({
page: page + 1,
category,
})
: null;
return (
<main>
<h1>{category ? `Каталог: ${category}` : "Каталог"}</h1>
<ul>
{visibleProducts.map((product) => (
<li key={product.id}>
<article>
<h2>{product.name}</h2>
<p>Категорія: {product.category}</p>
</article>
</li>
))}
</ul>
{totalPages > 1 && (
<nav aria-label="Пагінація каталогу">
{previousUrl && (
<Link href={previousUrl} rel="prev">
Попередня
</Link>
)}
<ul>
{Array.from({ length: totalPages }, (_, index) => {
const pageNumber = index + 1;
const pageUrl = buildCatalogUrl({
page: pageNumber,
category,
});
return (
<li key={pageNumber}>
<Link
href={pageUrl}
aria-current={pageNumber === page ? "page" : undefined}
>
{pageNumber}
</Link>
</li>
);
})}
</ul>
{nextUrl && (
<Link href={nextUrl} rel="next">
Наступна
</Link>
)}
</nav>
)}
</main>
);
}У Next.js 15 searchParams для сторінки App Router є асинхронним значенням, тому в прикладі використовується Promise і await. Для версій Next.js, де searchParams є звичайним об’єктом, тип і використання потрібно адаптувати:
type SearchParams = {
page?: string;
category?: string;
};Основна SEO-логіка від цього не змінюється.
Пошуковий робот повинен побачити посилання в HTML:
<a href="/catalog?page=2">2</a>Компонент Link Next.js генерує звичайний елемент a, тому він підходить для пагінації. Важливо не замінювати його на кнопку, яка змінює стан лише в браузері:
// Поганий варіант для індексації сторінок списку
<button onClick={() => setPage(2)}>2</button>Кнопка може бути потрібна для інтерфейсу, але сама по собі вона не створює URL, який пошуковий робот зможе стабільно обійти.
Якщо пагінація працює через клієнтську логіку, кожна сторінка все одно повинна мати доступний URL і звичайне посилання на нього.
Розглянемо URL:
/catalog?page=999Якщо в каталозі лише три сторінки, правильна поведінка — повернути 404, а не показати порожню сторінку зі статусом 200.
У Next.js для цього використовується notFound():
import { notFound } from "next/navigation";
if (page > totalPages) {
notFound();
}Це запобігає індексації неіснуючих сторінок і не створює враження, що сторінка є коректним документом.
Окремо потрібно визначити поведінку для порожнього результату фільтра:
якщо категорія існує, але товарів у ній немає, це може бути валідна сторінка;
якщо категорія не існує взагалі, можна повернути 404;
не слід автоматично додавати noindex до кожної сторінки з нульовими результатами без вимог конкретного проєкту.
Не всі query-параметри змінюють основний контент. Наприклад:
/catalog?page=2
/catalog?page=2&utm_source=newsletterutm_source використовується для аналітики, а не для формування окремої сторінки. Canonical у такому випадку повинен ігнорувати цей параметр:
/catalog?page=2Те саме стосується параметрів, які не змінюють товари, заголовок або зміст сторінки.
Натомість параметри, що змінюють набір результатів, потрібно враховувати:
/catalog?category=books&page=2
/catalog?category=devices&page=2Це різні списки, тому canonical для них не повинен бути однаковим.
Практичний підхід:
Визначити whitelist параметрів, які впливають на контент.
Додати до canonical лише ці параметри.
Видалити зайві аналітичні та технічні параметри.
Нормалізувати значення і порядок параметрів.
Не створювати canonical на URL, який показує інший список.
noindex для пагінаціїНе потрібно автоматично ставити noindex на всі сторінки після першої:
/catalog?page=2
/catalog?page=3Якщо вони містять унікальні товари, доступні через посилання та мають коректні canonical, вони можуть бути корисними для пошуку.
noindex може бути доречним для:
технічних сторінок;
сторінок із некоректними параметрами;
комбінацій фільтрів, які не мають пошукової цінності;
внутрішніх результатів пошуку, якщо це передбачено SEO-стратегією проєкту.
У generateMetadata це можна задати так:
export const metadata: Metadata = {
robots: {
index: false,
follow: true,
},
};Не слід одночасно блокувати URL у robots.txt і очікувати, що noindex буде прочитано. Якщо пошуковому роботу заборонено обходити сторінку, він може не побачити її meta robots. Для конкретної SEO-стратегії потрібно вибрати узгоджену поведінку.
Коли користувач переходить на іншу категорію, номер сторінки потрібно скинути:
/catalog?category=books&page=4
→ /catalog?category=devicesІнакше нова категорія може не мати четвертої сторінки, а користувач отримає 404 або порожній список.
Це також спрощує canonical: кожна комбінація фільтра та сторінки має передбачуваний URL.
Для кожної сторінки списку перевірте:
/catalog має canonical /catalog;
/catalog?page=1 має canonical /catalog;
/catalog?page=2 має canonical /catalog?page=2;
сторінка 2 містить інші елементи, ніж сторінка 1;
у HTML є посилання на попередню та наступну сторінки;
посилання мають реальні href;
неіснуюча сторінка повертає 404;
canonical не містить непотрібних tracking-параметрів;
canonical враховує фільтри, які змінюють контент;
метадані генеруються на сервері для кожного URL.
/catalog?page=2 → canonical /catalog
/catalog?page=3 → canonical /catalogТак пошуковій системі повідомляється, що друга і третя сторінки не є самостійними URL. Для звичайної пагінації краще використовувати canonical самої сторінки.
onClickЯкщо перемикання сторінок не змінює URL і не створює a href, пошуковий робот не матиме повної структури каталогу для обходу.
page=1 як окремої сторінкиURL /catalog і /catalog?page=1 можуть стати дублями. Нормалізуйте першу сторінку до /catalog.
200 для будь-якого номера сторінкиURL /catalog?page=9999 не повинен повертати звичайний порожній каталог зі статусом 200. Використовуйте notFound() для сторінки за межами діапазону.
Посилання на наступну сторінку не повинно перетворювати:
/catalog?category=books&page=2на:
/catalog?page=3У такому випадку користувач і пошуковий робот залишають відфільтрований список.
Не формуйте абсолютні canonical через довільний HTTP-заголовок. Неправильна конфігурація proxy або host може створити canonical на сторонній домен. Краще задати стабільний metadataBase у конфігурації застосунку.
Кожна значуща сторінка пагінації повинна мати власний стабільний URL.
Перша сторінка має один нормалізований URL без page=1.
Сторінки з різним вмістом повинні мати self-referencing canonical.
Фільтри, які змінюють список, потрібно зберігати в URL і canonical.
Пагінація повинна використовувати звичайні HTML-посилання або next/link.
rel="next" і rel="prev" не замінюють правильні URL та посилання.
Неіснуючі номери сторінок мають повертати 404.
noindex застосовується до технічних або нецінних комбінацій, а не автоматично до всіх сторінок після першої.