Všechny příspěvky

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:

  1. Zaregistruje route /[segment]/opengraph-image
  2. Nastaví <meta property="og:image"> v <head> dané stránky
  3. 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.png ve 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.