Skip to main content
Назад к блогу
5 мин чтения

Генерация OG-изображений в Next.js 16 без бессонных ночей

Динамические OG-изображения делают ссылки профессиональными в Twitter и Slack. Вот сетап, который работает в продакшне без сюрпризов — и подходы, от которых я отказался.

next.jsperformance

Когда кто-то шерит ссылку в Twitter, Slack или LinkedIn — OG-изображение первое, что видят. Общий фоллбэк говорит «мне было лень». Хорошо сгенерированное динамическое изображение говорит «это реальный продукт, который поддерживает кто-то, кому не всё равно».

Next.js имеет встроенную генерацию OG-изображений через next/og. Работает. Но дефолтные туториалы пропускают продакшн-подводные камни. Вот сетап, который я использую после того, как потратил время на три разных подхода.

Подход, который работает: next/og с ImageResponse

Next.js предоставляет ImageResponse из next/og, который рендерит JSX в PNG по запросу. Работает на Edge-рантайме и использует Satori (движок лейаутов, который конвертирует подмножество HTML/CSS в SVG, потом в PNG).

// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'

export const runtime = 'edge'
export const alt = 'Заголовок поста'
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(
    <div
      style={{
        width: '100%',
        height: '100%',
        display: 'flex',
        flexDirection: 'column',
        justifyContent: 'center',
        padding: '60px',
        background: 'linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 100%)',
        color: 'white',
        fontFamily: 'Inter',
      }}
    >
      <div style={{ fontSize: 48, fontWeight: 700, lineHeight: 1.2 }}>
        {post.title}
      </div>
      <div style={{ fontSize: 24, opacity: 0.7, marginTop: 20 }}>
        yoursite.com
      </div>
    </div>,
    { ...size }
  )
}

Почему работает: Next.js автоматически обнаруживает файлы opengraph-image.tsx и добавляет правильный <meta property="og:image"> тег. Никакой ручной привязки. Изображение генерируется при сборке для статических страниц (SSG) или по запросу для динамических.

Кастомные шрифты

Дефолтный системный шрифт выглядит дженериково. Кастомный шрифт делает OG-изображения соответствующими бренду. Нюанс: Satori не читает .woff2. Нужен .ttf или .otf.

export default async function Image({ params }) {
  const fontData = await fetch(
    new URL('../../assets/Inter-Bold.ttf', import.meta.url)
  ).then((res) => res.arrayBuffer())

  return new ImageResponse(
    (/* JSX */),
    {
      ...size,
      fonts: [
        {
          name: 'Inter',
          data: fontData,
          style: 'normal',
          weight: 700,
        },
      ],
    }
  )
}

Подводный камень: fetch с относительным URL работает в разработке, но может упасть в продакшне, если файл не включён в edge-бандл. Положи файл шрифта в ту же директорию или используй абсолютный URL на CDN-копию.

Размер имеет значение: TTF-файл в 500КБ загружается при каждом запросе OG-изображения. Обрежь шрифт до нужных символов (латиница + кириллица, если поддерживаешь эти локали). Я использую pyftsubset из fonttools — обычно режет файл с 500КБ до 50КБ.

Подмножество CSS

Satori поддерживает подмножество CSS. Вот что ловит людей:

Поддерживается: display: flex, flexDirection, justifyContent, alignItems, padding, margin, border, borderRadius, background, color, fontSize, fontWeight, lineHeight, opacity, position, top/left/right/bottom, overflow: hidden, линейные градиенты.

Не поддерживается: grid, gap (используй margin), box-shadow, CSS-переменные, transform, анимации, text-overflow: ellipsis (обрезай в JS).

Если попробуешь неподдерживаемый CSS — Satori молча игнорирует. Изображение рендерится без стиля — никакой ошибки, просто лейаут выглядит неправильно.

Мой подход: держи лейауты OG-изображений максимально простыми. Один flex столбец, текст, может логотип или акцентная полоса. Если борешься с CSS-поддержкой Satori — дизайн слишком сложный для превью 1200×630.

Обрезка текста

Длинные заголовки ломают лейаут. Satori не поддерживает text-overflow: ellipsis, поэтому обрезай в коде:

function truncate(str: string, maxLength: number) {
  if (str.length <= maxLength) return str
  return str.slice(0, maxLength - 1) + '…'
}

// В JSX:
;<div style={{ fontSize: 48 }}>{truncate(post.title, 80)}</div>

80 символов — безопасный максимум для шрифта 48px при ширине 1200px с отступами 60px с каждой стороны. Тестируй со своим шрифтом — ширина символов отличается.

Кэширование

OG-изображения дорого генерировать (50–200мс на запрос). Кэшируй агрессивно.

Для статических страниц (SSG) Next.js генерирует изображение при сборке. Нулевая стоимость рантайма.

Для динамических страниц добавь заголовки кэша:

export const revalidate = 86400 // Кэш на 24 часа

Или если используешь generateStaticParams — OG-изображения для этих путей генерируются при сборке автоматически.

Ловушка кэша Slack: Slack агрессивно кэширует OG-изображения. Если обновишь изображение и перешеришь ссылку — Slack покажет старое. Нет способа форсировать обновление с твоей стороны — Slack перезапрашивает по своему расписанию. Не паникуй, когда обновлённое изображение не появляется сразу.

Подходы, от которых я отказался

Скриншоты Puppeteer/Playwright: Поднять headless-браузер, загрузить HTML-страницу, сделать скриншот. Работает, но медленно (2–5 секунд на изображение), требует бинарник браузера в деплое, и проблемы с памятью на масштабе. Использовал на двух проектах до появления next/og. Не вернусь.

Внешние сервисы (Cloudinary, Bannerbear): Работают хорошо, но добавляют зависимость и стоимость за изображение. Для портфолио или маленького SaaS с сотнями страниц next/og бесплатен и держит всё в кодовой базе.

Статический фоллбэк: Одно дженериковое изображение для всех страниц. Лучше, чем ничего, но динамическое изображение с заголовком страницы получает значительно больше кликов. Настройка next/og — 30 минут, улучшение CTR — перманентное.

Минимальный сетап

Если хочешь максимально простой OG-сетап, который всё равно выглядит профессионально:

  1. Один opengraph-image.tsx в директории [slug] блога
  2. Тёмный градиентный фон, белый текст, URL сайта внизу
  3. Кастомный шрифт (обрезанный, <50КБ)
  4. Обрезка заголовка на 80 символах
  5. 24-часовая ревалидация для динамических страниц

Это 40 строк кода. Покрывает каждый пост блога, каждую страницу, и делает каждую пошеренную ссылку намеренной, а не случайной.


Хочешь, чтобы Next.js-сайт выглядел отполированно везде, где его шерят? Напиши — OG-изображения — часть базового технического SEO, которое я шиплю на каждом проекте.