У сучасному вебі соціальні мережі, пошукові краулери та месенджери (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 15 реалізує парадигму 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)
+-------------------------------------------------------------------------+
| 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 |
+-------------------------------------------------------------------------+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-кешування на рівні Edge 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,
}
);
}Satori не є повноцінним браузерним рушієм. Він підтримує лише обмежений набір правил CSS на основі рушія Yoga Flexbox. Спроба використати display: grid, float, псевдоелементи ::before/::after, складні CSS-селектори, анімації (@keyframes) або фільтри розмиття (backdrop-filter) призведе до збою компіляції або повного ігнорування стилів. Верстка для Satori має базуватися виключно на display: 'flex' з властивостями flexDirection, alignItems, justifyContent та інлайн-стилями.
В ізолятах Edge Runtime (Vercel Edge або Cloudflare Workers) відсутні локальні системні шрифти ОС (Arial чи Helvetica). Якщо в ImageResponse не передано масив fonts із валідним бінарним буфером (ArrayBuffer), Satori виконає мережевий запит до віддаленого сховища Noto Sans. Це спричинить значні затримки або викличе фатальну помилку Font loading failed при мережевих збоях. Завжди передавайте локальний файл шрифту TTF або WOFF через ArrayBuffer.
Ліміти на розмір серверних функцій в Edge Runtime зазвичай складають від 1 МБ до 4 МБ. Підключення повного TTF-шрифту з підтримкою всіх символів східних мов може збільшити розмір бандла на 10–20 МБ, заблокувавши деплой застосунку. Застосовуйте Subsetting шрифтів (залишаючи лише латиницю та кирилицю) або обирайте оптимізовані WOFF-файли розміром до 50–100 КБ.
Якщо не передати явний заголовок Cache-Control, кожен візит бота соціальної мережі чи пошукового павука призводитиме до повторного виконання коду ImageResponse, марнуючи ресурси процесора. Згенеровані OpenGraph-зображення для публікацій повинні мати сувору політику кешування: 'Cache-Control': 'public, immutable, no-transform, max-age=31536000'.
Краулер Facebook або Telegram запитує /blog/react-19/opengraph-image.
Edge-воркер паралельно отримує метадані статті та зчитує бінарні буфери шрифтів TTF/WOFF.
Рушій Yoga обчислює координати Flexbox-блоків і синтезує векторне SVG-дерево в пам'яті.
Модуль WebAssembly миттєво перетворює векторні контури та гліфи в растрові пікселі PNG.
Бінарний потік PNG повертається клієнту з заголовками immutable і осідає в Edge-кеші.