Automaticky generované OG obrázky v Next.js
Sdílíte odkaz na sociální sítě a místo náhledu se zobrazí prázdný šedý čtverec. Znáte to. OpenGraph obrázky jsou tou malou, neviditelnou prací, která rozhoduje, jestli na váš odkaz někdo klikne nebo ho přejede.
Next.js nabízí elegantní řešení: soubor opengraph-image.tsx přímo ve složce routy. Žádná externí služba, žádný headless prohlížeč, žádný puppeteer v závislosti. Čistý React → PNG.
Jak to funguje
Next.js při nalezení souboru opengraph-image.tsx v route segmentu automaticky:
- Zaregistruje route
/[segment]/opengraph-image - Nastaví
<meta property="og:image">v<head>dané stránky - Přidá Twitter Card meta tagy
Pod kapotou běží knihovna Satori — engine vyvinutý Vercelem, který přeloží podmnožinu React JSX a CSS flexbox do SVG. SVG pak převede na PNG přes resvg-wasm.
// src/app/[locale]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export default function Image() {
return new ImageResponse(
<div style={{ background: '#1d1c19', width: '100%', height: '100%' }}>
Obsah obrázku jako JSX
</div>
)
}
Fonty
Satori nemůže použít systémové fonty ani next/font. Fonty musíte načíst ručně jako ArrayBuffer z TTF nebo OTF souboru:
import { readFile } from 'fs/promises'
import { join } from 'path'
const font = await readFile(join(process.cwd(), 'public/fonts/Geist-Medium.ttf'))
return new ImageResponse(<div>text</div>, {
fonts: [{ name: 'Geist', data: font, weight: 500 }],
})
Proto má tento projekt složku public/fonts/ s TTF soubory — používají je výhradně OG obrázky.
Architektura OG obrázků tohoto blogu
Projekt má tři úrovně OG obrázků, každý ve vlastním route segmentu:
src/app/[locale]/
opengraph-image.tsx ← portfolio homepage
blog/
opengraph-image.tsx ← blog index (seznam příspěvků)
[slug]/
opengraph-image.tsx ← šablona pro každý post
Next.js se řídí zásadou nejbližší soubor vyhraje — post stránka použije [slug]/opengraph-image.tsx, blog index použije blog/opengraph-image.tsx, homepage použije kořenový soubor.
Šablona příspěvku
Soubor [slug]/opengraph-image.tsx je univerzální šablona. Přijme params: { locale, slug }, načte data příspěvku a vykreslí obrázek:
export default async function Image({ params }) {
const { locale, slug } = await params
// 1. Načíst lokalizované řetězce
const { default: m } = await import(`@/i18n/messages/${locale}.json`)
// 2. Načíst data příspěvku ze souborového systému
const post = getPostBySlug(locale, slug)
// 3. Dynamická velikost písma podle délky nadpisu
const fontSize = post.title.length <= 50 ? 80
: post.title.length <= 80 ? 60 : 46
return new ImageResponse(/* JSX layout */)
}
Data pro každý obrázek pocházejí z MDX frontmatteru příspěvku — title, description, tags, date, readingTime. Žádná databáze, žádné API volání.
Živé příklady
Každý OG obrázek si můžete prohlédnout přímo v prohlížeči:
Silné stránky
Nulová konfigurace
Soubor ve správné složce, export výchozí funkce — Next.js udělá zbytek. Žádný plugin, žádná konfigurace v next.config.ts.
Přístup k design systému
OG obrázky mají přístup ke stejným fontům, barvám a datům jako zbytek aplikace. Výsledek je vizuálně konzistentní se skutečným webem.
Dynamická data
Každý post dostane vlastní obrázek s názvem, popisem, tagy a dobou čtení. Šablona je jedna, obrázků vznikne tolik, kolik je příspěvků.
Bez externích závislostí
Žádný Puppeteer (stovky MB), žádný headless Chrome, žádná třetí strana jako Cloudinary nebo Bannerbear. Vše běží na serveru v Node.js procesu.
Cachování
Na Vercelu se obrázky cachují automaticky na CDN. Po prvním vygenerování je následující requesty obslouží edge bez spuštění funkce.
Slabé stránky
Omezená podmnožina CSS
Satori nepodporuje celé CSS. Funguje jen flexbox layout — žádný grid, žádné position: absolute/fixed uvnitř flex kontejnerů (s výjimkami), žádné pseudoelementy (:before, :after), žádné transform na textu.
Ruční načítání fontů
next/font není k dispozici. Fonty musí být jako TTF/OTF soubory v /public, načtené přes fs.readFile. Každý request znovu načte soubory — nebo musíte implementovat cachování na úrovni modulu.
Ořez dlouhého textu
Satori nepodporuje text-overflow: ellipsis ani -webkit-line-clamp. Pokud je nadpis příliš dlouhý, přeteče přes okraj. Řeší se buď ořezem řetězce v JS, nebo dynamickou úpravou velikosti písma (jak to dělá tato šablona).
Ladění je nepříjemné
Satori obrázky nevidíte v React DevTools. Musíte navštívit /opengraph-image v prohlížeči a obnovovat stránku. Pro složitější layouty to bolí.
Omezená podpora emoji
Emoji vyžadují speciální emoji font a přesné nastavení fontFamily. Bez toho se zobrazí jako prázdné čtverce.
Cold start
První request spustí načtení fontů a kompilaci JSX. Na Vercelu to bývá v řádu stovek milisekund — pro OG obrázky zcela přijatelné, ale je to měřitelná latence.
Alternativy
Pokud vám omezení Satori vadí, existují alternativy:
- Puppeteer / Playwright — renderuje skutečný prohlížeč, full CSS support, ale pomalé a těžké
- Cloudinary transformations — rychlé, ale vendor lock-in a limit na složitost
- Statické obrázky — jednoduchý
opengraph-image.pngve složce routy, žádná dynamika
Pro většinu blogů a portfolií je file-based ImageResponse správná volba. Dynamická, lehká, bez závislostí.
Zdrojový kód všech tří OG šablon tohoto blogu je na GitHubu — viz src/app/[locale]/**/opengraph-image.tsx.