In modern web platforms, social networks, search engine crawlers, and messaging applications (Telegram, Slack, Twitter/X, Discord, LinkedIn, iMessage) parse OpenGraph (og:image, og:title) and Twitter Cards (twitter:image) metadata to construct interactive preview cards (Rich Social Snippets). A high-fidelity, contextual social preview directly amplifies user engagement and link attractiveness (CTR).
Historically, engineering teams relied on two contrasting approaches to generate dynamic preview graphics:
Next.js addresses this challenge through its built-in ImageResponse image generation subsystem (@vercel/og), powered by the Satori layout engine. Satori transforms JSX markup with a strictly defined subset of CSS Flexbox directly into vector SVG using the Yoga Layout Engine. The resulting SVG is immediately rasterized into an optimized PNG via the lightweight Resvg WebAssembly module. This entire rendering pipeline completes in under 20–50 milliseconds and requires only a few megabytes of memory, enabling near-instantaneous, personalized visual generation directly on Edge CDN nodes.
Next.js 15 adopts the File-Based Metadata paradigm — an automated filesystem convention where specially named files placed within route segments automatically compile into corresponding <head> meta tags in the generated HTML.
File-based metadata takes precedence over static metadata.openGraph.images configuration and is bound directly to its parent route segment.
+-------------------------------------------------------------------------+
| app/blog/[slug]/opengraph-image.tsx |
| JSX Card Template: Flexbox container, title, avatar, brand logo |
+-------------------------------------------------------------------------+
│
▼ (1. Parse markup & styles)
+-------------------------------------------------------------------------+
| Satori Engine (Yoga) |
| - Computes Flexbox layout geometry |
| - Embeds vector font glyphs (TTF / WOFF) |
| - Synthesizes valid in-memory SVG DOM tree |
+-------------------------------------------------------------------------+
│
▼ (2. Vector SVG Stream)
+-------------------------------------------------------------------------+
| Resvg WASM Core |
| - High-throughput vector curve rasterization into pixels |
| - Encodes compressed PNG output (1200 x 630 px) |
+-------------------------------------------------------------------------+
│
▼ (3. HTTP ImageResponse)
+-------------------------------------------------------------------------+
| HTTP/2 200 OK (Content-Type: image/png) |
| Cache-Control: public, immutable, max-age=31536000 |
+-------------------------------------------------------------------------+icon.tsx and apple-icon.tsx adjusting color schemes or notification badges based on themes or deployment environments.| File | Purpose | Generated HTML Tag |
| :--- | :--- | :--- |
| favicon.ico | Default browser tab icon | <link rel="icon" href="/favicon.ico" sizes="any" /> |
| icon.(ico\|jpg\|png\|svg\|tsx) | Configurable application favicon (code-generated) | <link rel="icon" href="/icon?<hash>" type="image/..." sizes="..." /> |
| apple-icon.(png\|jpg\|tsx) | Apple iOS home screen touch icon | <link rel="apple-touch-icon" href="/apple-icon?<hash>" sizes="..." /> |
| opengraph-image.(png\|jpg\|tsx) | Social preview card for OpenGraph protocol | <meta property="og:image" content="https://.../opengraph-image?<hash>" /> |
| twitter-image.(png\|jpg\|tsx) | Dedicated Twitter / X summary large image card | <meta name="twitter:image" content="https://.../twitter-image?<hash>" /> |
| manifest.(json\|webmanifest) | Progressive Web App (PWA) manifest | <link rel="manifest" href="/manifest.webmanifest" /> |
app/blog/[slug]/opengraph-image.tsx)#In opengraph-image.tsx, export configuration constants (size, contentType, alt, runtime) alongside a default export function that returns an ImageResponse instance.
import { ImageResponse } from 'next/og';
import { getPostBySlug } from '@/lib/data/posts';
// 1. Runtime and image dimension configuration
export const runtime = 'edge'; // Execute across global Vercel/Cloudflare Edge nodes
export const alt = 'Fullstack Agentic Lab Publication Preview';
export const size = {
width: 1200,
height: 630,
};
export const contentType = 'image/png';
interface Props {
params: Promise<{
slug: string;
}>;
}
// 2. Primary dynamic image handler
export default async function Image({ params }: Props) {
const { slug } = await params;
const post = await getPostBySlug(slug);
const title = post?.title || 'Engineering Web Platforms';
const category = post?.category || 'Next.js 15';
const authorName = post?.author?.name || 'Fullstack Agentic Lab Team';
// 3. Load custom font data into a binary buffer (ArrayBuffer)
// Satori does not have access to OS system fonts inside the 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(
(
// Satori root Flexbox container (JSX with inline styles)
<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',
}}
>
{/* Header bar: logo and category badge */}
<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>
{/* Center content: publication headline */}
<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>
{/* Footer bar: author and domain attribution */}
<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,
}}
>
Author: {authorName}
</span>
<span
style={{
fontSize: '22px',
color: '#71717a',
}}
>
agenticlab.dev
</span>
</div>
</div>
),
{
...size,
fonts: [
{
name: 'Inter',
data: fontData,
style: 'normal',
weight: 700,
},
],
// CDN edge caching policy
headers: {
'Cache-Control': 'public, immutable, no-transform, max-age=31536000',
},
}
);
}app/icon.tsx)#Next.js also allows dynamic generation of application favicons, adjusting brand colors or badge status depending on runtime parameters:
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 is not a full-featured browser engine. It implements only a constrained CSS subset based on the Yoga Flexbox engine. Attempting to use display: grid, float, pseudo-elements (::before, ::after), complex CSS selectors, keyframe animations, or graphical filter properties (backdrop-filter, filter) will either trigger compilation errors or be discarded silently. Layouts designed for Satori must strictly adhere to display: 'flex' with standard properties: flexDirection, alignItems, justifyContent, and explicit inline styles.
Vercel Edge Runtime and Cloudflare Workers do not provide access to host operating system fonts (such as Arial, Helvetica, or Roboto). If ImageResponse is executed without a populated fonts array containing a valid font binary (ArrayBuffer), Satori will attempt to fetch Google's Noto Sans over external networks. This induces extreme latency and risks a catastrophic Font loading failed error on intermittent network failures. Always bundle and load explicit TTF or WOFF font assets.
Edge Runtime functions impose strict bundle limits, typically between 1 MB and 4 MB. Bundling an unsubsetted TrueType font with full coverage for CJK and Cyrillic character sets can easily balloon the bundle by 10–20 MB, causing deployment failures. Always apply Font Subsetting (retaining only target character ranges) or use pre-subsetted WOFF files under 50–100 KB.
Without an explicit Cache-Control header, every crawler or social media bot accessing an image URL will trigger full re-execution of the ImageResponse route, exhausting serverless CPU quotas. Generated OpenGraph images for immutable articles should declare an immutable cache policy: 'Cache-Control': 'public, immutable, no-transform, max-age=31536000'.
A crawler from Facebook or Telegram requests /blog/react-19/opengraph-image.
The edge worker concurrently fetches article metadata and loads font binary buffers.
The Yoga engine calculates Flexbox coordinates and constructs an in-memory vector SVG tree.
The WebAssembly module instantly compiles vector paths and glyphs into PNG bitmap pixels.
The binary PNG response is returned with immutable headers and cached across global Edge PoPs.