Если поставщик данных не отвечает, товар ещё не стал удалённым. Однако страница может показывать «Товар не найден» из-за общего обработчика ошибок. Посетитель видит сообщение об отсутствии товара, а поисковый робот получает сигнал, который разработчик предназначал для удалённой карточки. Исправлять нужно решение в коде, а не надпись в шаблоне.
Разберём собственный учебный пример магазина на Next.js 15 с App Router. Карточка /catalog/red-mug получает товар из API. Такой адрес и ответы API используются только для объяснения; это не результаты проверки настоящего магазина. Для Pages Router и других основных версий потребуется проверить соответствующий механизм получения данных.
Когда товар отсутствует, а когда проверка не состоялась
Сначала согласуйте контракт с API. В нашем примере 404 означает: товар с этим адресом действительно не существует. 200 возвращает опубликованную карточку. 500, 502, тайм-аут и невалидный JSON означают, что приложение не смогло получить подтверждённые сведения.
Не всегда поставщик использует именно такой контракт. Иногда отсутствие обозначается 200 с полем found: false, а ответ 404 принадлежит самому неправильно настроенному маршруту API. Уточните документацию и посмотрите сохранённые ответы. Нельзя считать удалённым товар, если приложение обращалось к неверному сервису или потеряло авторизацию.
| Ответ источника | Что известно | Что должен делать интерфейс |
|---|---|---|
Подтверждённый 404 товара | Карточки нет | Показать предусмотренное состояние отсутствия |
Успешный 200 с ожидаемыми данными | Товар найден | Вывести карточку |
401 или 403 | Не прошла авторизация | Зарегистрировать проблему доступа, не объявлять товар удалённым |
500 или тайм-аут | Данные неизвестны | Обработать временный сбой |
200 с повреждённым JSON | Данные непригодны | Обработать ошибку формата |
Найдите место, которое скрывает разные причины
Типичный источник проблемы — функция, возвращающая null после любой ошибки. Следующая строка if (!product) notFound() уже не знает, был ли товар удалён или сервер перестал отвечать. Так удобно писать короткий код, но невозможно правильно объяснить его результат.
Посмотрите функцию загрузки товара, общий API-клиент и компонент страницы. Важно проверить все три: нужное различие может теряться глубже, чем кажется по странице. Поиск по notFound( и catch поможет найти кандидатов, но совпадение само по себе не доказывает проблему. Сравните найденную ветку с конкретным неуспешным запросом.
Попросите разработчика временно записывать идентификатор обращения, маршрут источника без секретов и его статус. Токен авторизации, персональные данные покупателя и полный ответ внутреннего сервиса в общедоступные логи не нужны. Такая запись должна позволить связать сбой страницы с ответом API.
Разделите обработку в серверной странице
Ниже учебный пример app/catalog/[slug]/page.tsx. Переменная CATALOG_API_ORIGIN должна указывать на ваш доверенный API. Пример намеренно не использует кеш: сначала проверяем правильную обработку разных ответов. Схему JSON, авторизацию и правила публикации адаптируют к настоящему источнику.
import { notFound } from 'next/navigation'
type Product = { name: string; slug: string }
async function loadProduct(slug: string): Promise<Product | null> {
const origin = process.env.CATALOG_API_ORIGIN
if (!origin) throw new Error('Catalog API is not configured')
const url = new URL(`/products/${encodeURIComponent(slug)}`, origin)
const response = await fetch(url, {
cache: 'no-store',
signal: AbortSignal.timeout(5000),
})
if (response.status === 404) return null
if (!response.ok) throw new Error(`Catalog API status ${response.status}`)
const product: unknown = await response.json()
if (!product || typeof product !== 'object'
|| !('name' in product) || typeof product.name !== 'string'
|| !('slug' in product) || product.slug !== slug) {
throw new Error('Unexpected catalog response')
}
return product as Product
}
export default async function ProductPage({
params,
}: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const product = await loadProduct(slug)
if (product === null) notFound()
return <main><h1>{product.name}</h1></main>
}notFound() завершает рендеринг сегмента и добавляет noindex. Поэтому вызывать его нужно после подтверждения отсутствия. Это поведение описано в справке Next.js 15. Ошибки загрузки в примере остаются ошибками: они не превращаются в null.
Подготовьте сообщение для временного сбоя
В error.tsx соответствующего сегмента можно показать понятное сообщение: «Не удалось загрузить товар. Попробуйте ещё раз». Для посетителя важнее следующий шаг, чем название внутреннего исключения. Не выводите необработанное error.message: оно может раскрывать устройство сервиса, а человеку не объяснит решение.
У страницы отсутствующего товара свой сценарий: ссылка на каталог, поиск или ближайший подходящий раздел. У временного сбоя — возможность повторить попытку. Различие полезно и сотруднику поддержки: он не будет отвечать, что карточку удалили, когда источник просто недоступен. Общие правила для ошибок серверных компонентов и вложенных границ приведены в документации по обработке ошибок.
Не обещайте посетителю, что повтор обязательно поможет. Если API недоступен долго, кнопка лишь повторит неудачный запрос. Команда должна видеть уведомление о сбое источника и иметь способ временно отключить проблемный сценарий или показать проверенные сохранённые данные с их настоящей датой.
Проверьте ответы вне переходов внутри приложения
Соберите тестовый источник с четырьмя режимами: нормальный товар, подтверждённое отсутствие, временный 503, испорченный JSON. Откройте каждый адрес прямым переходом в новом окне. Затем повторите переход из каталога. Сравните сообщение, серверные логи, статус основного документа и наличие noindex.
Нельзя определять статус страницы только по статусу запроса API в панели Network. Это два разных ответа. Кроме того, в App Router уже начавшийся потоковый ответ способен сохранить 200, даже когда позднее возникло состояние отсутствия. Этот отдельный случай разобран в статье про notFound и потоковую отдачу.
После исправления запустите технический аудит страницы и сопоставьте его с ручной проверкой. Аудит фиксирует полученный ответ, но не воспроизводит все ваши тестовые режимы и не доказывает устойчивость API под нагрузкой. Для общей схемы проекта пригодится руководство по Next.js 15.
Как принять работу разработчика
В задаче запишите исходный адрес, тип временного сбоя и ожидаемое различие состояний. Приложите обезличенный пример ответа API. Попросите показать, где заканчивается обработка подтверждённого отсутствия и начинается обработка неизвестного результата.
Приёмка должна включать существующий товар, отсутствующий товар, ошибку авторизации, тайм-аут и повреждённые данные. После восстановления API существующая карточка снова открывается с прежним содержимым; её не нужно вручную возвращать из состояния «удалено». Никакие ошибки источника не записываются как доказанное удаление товара.
Наконец, проверьте, что разработчик не исправил проблему простым удалением всех notFound(). Настоящие отсутствующие карточки тоже должны обрабатываться осмысленно. Цель изменения — сохранить правильный ответ для каждого подтверждённого состояния и честно обозначить ситуации, в которых данных пока нет.