Обязательно использовать canonical в generateMetadata

Обязательно ли использовать canonical в generateMetadata? Разбираем на практике

Мета-тег canonical — это не просто «хорошая практика», а краеугольный камень SEO в современном вебе. Если вы работаете с Next.js и App Router, игнорирование этого тега — это прямое указание поисковым системам ранжировать ваш сайт хуже, чем он того заслуживает.

Что такое canonical и зачем он нужен?

Простыми словами: канонический URL — это главный, предпочтительный адрес страницы.

Представьте, что у вас есть продукт, который можно найти по разным путям:

  • https://store.com/products/awesome-widget
  • https://store.com/category/gadgets/awesome-widget
  • https://store.com/products/awesome-widget?utm_source=facebook

Для поисковых систем (Google, Yandex) это 3 разные страницы. Они могут начать ранжировать их отдельно, дробить вес (линковый и поведенческий) и, в худшем случае, посчитать это дублирующим контентом.

Канонический тег решает эту проблему. Он говорит поисковому роботу: «Эй, все эти адреса — одна и та же страница! Ранжируй вот этот, основной URL.»

<!-- Такой тег в <head> указывает на главную версию -->
<link rel="canonical" href="https://store.com/products/awesome-widget" />
```html

## Next.js App Router: Где и как это делать?

В старом Pages Router мы бы прописывали это в next/head. В современном App Router вся работа с метаданными сосредоточена в функции generateMetadata.

## Базовый пример: Статический canonical

Допустим, у нас страница app/products/page.tsx. Её канонический URL всегда один и тот же.


```tsx
// app/products/page.tsx
import { Metadata } from 'next';

export function generateMetadata(): Metadata {
  return {
    title: 'Все наши продукты',
    description: 'Лучшие продукты на рынке.',
    // Самое главное:
    alternates: {
      canonical: '/products'
    },
  };
}

export default function ProductsPage() {
  // ... JSX вашей страницы
}

Что здесь происходит? Next.js автоматически превратит относительный путь /products в абсолютный URL (например, https://yourdomain.com/products) и добавит тег <link rel="canonical" ... /> в <head> страницы.

Продвинутый пример: Динамический canonical (SSR/SSG)

На практике чаще всего canonical нужен для динамических страниц, например, страниц товаров или статей.

// app/blog/[slug]/page.tsx
import { Metadata, ResolvingMetadata } from 'next';

type Props = {
  params: { slug: string };
  searchParams: { [key: string]: string | string[] | undefined };
}

// Функция для получения данных (из вашего CMS или API)
async function getPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`);
  return res.json();
}

export async function generateMetadata(
  { params, searchParams }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {
  // Получаем данные поста
  const post = await getPost(params.slug);

  // Формируем полный абсолютный URL для canonical
  const canonicalUrl = `${process.env.NEXT_PUBLIC_SITE_URL}/blog/${post.slug}`;

  return {
    title: post.seo_title,
    description: post.meta_description,
    openGraph: {
      title: post.seo_title,
      description: post.meta_description,
      url: canonicalUrl, // Важно и для OG!
      // ... другие OG-теги
    },
    // Ключевой момент:
    alternates: {
      canonical: canonicalUrl,
    },
  };
}

export default function BlogPostPage({ params }: Props) {
  // ... React-компонент страницы
}

Ключевые моменты этого кода:

  1. process.env.NEXT_PUBLIC_SITE_URL: Мы используем переменную окружения для базового URL. Это MUST-HAVE для продакшена. Задайте её в .env.local:

NEXT_PUBLIC_SITE_URL=https://your-production-domain.com 2. Синхронизация с openGraph:url: Мы используем тот же canonicalUrl для OpenGraph-тега url. Это обеспечивает согласованность, когда ваша страница делится в соцсетях.

  1. Чистота URL: Обратите внимание, мы используем "чистый" URL без query-параметров (?utm_source=...). Именно это и должна делать canonical — отсекать служебные параметры.

Частые кейсы и ошибки

1. Страницы пагинации

Для страниц пагинации canonical должен вести на первую страницу, если контент дублируется.

// app/blog/page/[num]/page.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const pageNum = parseInt(params.num);

  return {
    alternates: {
      // Для первой страницы canonical ведет на себя, для остальных — на первую.
      canonical: pageNum === 1 ? '/blog' : `/blog/page/${pageNum}`,
    },
  };
}

2. Страницы с фильтрами и сортировкой

Это сложный случай. Если при применении фильтров контент страницы меняется кардинально (например, показываются товары другого типа), то такая страница может считаться уникальной и иметь свой собственный canonical.

Но! Если изменения незначительны или это просто сортировка одних и тех же товаров, лучшей практикой является указание canonical на URL без параметров фильтрации и сортировки. Это предотвращает дублирование контента.

	// app/products/page.tsx (с query-параметрами)
export async function generateMetadata({ searchParams }: Props): Promise<Metadata> {
  // Например, URL: /products?sort=price&brand=nike
  // Но основная версия — это /products

  return {
    alternates: {
      canonical: '/products', // Игнорируем параметры сортировки/фильтров
    },
  };
}
	

Резюме для разработчика

  • Canonical — это MUST-HAVE. Это директива, а не рекомендация. Без неё вы рискуете получить санкции за дублирующий контент и потерять в позициях.
  • В App Router используйте alternates.canonical внутри generateMetadata.
  • Всегда используйте абсолютные URL для динамических страниц, предварительно задав NEXT_PUBLIC_SITE_URL.
  • Держите canonical в чистоте: URL должен быть основным, без UTM-меток, сессионных ID и параметров сортировки, которые не меняют сущность контента.
  • Синхронизируйте alternates.canonical с openGraph.url.

Внедрение этой простой практики не займет много времени, но станет значительным вкладом в SEO-здоровье вашего Next.js-приложения. Не пренебрегайте ей.