Как добавить метаданные и создавать OG-изображения

API метаданных можно использовать для определения метаданных вашего приложения с целью улучшения SEO и возможности расшаривания в интернете. Доступны следующие варианты:

  1. Статический объект metadata
  2. Динамическая функция generateMetadata
  3. Специальные конвенции файлов для добавления статических или динамически генерируемых фавиконов и OG-изображений.

Со всеми перечисленными вариантами Next.js автоматически сгенерирует соответствующие теги <head> для вашей страницы, которые можно проверить в инструментах разработчика браузера.

Стандартные поля

Есть два стандартных тега meta, которые всегда добавляются, даже если маршрут не определяет метаданные:

  • Тег meta charset устанавливает кодировку символов для сайта.
  • Тег meta viewport задает ширину и масштаб области просмотра для адаптации под разные устройства.
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

Остальные поля метаданных можно определить с помощью объекта Metadata (для статических метаданных) или функции generateMetadata (для генерируемых метаданных).

Статические метаданные

Чтобы определить статические метаданные, экспортируйте объект Metadata из статического файла layout.js или page.js. Например, чтобы добавить заголовок и описание для маршрута блога:

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Мой блог',
  description: '...',
}

export default function Page() {}
export const metadata = {
  title: 'Мой блог',
  description: '...',
}

export default function Page() {}

Полный список доступных опций можно найти в документации generateMetadata.

Генерируемые метаданные

Функция generateMetadata позволяет получать метаданные, зависящие от данных. Например, чтобы получить заголовок и описание для конкретной записи блога:

import type { Metadata, ResolvingMetadata } from 'next'

type Props = {
  params: Promise<{ slug: string }>
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}

export async function generateMetadata(
  { params, searchParams }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {
  const slug = (await params).slug

  // получаем информацию о записи
  const post = await fetch(`https://api.vercel.app/blog/${slug}`).then((res) =>
    res.json()
  )

  return {
    title: post.title,
    description: post.description,
  }
}

export default function Page({ params, searchParams }: Props) {}
export async function generateMetadata({ params, searchParams }, parent) {
  const slug = (await params).slug

  // получаем информацию о записи
  const post = await fetch(`https://api.vercel.app/blog/${slug}`).then((res) =>
    res.json()
  )

  return {
    title: post.title,
    description: post.description,
  }
}

export default function Page({ params, searchParams }) {}

Под капотом Next.js будет стримить метаданные отдельно от UI и вставлять их в HTML, как только они будут разрешены.

Мемоизация запросов данных

В некоторых случаях может потребоваться получать одни и те же данные как для метаданных, так и для самой страницы. Чтобы избежать дублирования запросов, можно использовать функцию React cache для мемоизации возвращаемого значения и выполнения запроса только один раз. Например, чтобы получить информацию о записи блога и для метаданных, и для страницы:

import { cache } from 'react'
import { db } from '@/app/lib/db'

// getPost будет использоваться дважды, но выполнится только один раз
export const getPost = cache(async (slug: string) => {
  const res = await db.query.posts.findFirst({ where: eq(posts.slug, slug) })
  return res
})
import { cache } from 'react'
import { db } from '@/app/lib/db'

// getPost будет использоваться дважды, но выполнится только один раз
export const getPost = cache(async (slug) => {
  const res = await db.query.posts.findFirst({ where: eq(posts.slug, slug) })
  return res
})
import { getPost } from '@/app/lib/data'

export async function generateMetadata({
  params,
}: {
  params: { slug: string }
}) {
  const post = await getPost(params.slug)
  return {
    title: post.title,
    description: post.description,
  }
}

export default async function Page({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug)
  return <div>{post.title}</div>
}
import { getPost } from '@/app/lib/data'

export async function generateMetadata({ params }) {
  const post = await getPost(params.slug)
  return {
    title: post.title,
    description: post.description,
  }
}

export default async function Page({ params }) {
  const post = await getPost(params.slug)
  return <div>{post.title}</div>
}

Файловые метаданные

Для метаданных доступны следующие специальные файлы:

Их можно использовать для статических метаданных или генерировать программно.

Фавиконы

Фавиконы — это маленькие иконки, представляющие ваш сайт в закладках и результатах поиска. Чтобы добавить фавикон в приложение, создайте файл favicon.ico в корневой папке приложения.

Специальный файл Favicon в папке App рядом с файлами layout и page

Фавиконы также можно генерировать программно. Подробнее см. в документации по фавиконам.

Статические Open Graph изображения

Open Graph (OG) изображения представляют ваш сайт в социальных сетях. Чтобы добавить статическое OG-изображение в приложение, создайте файл opengraph-image.png в корневой папке приложения.

Специальный файл OG-изображения в папке App рядом с файлами layout и page

Также можно добавить OG-изображения для конкретных маршрутов, создав файл opengraph-image.png глубже в структуре папок. Например, чтобы создать OG-изображение для маршрута /blog, добавьте файл opengraph-image.jpg в папку blog.

Специальный файл OG-изображения в папке blog

Более специфичное изображение будет иметь приоритет над любыми OG-изображениями выше в структуре папок.

Также поддерживаются другие форматы изображений, такие как jpeg, png и webp. Подробнее см. в документации по Open Graph изображениям.

Генерируемые Open Graph изображения

Конструктор ImageResponse позволяет генерировать динамические изображения с использованием JSX и CSS. Это полезно для OG-изображений, зависящих от данных.

Например, чтобы сгенерировать уникальное OG-изображение для каждой записи блога, добавьте файл opengraph-image.ts в папку blog и импортируйте конструктор ImageResponse из next/og:

import { ImageResponse } from 'next/og'
import { getPost } from '@/app/lib/data'

// Метаданные изображения
export const size = {
  width: 1200,
  height: 630,
}

export const contentType = 'image/png'

// Генерация изображения
export default async function Image({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug)

  return new ImageResponse(
    (
      // Элемент JSX для ImageResponse
      <div
        style={{
          fontSize: 128,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        {post.title}
      </div>
    )
  )
}
import { ImageResponse } from 'next/og'
import { getPost } from '@/app/lib/data'

// Метаданные изображения
export const size = {
  width: 1200,
  height: 630,
}

export const contentType = 'image/png'

// Генерация изображения
export default async function Image({ params }) {
  const post = await getPost(params.slug)

  return new ImageResponse(
    (
      // Элемент JSX для ImageResponse
      <div
        style={{
          fontSize: 128,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        {post.title}
      </div>
    )
  )
}

ImageResponse поддерживает общие CSS-свойства, включая flexbox и абсолютное позиционирование, пользовательские шрифты, перенос текста, центрирование и вложенные изображения. Полный список поддерживаемых CSS-свойств.

Полезно знать:

  • Примеры доступны в Vercel OG Playground.
  • ImageResponse использует @vercel/og, satori и resvg для преобразования HTML и CSS в PNG.
  • Поддерживаются только flexbox и подмножество CSS-свойств. Сложные макеты (например, display: grid) работать не будут.