После переноса магазина на новый домен карточка товара открывается правильно, а её canonical всё ещё ведёт на старый адрес. Иногда разработчик уже заменил metadataBase, но результат не изменился. Причину нужно искать в том месте, где собирается конкретный canonical: базовый адрес не исправляет каждую переданную ему ссылку.
Ниже — собственный учебный пример магазина на Next.js 15 с App Router. У него новый публичный домен shop.example.ru, старый old-shop.example.ru и карточка /catalog/red-mug. Домены и товар вымышленные. Такой пример помогает проверить настройку, но не доказывает причину ошибки на вашем сайте без просмотра кода и ответа сервера.
Сначала запишите адрес страницы и фактический canonical
Откройте проблемную карточку через новый публичный домен и сохраните полный HTML ответа. Найдите элемент link с rel="canonical", запишите его href, итоговый URL после перенаправлений и HTTP-статус. Отдельно посмотрите, нет ли канонического адреса в заголовке Link ответа: его может добавлять другой слой приложения.
Проверьте хотя бы три разных карточки. Если все указывают на главную, вероятна общая настройка родительского сегмента. Если неправильный домен встречается только в одной категории, начинайте с её layout, шаблона метаданных или данных CMS. Это направления поиска, а не подтверждённый диагноз.
Снимок DOM полезен, но не заменяет сохранённый ответ. JavaScript, расширение браузера или другая версия релиза могут изменить наблюдаемую картину. Метаданные при некоторых настройках поступают позже в потоке ответа, поэтому проверяйте завершённый HTML, а не первые полученные строки.
Подготовьте небольшой список: адрес проверенной страницы, обнаруженный canonical, ожидаемый canonical, дата, окружение. Для учебной карточки ожидаемый результат — https://shop.example.ru/catalog/red-mug. Он подходит лишь при условии, что именно эта страница выбрана основной, доступна и содержит нужный товар.
Почему изменение metadataBase не помогает
В документации Metadata API для Next.js 15 описаны два разных случая: относительное значение URL дополняется через metadataBase, а уже абсолютное значение использует собственный адрес. Поэтому старый домен внутри alternates.canonical нужно исправить непосредственно в этом значении.
В учебном магазине root layout содержит такой фрагмент:
import type { Metadata } from 'next'
export const metadata: Metadata = {
metadataBase: new URL('https://shop.example.ru'),
}Это только экспорт метаданных: существующий компонент layout остаётся в файле. Сам по себе экспорт не меняет маршрутизацию, не создаёт редирект и не выбирает правильную карточку за приложение.
Теперь сравните два значения в метаданных страницы:
// Неправильно для нашего переехавшего магазина:
const oldCanonical = 'https://old-shop.example.ru/catalog/red-mug'
// Относительный путь дополняется публичным metadataBase:
const newCanonical = '/catalog/red-mug'Если CMS отдаёт oldCanonical, обновление layout оставит этот абсолютный URL прежним. Если metadataBase сам содержит неправильный домен, ошибка проявится в относительных значениях. Найдите обе настройки, прежде чем менять одну из них наугад.
Проверьте и структуру наследования. Родительский canonical для главной не должен случайно становиться canonical всех карточек. На каждой самостоятельной товарной странице нужно получить адрес, соответствующий её содержимому.
Соберите canonical из доверенной настройки сайта
В нашем примере slug уже проверен сервером при загрузке товара. Страница получает опубликованную карточку из каталога; неизвестный slug обрабатывается отдельно. Следующий фрагмент показывает только экспорт generateMetadata файла app/catalog/[slug]/page.tsx, а не готовую реализацию страницы:
import type { Metadata } from 'next'
type Props = { params: Promise<{ slug: string }> }
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
if (!/^[a-z0-9-]{1,80}$/.test(slug)) {
return { robots: { index: false, follow: false } }
}
return {
alternates: {
canonical: `/catalog/${encodeURIComponent(slug)}`,
},
}
}Для допустимого slug функция возвращает относительный путь; домен берётся из layout. В реальном магазине используйте canonical slug найденной записи, если входной адрес может быть алиасом. Проверка регулярным выражением подтверждает только формат, а существование и публичность товара должны проверяться по данным каталога.
Не собирайте публичный canonical из произвольного заголовка Host, параметра запроса или адреса, присланного посетителем. Публичный домен — доверенная настройка проекта. Если он хранится в переменной окружения, проверьте её отдельно в сборке и работающем процессе. Запишите ожидаемое значение для каждого релиза, чтобы старый домен не вернулся при следующем деплое.
Предпочтите базовый origin без вложенного пути, если магазин расположен в корне домена. Проект с базовым путём требует собственного теста формирования URL: механическое добавление строк часто приводит к удвоенному префиксу.
Разделите production, preview и локальную разработку
У окружений разные задачи. Публичный production должен отдавать правильные адреса основных страниц. Preview нужен для проверки будущих изменений и не должен автоматически открывать в поиск ещё неопубликованный каталог. Локальные адреса вообще не должны попадать в публичную sitemap.
Для одинаковой публичной карточки preview может показывать canonical production-версии, но это не заменяет отдельную политику закрытия тестового окружения. Приватный предпросмотр должен также защищаться доступом. Если содержимое отличается существенно, нельзя считать две страницы дублями только потому, что у них похожий путь.
Яндекс рассматривает canonical как рекомендацию и описывает причины, по которым она не учитывается, включая недоступную основную страницу, несколько значений и чужой домен. Поэтому случайный canonical на другой магазин нужно исправлять, а не ждать, что поисковик сам распознает замысел.
Проверьте robots отдельно: правильный canonical при сохранившемся noindex не делает страницу открытой для индексации. Поиск причины такого сочетания разобран в статье про наследование robots в Next.js.
Проверьте исправление на готовом релизе
Соберите и запустите production-версию проекта в согласованном окружении. Проверьте обычным GET главную, категорию, две существующие карточки, неизвестный товар и карточку с меткой перехода. Для каждого сохраните завершённый HTML и заголовки. Повторите запрос к публичному домену после деплоя: локальный результат не подтверждает конфигурацию CDN.
У самостоятельных опубликованных карточек должны быть собственные основные URL. Отсутствующий товар не должен притворяться существующей карточкой с canonical на главную. Для URL с маркетинговой меткой заранее согласуйте, какой чистый адрес считается основным; метка не должна случайно менять идентичность товара.
Дальше сравните canonical с внутренними ссылками и sitemap. Если шаблон карточки исправлен, а список товаров продолжает вести на старый домен, посетители и робот всё ещё получают противоречивый маршрут. В техническом аудите проверьте публичные страницы; сохраните точный охват отчёта, чтобы не переносить вывод одной карточки на весь каталог.
Поисковое решение проверяется отдельно после обхода в панелях вебмастеров. Успешная проверка HTML подтверждает исправленный сигнал, а не гарантирует немедленную смену выбранной страницы или рост позиций.
Что передать разработчику и как принять работу
Передайте список проблемных URL и ожидаемых canonical, HTML до исправления, сведения об окружении и место хранения публичного домена. Попросите найти источник абсолютного старого адреса, исправить формирование метаданных и проверить наследование на разных сегментах. Не формулируйте задачу как «поменять canonical везде на главную».
Приёмка включает уникальный корректный canonical проверенных карточек, доступность его цели, отсутствие лишних значений, согласованные robots и чистые публичные URL в sitemap. Добавьте проверку после следующего релиза: ошибки окружения часто возвращаются при пересборке.
Архитектуру своего проекта сопоставьте с разбором Next.js 15. Если используются Pages Router, сторонняя библиотека head или отдельная CMS-генерация ссылок, приведённый экспорт Metadata API нельзя переносить без адаптации. Сначала выясните, какой код действительно выпускает ошибочный элемент в конечный ответ.