App Router заменил next/head на типизированный Metadata API. Два способа: статический экспорт для фиксированных страниц и generateMetadata для динамических:
// app/layout.tsx — общие настройки сайта с шаблоном тайтла
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://example.com"),
title: {
template: "%s | Acme",
default: "Acme — студия веб-разработки",
},
description: "Описание по умолчанию, если страница не задала своё.",
};
// app/blog/[slug]/page.tsx — динамические метаданные для поста
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug);
if (!post) return { title: "Пост не найден" };
return {
title: post.title, // станет «Заголовок поста | Acme» через шаблон
description: post.excerpt,
};
}
Практические правила, которые реально влияют на позиции:
- Title: ~50–60 символов, главный ключ ближе к началу, уникальный для каждой страницы. Google переписывает плохие тайтлы — не давайте ему повода.
- Description: ~150–160 символов. Напрямую на ранжирование не влияет, но это ваш рекламный текст в выдаче — от него зависит CTR.
metadataBaseобязателен — без него относительные URL OG-картинок и canonical ломаются в проде.- Дедуплицируйте запрос между
generateMetadataи компонентом страницы через Reactcache()— иначе будете ходить в CMS дважды на каждый запрос.
import { cache } from "react";
export const getPost = cache(async (slug: string) => {
return fetchPostFromCMS(slug);
});
Чеклист:
-
metadataBaseзадан в корневом layout - Шаблон тайтла определён один раз, страницы задают только свою часть
- У каждой страницы уникальные title и description
- Страницы из CMS используют
generateMetadataс fetch, обёрнутым вcache() - Есть fallback-метаданные для 404 и отсутствующего контента