В современном веб-пространстве социальные сети, поисковые боты и мессенджеры (Telegram, Slack, Twitter/X, Discord, LinkedIn, iMessage) считывают разметку OpenGraph (og:image, og:title) и Twitter Cards (twitter:image) для построения интерактивных превью-карточек (Rich Social Snippets). Качественное превью напрямую повышает метрику привлекательности ссылок (CTR).
Исторически существовало два подхода к созданию таких изображений:
Архитектура Next.js решает эту задачу с помощью встроенной подсистемы генерации изображений ImageResponse (библиотека @vercel/og), базирующейся на движке Satori. Satori транслирует JSX-разметку с ограниченным подмножеством CSS Flexbox непосредственно в векторный формат SVG, используя вычислительный движок Yoga Layout Engine. Полученный SVG-вектор моментально растеризуется в оптимизированный PNG через легковесный WebAssembly-модуль Resvg. Весь цикл занимает менее 20–50 миллисекунд и потребляет считанные мегабайты памяти, что позволяет генерировать персонализированную графику прямо на Edge CDN.
Next.js реализует парадигму File-based Metadata — автоматическое сопоставление файлов специального наименования в дереве маршрутов с соответствующими HTML-тегами секции <head>.
Файловые метаданные имеют более высокий приоритет по сравнению со статическим экспортом metadata.openGraph.images и автоматически связываются с сегментом маршрута.
+-------------------------------------------------------------------------+
| app/blog/[slug]/opengraph-image.tsx |
| JSX-шаблон карточки: Flexbox-контейнер, заголовок, аватар, логотип |
+-------------------------------------------------------------------------+
│
▼ (1. Парсинг разметки)
+-------------------------------------------------------------------------+
| Satori Engine (Yoga) |
| - Расчет Flexbox геометрии |
| - Встраивание векторных глифов шрифтов (TTF / WOFF) |
| - Генерация валидного SVG-дерева в памяти |
+-------------------------------------------------------------------------+
│
▼ (2. Векторный SVG Stream)
+-------------------------------------------------------------------------+
| Resvg WASM Core |
| - Высокоскоростная растеризация векторов в пиксели |
| - Сжатие в формат PNG (1200 x 630 px) |
+-------------------------------------------------------------------------+
│
▼ (3. HTTP ImageResponse)
+-------------------------------------------------------------------------+
| HTTP/2 200 OK (Content-Type: image/png) |
| Cache-Control: public, immutable, max-age=31536000 |
+-------------------------------------------------------------------------+Краулер Facebook или Telegram запрашивает /blog/react-19/opengraph-image.
Воркер параллельно подгружает параметры статьи и бинарные буферы TTF/WOFF шрифтов.
Движок Yoga производит расчет Flexbox-координат и строит векторное дерево разметки SVG.
Модуль WebAssembly мгновенно компилирует векторные кривые и глифы в растровые пиксели PNG.
Бинарный PNG отдается клиенту с заголовком immutable и оседает в глобальном Edge кэше.
Каким образом ImageResponse (@vercel/og) формирует бинарный PNG-файл из React-компонента без запуска Headless Chrome?
icon.tsx и apple-icon.tsx с динамическим изменением цвета или индикаторов уведомлений в зависимости от окружения или темы.| Файл | Назначение | Генерируемый HTML-тег |
| :--- | :--- | :--- |
| favicon.ico | Базовая иконка вкладки браузера | <link rel="icon" href="/favicon.ico" sizes="any" /> |
| icon.(ico\|jpg\|png\|svg\|tsx) | Настраиваемый фавикон (допустим код) | <link rel="icon" href="/icon?<hash>" type="image/..." sizes="..." /> |
| apple-icon.(png\|jpg\|tsx) | Иконка домашнего экрана Apple iOS | <link rel="apple-touch-icon" href="/apple-icon?<hash>" sizes="..." /> |
| opengraph-image.(png\|jpg\|tsx) | Карточка для OpenGraph сетей | <meta property="og:image" content="https://.../opengraph-image?<hash>" /> |
| twitter-image.(png\|jpg\|tsx) | Карточка для Twitter / X | <meta name="twitter:image" content="https://.../twitter-image?<hash>" /> |
| manifest.(json\|webmanifest) | Манифест Progressive Web App (PWA) | <link rel="manifest" href="/manifest.webmanifest" /> |
app/blog/[slug]/opengraph-image.tsx)#В файле opengraph-image.tsx экспортируются конфигурационные константы (size, contentType, alt, runtime) и функция по умолчанию, возвращающая экземпляр ImageResponse.
import { ImageResponse } from 'next/og';
import { getPostBySlug } from '@/lib/data/posts';
// 1. Конфигурация рантайма и метаданных изображения
export const runtime = 'edge'; // Запуск на глобальных Edge-нодах Vercel/Cloudflare
export const alt = 'Превью публикации Fullstack Agentic Lab';
export const size = {
width: 1200,
height: 630,
};
export const contentType = 'image/png';
interface Props {
params: Promise<{
slug: string;
}>;
}
// 2. Основной генератор изображения
export default async function Image({ params }: Props) {
const { slug } = await params;
const post = await getPostBySlug(slug);
const title = post?.title || 'Инженерная веб-разработка';
const category = post?.category || 'Next.js 15';
const authorName = post?.author?.name || 'Fullstack Agentic Lab Team';
// 3. Загрузка кастомного шрифта в бинарный буфер (ArrayBuffer)
// Satori не имеет доступа к локальным системным шрифтам в Edge Runtime
const fontData = await fetch(
new URL('@/assets/fonts/Inter-Bold.ttf', import.meta.url)
).then((res) => {
if (!res.ok) throw new Error('Failed to load font');
return res.arrayBuffer();
});
return new ImageResponse(
(
// Корневой Flexbox-контейнер Satori (JSX с инлайн-стилями)
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'space-between',
backgroundColor: '#09090b',
backgroundImage: 'radial-gradient(circle at 25px 25px, #27272a 2%, transparent 0%)',
backgroundSize: '40px 40px',
padding: '80px',
fontFamily: 'Inter',
}}
>
{/* Верхняя панель: логотип и бейдж раздела */}
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
}}
>
<div
style={{
width: '40px',
height: '40px',
borderRadius: '8px',
backgroundColor: '#06b6d4',
}}
/>
<span
style={{
fontSize: '28px',
fontWeight: 800,
color: '#ffffff',
letterSpacing: '-0.03em',
}}
>
Fullstack Agentic Lab
</span>
</div>
<div
style={{
display: 'flex',
padding: '8px 20px',
borderRadius: '9999px',
backgroundColor: 'rgba(6, 182, 212, 0.1)',
border: '1px solid rgba(6, 182, 212, 0.3)',
color: '#06b6d4',
fontSize: '18px',
fontWeight: 700,
textTransform: 'uppercase',
}}
>
{category}
</div>
</div>
{/* Центральный блок: заголовок статьи */}
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '16px',
maxWidth: '1000px',
}}
>
<h1
style={{
fontSize: '64px',
fontWeight: 800,
color: '#ffffff',
lineHeight: 1.15,
letterSpacing: '-0.04em',
}}
>
{title}
</h1>
</div>
{/* Нижняя панель: автор и платформа */}
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
borderTop: '1px solid #27272a',
paddingTop: '32px',
}}
>
<span
style={{
fontSize: '22px',
color: '#a1a1aa',
fontWeight: 600,
}}
>
Автор: {authorName}
</span>
<span
style={{
fontSize: '22px',
color: '#71717a',
}}
>
agenticlab.dev
</span>
</div>
</div>
),
{
...size,
fonts: [
{
name: 'Inter',
data: fontData,
style: 'normal',
weight: 700,
},
],
// Тонкая настройка заголовков HTTP-кэширования на CDN
headers: {
'Cache-Control': 'public, immutable, no-transform, max-age=31536000',
},
}
);
}app/icon.tsx)#Next.js позволяет программно генерировать иконку сайта, адаптируя ее под тему оформления или динамические параметры:
import { ImageResponse } from 'next/og';
export const runtime = 'edge';
export const size = { width: 32, height: 32 };
export const contentType = 'image/png';
export default function Icon() {
return new ImageResponse(
(
<div
style={{
fontSize: 20,
background: '#09090b',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#06b6d4',
borderRadius: '6px',
border: '1px solid #27272a',
fontWeight: 900,
}}
>
W
</div>
),
{
...size,
}
);
}Почему при генерации карточки в opengraph-image.tsx попытка применить стиль display: 'grid' или подключить шрифт в формате WOFF2 вызовет сбой?
Satori не является полноценным браузерным движком. Он реализует только подмножество CSS на базе Yoga Flexbox. Попытка использовать display: grid, float, псевдоэлементы ::before, сложные CSS-селекторы, анимации (keyframes) или фильтры (backdrop-filter) приведет к ошибке компиляции или полному игнорированию этих свойств. Верстка для Satori обязана строиться строго на display: 'flex' с базовыми свойствами flexDirection, alignItems, justifyContent и инлайн-стилями.
В среде Vercel Edge Runtime или Cloudflare Workers отсутствуют предустановленные системные шрифты (Arial, Helvetica, Roboto). Если в ImageResponse не передан массив fonts с валидным бинарным буфером шрифта (ArrayBuffer), Satori попытается загрузить дефолтный шрифт Noto Sans через внешний сетевой запрос, что резко замедлит генерацию или приведет к ошибке Font loading failed при сбоях сети. Всегда передавайте локальный шрифт формата TTF или WOFF.
Лимит размера функции в Edge Runtime обычно составляет от 1 МБ до 4 МБ. Если подключить полный шрифт TTF со всеми глифами китайского, японского и арабского языков (размер которого может достигать 10–20 МБ), деплой приложения завершится ошибкой превышения квоты бандла. Применяйте Subsetting шрифтов (оставляя только базовую латиницу и кириллицу) или отдавайте предпочтение WOFF-шрифтам размером до 50–100 КБ.
Если не указать заголовок Cache-Control, при каждом запросе робота социальной сети или поискового краулера Next.js будет повторно исполнять код ImageResponse, расходуя вычислительные ресурсы и процессорное время. Сгенерированные OpenGraph-изображения для неизменяемых статей должны иметь строгую политику кэширования: 'Cache-Control': 'public, immutable, no-transform, max-age=31536000'.