1. Введение
Статические метаданные — это круто, когда у вас обычные страницы вроде "О нас" или "Контакты". Но что делать, если у вас интернет-магазин с сотнями товаров? Или блог, где каждая статья должна иметь свой уникальный заголовок и описание для поисковиков? Или, например, у вас есть страница профиля пользователя, и хочется, чтобы в <title> было его имя?
Вот тут и появляется задача: как сделать так, чтобы метаданные (title, description, open graph и т.д.) подставлялись автоматически, исходя из параметров страницы или данных из базы?
Ответ: Использовать функцию generateMetadata() в Next.js 15.
Как работает generateMetadata()
generateMetadata() — это специальная асинхронная функция, которую вы можете экспортировать из файла страницы (page.tsx) или layout-компонента (layout.tsx). Она вызывается Next.js на сервере при генерации страницы и возвращает объект метаданных, аналогичный тому, который вы бы описали статически.
Главное отличие:
Внутри generateMetadata() можно получать параметры маршрута, делать запросы к базе или API, использовать любые вычисления и возвращать метаданные "на лету".
Сигнатура
export async function generateMetadata(
props: { params: Record<string, string>, searchParams: Record<string, string> }
): Promise<Metadata> {
// ...
}
Пояснение:
- params — параметры динамического маршрута (например, [id]).
- searchParams — query-параметры из URL (например, ?page=2).
- Возвращаемый объект должен соответствовать типу Metadata (тот же, что для статического metadata).
2. Пример: Динамический title для страницы товара
Давайте представим, что у нас интернет-магазин, и есть страница товара по адресу /product/[id]. Для SEO хочется, чтобы title был вида "Купить [название товара] — Магазин".
Шаг 1: Структура проекта
app/
product/
[id]/
page.tsx
Шаг 2: Пример функции generateMetadata
// app/product/[id]/page.tsx
import type { Metadata } from 'next';
// Фейковая функция для примера — обычно тут будет запрос к базе или API
async function getProductById(id: string) {
// В реальном приложении тут был бы fetch или DB-запрос
const products = {
'1': { name: 'Смартфон XPhone 42', description: 'Лучший смартфон года!' },
'2': { name: 'Ноутбук UltraBook', description: 'Мощный ноутбук для работы и игр.' },
};
return products[id] || { name: 'Неизвестный товар', description: '' };
}
// Это и есть динамическая генерация метаданных
export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> {
const product = await getProductById(params.id);
return {
title: `Купить ${product.name} — Магазин`,
description: product.description,
openGraph: {
title: `Купить ${product.name} — Магазин`,
description: product.description,
// Можно добавить картинку и другие OG-поля
},
};
}
// Основной компонент страницы
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProductById(params.id);
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
</main>
);
}
Что происходит:
- При заходе на /product/1 Next.js вызывает generateMetadata, передает туда { params: { id: '1' } }.
- Функция получает данные товара (пусть даже фейковые), формирует title и description на основе этих данных.
- SEO-роботы, соцсети и браузеры увидят корректные метаданные.
3. Использование searchParams в generateMetadata
Иногда нужно подставлять метаданные в зависимости от query-параметров, например, для фильтрации или пагинации.
export async function generateMetadata({
params,
searchParams,
}: {
params: { id: string },
searchParams: { page?: string }
}): Promise<Metadata> {
const page = searchParams.page || '1';
return {
title: `Товар ${params.id} — страница ${page}`,
description: `Описание товара ${params.id}, страница ${page}`,
};
}
4. Динамическая генерация Open Graph и других метаданных
Метаданные — это не только title и description. Можно динамически подставлять картинки для соцсетей, авторов, даты публикации и всё, что поддерживает объект Metadata.
export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> {
const product = await getProductById(params.id);
return {
title: `Купить ${product.name} — Магазин`,
description: product.description,
openGraph: {
title: `Купить ${product.name} — Магазин`,
description: product.description,
images: [`https://example.com/images/products/${params.id}.jpg`],
type: 'product',
},
twitter: {
card: 'summary_large_image',
title: `Купить ${product.name}`,
description: product.description,
images: [`https://example.com/images/products/${params.id}.jpg`],
},
};
}
Где можно использовать generateMetadata
- В файле страницы (page.tsx) — для генерации метаданных, специфичных для конкретной страницы.
- В layout.tsx — для генерации метаданных, которые касаются целого раздела (например, всех страниц профиля пользователя).
- Можно использовать одновременно в layout и page — метаданные будут умно объединяться (page "перебивает" layout).
5. Практика: Динамический title для блога
Представим, что у нас есть блог, и каждый пост доступен по адресу /blog/[slug]. Нам нужно, чтобы title страницы был названием статьи, а description — её аннотацией.
// app/blog/[slug]/page.tsx
// Фейковая функция для примера
async function getPostBySlug(slug: string) {
const posts = {
'nextjs-15': { title: 'Что нового в Next.js 15', excerpt: 'Обзор всех фич новой версии.' },
'react-hooks': { title: 'React Hooks: полное руководство', excerpt: 'Учимся использовать хуки по-взрослому.' },
};
return posts[slug] || { title: 'Пост не найден', excerpt: '' };
}
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
const post = await getPostBySlug(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
},
};
}
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
return (
<article>
<h1>{post.title}</h1>
<p>{post.excerpt}</p>
</article>
);
}
6. Особенности и нюансы динамических метаданных
- Асинхронность
generateMetadata() может быть асинхронной: вы можете делать fetch, обращаться к базе, читать файлы, всё что угодно. Но помните: чем дольше работает эта функция, тем дольше генерируется страница! - Кеширование
- По умолчанию Next.js кеширует результат generateMetadata() для одинаковых параметров (например, для /product/1).
- Если вы хотите отключить кеширование (например, если данные часто меняются), используйте опцию cache: "no-store" в fetch или настройте revalidate.
- Ошибки
Если внутри generateMetadata() произойдет ошибка (например, не найден товар), можно вернуть дефолтные метаданные, чтобы не "ломать" страницу. - Использование в Server Components
generateMetadata() всегда вызывается на сервере, даже если страница клиентская ("use client"). Это значит, что можно безопасно обращаться к серверным ресурсам.
7. Типичные ошибки при работе с generateMetadata
Ошибка №1: Путают статический объект metadata и функцию generateMetadata
Часто новички пытаются одновременно объявить и объект export const metadata = {...} и функцию export async function generateMetadata(). В этом случае Next.js отдаёт приоритет функции, а объект игнорирует. Не стоит дублировать оба варианта, выберите тот, который вам нужен.
Ошибка №2: Отсутствие обработки несуществующих данных
Если вы делаете fetch по параметру, а данных нет (например, товара с таким id не существует), не возвращайте undefined — это вызовет ошибку. Лучше вернуть дефолтные метаданные: title: "Товар не найден".
Ошибка №3: Использование client-only данных
Пытаются использовать внутри generateMetadata что-то, что есть только на клиенте (например, window, localStorage). Не работает! Всё, что внутри generateMetadata, должно быть серверным.
Ошибка №4: Слишком долгие запросы
Если в generateMetadata делаете очень медленный запрос, пользователи будут ждать дольше загрузки страницы. Старайтесь оптимизировать эти запросы или использовать кеширование.
Ошибка №5: Неправильная работа с params и searchParams
Иногда путают параметры маршрута (params) и query-параметры (searchParams). Например, если динамический сегмент называется [slug], то его значение будет в params.slug, а не в searchParams.slug.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