Все статьиNext.js

Динамические OG-изображения в Next.js с помощью @vercel/og

Узнайте, как автоматизировать генерацию превью для соцсетей в Next.js App Router с помощью @vercel/og. Повысьте CTR ссылок — закажите разработку в EvaLab!

8 мин readОбновлено 28.08.2026
Динамические OG-изображения в Next.js с помощью @vercel/og — статья блога EvaLab

В современном веб-маркетинге привлекательное превью ссылки (Open Graph image) при шеринге в Telegram, WhatsApp, LinkedIn или VK является одним из главных факторов высокого показатель кликабельности (CTR). Создание статических картинок вручную для каждой статьи, товара или карточки услуг в Figma быстро становится узким местом при масштабировании B2B-портала или сервиса.

Динамическая генерация OG-изображений на лету в Next.js позволяет на 100% автоматизировать этот процесс. С помощью библиотеки @vercel/og вы можете верстать брендированные карточки прямо на JSX и Tailwind CSS, а сервер Edge Runtime будет мгновенно рендерить их в формате PNG. В этой статье команда EvaLab разберет полный цикл настройки динамических OG-обложек для Next.js App Router.

Зачем нужна динамическая генерация Open Graph (OG) изображений

Когда пользователь или поисковый бот (TelegramBot, VKShare, TwitterBot) запрашивает URL вашей страницы, он ищет meta-тег <meta property="og:image" content="..." />.

Если у вас интернет-магазин на 10 000 товаров или динамический блог с частыми публикациями, классический подход с подготовкой PNG-файлов дизайнерской командой не работает. Автоматическая генерация дает сразу несколько преимуществ:

  1. Рост CTR на 25–40%: ссылки выглядят профессионально, содержат актуальное название статьи, имя автора, категорию, цену товара и брендовые элементы.
  2. Персонализация под контекст: можно выводить текущую дату, бейджи («Скидка 20%», «Новинка») или аватарки спикеров.
  3. Экономия времени команды: разработчикам и контент-менеджерам больше не нужно создавать и загружать графические файлы на CDN.

Архитектура Edge Runtime и пакета @vercel/og

Исторически серверная генерация картинок на Node.js требовала тяжелых библиотек вроде puppeteer (с запуском headless-браузера Chromium) или Node-Canvas. Это приводило к долгим cold-стартам (от 2 до 5 секунд), колоссальному потреблению RAM и высокой стоимости инфраструктуры.

Пакет @vercel/og кардинально изменил подход. Под капотом он использует две легкие высокопроизводительные технологии:

  • Satori: движок от Vercel, который преобразует JSX-разметку и CSS-стили (Flexbox) в векторный формат SVG.
  • Resvg: сверхбыстрый Rust-компилятор (собранный в WebAssembly), который моментально конвертирует SVG в бинарный PNG-буфер.

Благодаря запуску в изолированном Edge Runtime время генерации одной динамической картинки с нуля составляет всего 30–60 миллисекунд, а размер используемой памяти не превышает нескольких мегабайт.

Пошаговая реализация OG Route Handler в Next.js App Router

Рассмотрим практический пример создания API-ручки генерации OG-изображения в Next.js (App Router).

Создадим файл app/api/og/route.tsx:

import { ImageResponse } from '@vercel/og';
import { NextRequest } from 'next/server';

export const runtime = 'edge';

