Каталог может выглядеть заполненным в вашем браузере и отдавать пустую страницу до загрузки JavaScript. Например, сервер рисует заголовок и контейнер, а клиентский компонент получает все товары через useEffect. Это не доказывает, что поисковик никогда не увидит карточки, но добавляет зависимость от выполнения скриптов и успешного клиентского запроса.
Покажем собственный учебный вариант каталога на Next.js 15 с App Router: сервер получает опубликованные товары, а страница выводит названия и обычные ссылки. Кнопки корзины и интерактивный фильтр можно оставить клиентскими. Это пример устройства страницы, а не готовый шаблон магазина со всеми правилами продажи.
Посмотрите, что приходит до клиентской загрузки
Возьмите адрес категории с известными товарами. Откройте исходный HTML и найдите название одной карточки и её адрес. Затем отключите JavaScript в отдельном тестовом браузере и повторите прямой переход. Сравнивайте один и тот же URL, регион, язык и состояние авторизации.
Если сервер отдаёт только «Загрузка», ищите место получения каталога. Оно может находиться в useEffect, обработчике кнопки, внешнем виджете или запросе, который зависит от браузерного токена. Отличайте такой случай от нормальной серверной загрузки, просто медленно отдающей завершённый HTML.
Google умеет обрабатывать JavaScript, но отдельно описывает получение страницы и её рендеринг. Наличие красивого каталога в вашем сеансе не подтверждает результат обработки поисковым роботом. Для диагностики важны доступность ресурсов и фактическое содержание ответа. Основы JavaScript SEO от Google.
Согласуйте безопасный ответ каталожного API
В учебном примере источник возвращает только данные для публичного каталога: slug, title и признак published. Настоящий магазин может иметь другие поля и правила. Попросите разработчика отдельно описать, какие товары посетителю разрешено видеть и на какой стороне это ограничение применяется.
Не переносите в браузер весь внутренний ответ ERP, чтобы там скрыть ненужное фильтром. В нём могут оказаться закупочные цены, черновики, закрытые описания и служебные идентификаторы. Серверная страница должна выводить только нужные публичные данные. Авторизация самого источника остаётся на сервере и не появляется в HTML.
Проверьте поведение при ошибке API. Пустой успешный каталог и невозможность загрузить список — разные состояния. Если вместо сообщения о сбое приложение показывает «В этой категории нет товаров», посетитель может решить, что ассортимент закончился. У источника должны быть понятные условия для пустого ответа и для ошибки.
Получите товары в Server Component
По умолчанию page.tsx App Router является серверным компонентом, пока вы не переносите его в клиентскую границу. Учебная страница app/catalog/page.tsx может выглядеть так. CATALOG_API_ORIGIN задаётся серверной конфигурацией; маршрут /public/products и поля необходимо реализовать в своём источнике.
import Link from 'next/link'
type ProductCard = { slug: string; title: string }
async function publishedCards(): Promise<ProductCard[]> {
const origin = process.env.CATALOG_API_ORIGIN
if (!origin) throw new Error('Catalog source is not configured')
const response = await fetch(new URL('/public/products', origin), {
cache: 'no-store',
signal: AbortSignal.timeout(5000),
})
if (!response.ok) throw new Error('Catalog is temporarily unavailable')
const rows: unknown = await response.json()
if (!Array.isArray(rows)) throw new Error('Unexpected catalog format')
return rows.flatMap((row) => {
if (!row || typeof row !== 'object'
|| typeof row.published !== 'boolean') {
throw new Error('Unexpected product record')
}
if (!row.published) return []
if (typeof row.slug !== 'string' || typeof row.title !== 'string'
|| !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(row.slug)
|| !row.title.trim()) {
throw new Error('Invalid published product')
}
return [{ slug: row.slug, title: row.title }]
})
}
export default async function CatalogPage() {
const products = await publishedCards()
return <main>
<h1>Каталог кружек</h1>
{products.length === 0 ? <p>В этом разделе пока нет товаров.</p> :
<ul>{products.map((product) => <li key={product.slug}>
<Link href={`/catalog/${encodeURIComponent(product.slug)}`}>
{product.title}
</Link>
</li>)}</ul>}
</main>
}Пример использует no-store, чтобы при первом разборе исключить влияние серверного кеша. Это не рекомендация отключать кеш во всём магазине. Когда корректность подтверждена, выберите подходящую политику свежести данных. Возможность загрузки данных в серверном компоненте описана в документации Next.js 15.
Оставьте переходы настоящими ссылками
Карточка товара должна иметь ссылку с адресом в href. Нажатие на обработчик onClick, который меняет состояние или вызывает навигацию, решает интерфейсную задачу, но не заменяет понятную ссылку на самостоятельный URL.
Компонент Link Next.js используется для перехода между маршрутами приложения. Его настройки не освобождают от проверки результата: откройте DOM и найдите элемент a с нужным href. Подробности поведения компонента есть в справке Link для версии 15.
Название ссылки должно объяснять, куда она ведёт. Если вся карточка кликабельна, проверьте вложенные кнопки избранного и корзины: не вкладывайте одну ссылку в другую. Не заставляйте нажатие на «Добавить в корзину» одновременно открывать товар. У пользователя клавиатуры должны оставаться доступные самостоятельные действия.
Добавьте доступ к следующим страницам каталога
Если товаров много, выводить весь ассортимент в одном ответе не обязательно. Важно, чтобы следующие части имели доступные URL и ссылки, а не появлялись исключительно после прокрутки и выполнения скрипта. Определите, как ваш API возвращает номер страницы, общее количество и порядок товаров.
Например, категория может использовать /catalog?page=2. При прямом открытии этот адрес должен отдавать вторую часть списка. Не возвращайте на любом номере первую часть и не рассчитывайте, что клиент исправит её после загрузки. Проверьте пустую последнюю страницу, отрицательный номер и значение больше доступного диапазона.
Карта сайта дополняет навигацию, но не исправляет отсутствующие ссылки между страницами каталога. Она может содержать URL всех опубликованных карточек, даже если на первой странице видно только несколько. Для крупного ассортимента разберите разделение sitemap в Next.js.
Как проверить результат и передать задачу
Проверьте существующую категорию, пустую категорию и имитацию сбоя источника. Название выбранного товара и его ссылка должны присутствовать в завершённом серверном ответе. При отключённом JavaScript посетитель может прочитать каталог и перейти на карточку; необязательные интерактивные действия проверяются отдельно.
Обязательно сравните прямое открытие URL с переходом внутри приложения. Затем откройте соседнюю страницу пагинации. Проверьте, что набор карточек отличается ожидаемым способом и ссылка ведёт на тот же товар, название которого показано. Если используется потоковая отдача или loading.tsx, включите её в тест: временный экран загрузки не должен оставаться единственным доступным содержимым.
Запустите технический аудит страницы каталога. Он помогает посмотреть полученный ответ и найденные замечания, но не подтверждает индексацию каждого товара и не проверяет все варианты клиентского фильтра. Структура серверных и клиентских компонентов подробнее разобрана в руководстве по Next.js 15.
В задаче разработчику перечислите контрольные товары, допустимые поля, правила публикации, URL следующих страниц и сценарий сбоя. Работу принимают по содержимому ответа и поведению переходов. Рост трафика после такой правки отдельно оценивают по поисковым данным: исправление загрузки устраняет конкретную зависимость, но само по себе не гарантирует позиции.