export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url);

    // Получаем параметры из URL или задаем значения по умолчанию
    const title = searchParams.get('title') || 'Разработка цифровых продуктов';
    const category = searchParams.get('category') || 'EvaLab Tech';
    const readTime = searchParams.get('readTime') || '5 мин';

    return new ImageResponse(
      (
        <div
          style={{
            height: '100%',
            width: '100%',
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'flex-start',
            justifyContent: 'space-between',
            backgroundColor: '#0F172A',
            padding: '80px',
            fontFamily: 'sans-serif',
          }}
        >
          {/* Верхняя панель: Бренд и Категория */}
          <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
            <div
              style={{
                backgroundColor: '#3B82F6',
                color: '#FFFFFF',
                padding: '8px 20px',
                borderRadius: '20px',
                fontSize: 24,
                fontWeight: 600,
              }}
            >
              {category}
            </div>
            <span style={{ color: '#94A3B8', fontSize: 24 }}>• {readTime}</span>
          </div>

          {/* Заголовок статьи */}
          <div
            style={{
              display: 'flex',
              fontSize: 56,
              fontWeight: 800,
              color: '#F8FAFC',
              lineHeight: 1.2,
              letterSpacing: '-0.02em',
              maxWidth: '1000px',
            }}
          >
            {title}
          </div>

          {/* Футер: Логотип агентства */}
          <div
            style={{
              display: 'flex',
              width: '100%',
              justifyContent: 'space-between',
              alignItems: 'center',
              borderTop: '2px solid #1E293B',
              paddingTop: '32px',
            }}
          >
            <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
              <div
                style={{
                  width: '40px',
                  height: '40px',
                  borderRadius: '8px',
                  backgroundColor: '#3B82F6',
                }}
              />
              <span style={{ color: '#F8FAFC', fontSize: 28, fontWeight: 700 }}>
                EvaLab
              </span>
            </div>
            <span style={{ color: '#64748B', fontSize: 22 }}>
              evalab.site / b2b solutions
            </span>
          </div>
        </div>
      ),
      {
        width: 1200,
        height: 630,
      }
    );
  } catch (e: any) {
    return new Response(`Ошибка генерации изображения: ${e.message}`, {
      status: 500,
    });
  }
}

Интеграция динамических OG-картинок в generateMetadata

Чтобы Next.js автоматически связывал генерируемую обложку со страницей статьи, укажите путь к ручке в динамических метаданных страницы (app/blog/[slug]/page.tsx):

import { Metadata } from 'next';
import { getPostBySlug } from '@/lib/api';

type Props = {
  params: { slug: string };
};

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const post = await getPostBySlug(params.slug);

  const ogImageUrl = new URL('https://evalab.site/api/og');
  ogImageUrl.searchParams.set('title', post.title);
  ogImageUrl.searchParams.set('category', post.category);
  ogImageUrl.searchParams.set('readTime', post.readTime);

  return {
    title: post.title,
    description: post.description,
    openGraph: {
      title: post.title,
      description: post.description,
      type: 'article',
      images: [
        {
          url: ogImageUrl.toString(),
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.description,
      images: [ogImageUrl.toString()],
    },
  };
}

Кастомные шрифты и оптимизация производительности

По умолчанию Satori использует стандартный системный шрифт, который может не поддерживать кириллицу или не соответствовать вашему гайдлайну. Чтобы подключать фирменные шрифты (например, Inter или Montserrat), загружайте их через fetch и передавайте в опциях ImageResponse:

// Загрузка кастомного шрифта с поддержкой кириллицы
const fontData = await fetch(
  new URL('../../../assets/fonts/Inter-Bold.ttf', import.meta.url)
).then((res) => res.arrayBuffer());

return new ImageResponse(
  <div style={{ fontFamily: 'Inter' }}>...</div>,
  {
    width: 1200,
    height: 630,
    fonts: [
      {
        name: 'Inter',
        data: fontData,
        style: 'normal',
        weight: 700,
      },
    ],
    headers: {
      'Cache-Control': 'public, max-age=31536000, immutable',
    },
  }
);

[!TIP] Установка заголовка Cache-Control: public, max-age=31536000, immutable гарантирует, что CDN Vercel или Cloudflare закэширует сгенерированное изображение после первого запроса. Повторные шеринги статьи не будут нагружать Edge-функцию!

Ограничения Satori, о которых важно помнить

Хотя Satori позволяет верстать превью на JSX, оно поддерживает только ограниченное подмножество CSS-свойств:

  • Поддерживается только Flexbox (display: flex). CSS Grid и классический Block/Inline-block не поддерживаются.
  • Все стили лучше задавать инлайново в атрибуте style или использовать очищенный Tailwind CSS.
  • Анимации, псевдоклассы (:hover) и сложные видео-эффекты не поддерживаются.

Заключение

Динамическая генерация Open Graph картинок через @vercel/og — это современный стандарт веб-разработки для B2B-продуктов, медиа и сервисов на Next.js. Инструмент снижает ручную рутину и существенно повышает показатель кликабельности в социальных сетях и мессенджерах.

Хотите внедрить динамическую генерацию медиа-контента, оптимизировать SEO и выстроить высокопроизводительный стек на Next.js? Обратитесь в веб-агентство EvaLab — мы проектируем и разрабатываем надежные веб-приложения для бизнеса с гарантией высокого качества!

Следующий шаг

Превратим идею в рабочую систему

Разберём задачу, сопоставим её с целями бизнеса и предложим план внедрения с понятными этапами и критериями результата.