Przejdź do treści

Jak automatycznie generować grafiki Open Graph w Next.js?

Twórz unikalne podglądy social media w locie. Zobacz, jak wykorzystać ImageResponse z next/og, aby każdy wpis automatycznie zyskał profesjonalny obrazek Open Graph.

Maciej Sala

Founder StriveLab

5 min czytaniaOpublikowano 10 kwietnia 2026 (Aktualizacja 17 lipca 2026)

Po co dynamiczne OG Images w Next.js?

Kiedy ktoś udostępnia link do Twojej strony na Facebooku, X, LinkedIn czy Slacku, platforma pobiera obraz i wyświetla go jako preview. Statyczny obrazek jest lepszy niż nic, ale dynamiczny, który jest generowany z tytułem artykułu, imieniem autora i brandingiem, będzie znacznie lepszy. Skuteczniej przyciągnie użytkowników i będzie przy tym bardziej bardziej optymalny, niż tworzenie obrazów ręcznie. Przy stu artykułach to przynajmniej kilka dni, jeśli nie tydzien pracy, dlatego obraz lepiej wygenerować z kodu. ImageResponse z next/og składa go programowo z tytułu i brandingu, w runtime Edge albo Node.js.

Podstawowy setup dynamicznych OG Images

W Next.js App Router generowanie OG image to Route Handler, który zwraca ImageResponse:

Code
// app/api/og/route.tsx
import { ImageResponse } from 'next/og'
 
export const runtime = 'edge'
 
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const title = searchParams.get('title') || 'Firma'
  const subtitle = searchParams.get('subtitle') || 'Tworzenie stron w Next.js'
 
  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'center',
          width: '100%',
          height: '100%',
          backgroundColor: '#0f172a',
          padding: '60px',
        }}
      >
        <div
          style={{
            display: 'flex',
            flexDirection: 'column',
            gap: '16px',
          }}
        >
          <span style={{ color: '#3b82f6', fontSize: '24px', fontWeight: 600 }}>
            example.com
          </span>
          <h1
            style={{
              color: '#f8fafc',
              fontSize: '56px',
              fontWeight: 700,
              lineHeight: 1.2,
              margin: 0,
            }}
          >
            {title}
          </h1>
          <p style={{ color: '#94a3b8', fontSize: '24px', margin: 0 }}>
            {subtitle}
          </p>
        </div>
        <div
          style={{
            display: 'flex',
            alignItems: 'center',
            gap: '16px',
            marginTop: '40px',
          }}
        >
          <div
            style={{
              width: '48px',
              height: '48px',
              borderRadius: '50%',
              backgroundColor: '#3b82f6',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              color: '#fff',
              fontSize: '20px',
              fontWeight: 700,
            }}
          >
            MS
          </div>
          <span style={{ color: '#e2e8f0', fontSize: '20px' }}>
            Jan Kowalski
          </span>
        </div>
      </div>
    ),
    {
      width: 1200,
      height: 630,
    },
  )
}

URL: https://example.com/api/og?title=Mój artykuł&subtitle=Poradnik Next.js

Linijka export const runtime = 'edge' jest opcjonalnaImageResponse działa także w runtime Node.js. Wybierz Edge, gdy kod i zależności są z nim zgodne; jeśli generator potrzebuje systemu plików, ORM-u lub SDK niedostępnego na Edge, użyj Node.js i nie wymuszaj tej konfiguracji.

Publiczny generator to wektor nadużyć — ogranicz go

Zanim podepniesz ten endpoint do metadanych, zatrzymaj się przy jednej konsekwencji: /api/og?title=... przyjmuje dowolny tekst od dowolnej osoby i renderuje go z Twoim brandingiem, na Twojej domenie. Nic nie stoi na przeszkodzie, żeby ktoś wygenerował obraz „Twoje konto zostało zablokowane — kliknij tutaj" podpisany TwojaDomena.pl i użył go w phishingu.

Trzy poziomy obrony, od najprostszego:

  • Ogranicz wejście. Utnij długość parametrów (title.slice(0, 100)) i waliduj ich dozwolony format. Nagłówek Referer może nie wystąpić lub zostać sfałszowany, więc nie traktuj go jako kontroli dostępu.
  • Podpisuj URL-e. Generuj linki z HMAC (&sig=...) liczonym z parametrów i sekretu; endpoint odrzuca kombinacje, których sam nie wystawił.
  • Zrezygnuj z query params konwencja opengraph-image.tsx (sekcja niżej) bierze dane z Twojego CMS-a po slugu, więc nie ma czego podrabiać. Dla własnego bloga to najlepszy wybór, a endpoint z parametrami zostaw na przypadki, gdy obrazy generujesz dla treści spoza aplikacji.

Podłączenie OG Image do metadanych strony

Code
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
 
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>
}): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)
 
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      publishedTime: post.date,
      authors: ['Jan Kowalski'],
      images: [
        {
          url: `https://example.com/api/og?title=${encodeURIComponent(post.title)}&subtitle=${encodeURIComponent(post.excerpt)}`,
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [
        `https://example.com/api/og?title=${encodeURIComponent(post.title)}`,
      ],
    },
  }
}

Natywny plik opengraph-image.tsx w Next.js

Next.js App Router obsługuje konwencję pliku opengraph-image.tsx, obraz generowany automatycznie dla każdej strony bez konfiguracji metadanych:

Code
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
 
export const runtime = 'edge'
export const alt = 'Blog post cover'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
 
export default async function Image({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await fetch(`https://api.example.com/posts/${slug}`).then((r) =>
    r.json(),
  )
 
  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'flex-end',
          width: '100%',
          height: '100%',
          background: 'linear-gradient(135deg, #0f172a 0%, #1e3a5f 100%)',
          padding: '60px',
        }}
      >
        <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
          <div
            style={{
              display: 'flex',
              gap: '8px',
            }}
          >
            {post.tags?.map((tag: string) => (
              <span
                key={tag}
                style={{
                  backgroundColor: 'rgba(59, 130, 246, 0.3)',
                  color: '#93c5fd',
                  padding: '4px 12px',
                  borderRadius: '16px',
                  fontSize: '16px',
                }}
              >
                {tag}
              </span>
            ))}
          </div>
          <h1
            style={{
              color: '#f8fafc',
              fontSize: post.title.length > 60 ? '40px' : '52px',
              fontWeight: 700,
              lineHeight: 1.2,
              margin: 0,
            }}
          >
            {post.title}
          </h1>
          <div
            style={{
              display: 'flex',
              alignItems: 'center',
              gap: '12px',
              marginTop: '20px',
            }}
          >
            <span style={{ color: '#94a3b8', fontSize: '20px' }}>
              example.com
            </span>
            <span style={{ color: '#475569' }}>•</span>
            <span style={{ color: '#94a3b8', fontSize: '20px' }}>
              Jan Kowalski
            </span>
          </div>
        </div>
      </div>
    ),
    { ...size },
  )
}

Next.js automatycznie dodaje ten obraz do metadanych OpenGraph strony, więc nie trzeba nic konfigurować w generateMetadata.

Custom fonty, polskie znaki i emoji

Custom font to w polskim internecie nie kosmetyka, tylko konieczność: jeśli font używany do renderowania nie obejmuje polskich znaków, „ą", „ę" czy „ł" w tytule mogą wyrenderować się jako kwadraty. Dołącz font w wersji z zestawem latin-ext (np. Inter pobrany jako TTF z kompletem polskich glifów) i problem znika:

Code
// app/api/og/route.tsx
import { ImageResponse } from 'next/og'
 
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
 
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const title = searchParams.get('title') || 'Firma'
  const interBold = await readFile(join(process.cwd(), 'assets/Inter-Bold.ttf'))
 
  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          justifyContent: 'center',
          alignItems: 'center',
          width: '100%',
          height: '100%',
          backgroundColor: '#0f172a',
          fontFamily: 'Inter',
        }}
      >
        <h1 style={{ color: '#fff', fontSize: '64px' }}>{title}</h1>
      </div>
    ),
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: 'Inter',
          data: interBold,
          style: 'normal',
          weight: 700,
        },
      ],
    },
  )
}

ImageResponse domyślnie używa Twemoji, ale w opcjach konfiguracji możesz wybrać także blobmoji, noto lub openmoji. Pamiętaj o optymalizacji: JSX, CSS oraz wszystkie zasoby wliczają się do limitu 500 KB dla całego bundle’a. Aby go nie przekroczyć, dołączaj wyłącznie te kroje i grubości fontów, które są faktycznie wykorzystywane w grafice.

Cache dynamicznych OG Images

OG images nie zmieniają się często — cachuj je agresywnie:

Code
export async function GET(request: Request) {
  // ...
 
  return new ImageResponse(jsx, {
    width: 1200,
    height: 630,
    headers: {
      'Cache-Control':
        'public, max-age=86400, s-maxage=604800, stale-while-revalidate=86400',
    },
  })
}

Tydzień cache na CDN (s-maxage=604800) z jednodniowym stale-while-revalidate.

Nagłówki dotyczą endpointu /api/og. Konwencja opengraph-image.tsx jest domyślnie optymalizowana statycznie: Next.js generuje obraz podczas budowania projektu i buforuje go, o ile nie stosujesz API czasu żądania lub niecache'owanych danych. Konfiguracja fetch lub parametry segmentu trasy mogą zmienić ten tryb działania, dlatego zawsze weryfikuj zachowanie pod kątem swojego źródła danych. To kolejny argument przemawiający za korzystaniem z konwencji plikowej zamiast endpointów z parametrami.

Ograniczenia @vercel/og i zasady projektowania

(silnik renderujący pod spodem ImageResponse) obsługuje podzbiór CSS, a nie pełny CSS:

  • Obsługiwane: flexbox, kolory, fonty, padding, margin, border-radius, gradienty, opacity.

  • Nie obsługiwane lub inne podejście: CSS Grid, media queries oraz animacje (Satori nie jest pełnym silnikiem przeglądarki).

  • Dobrą praktyką jest stosowanie jawne display: 'flex' na kontenerach. Choć jest to wartość domyślna w Satori, poprawia to czytelność kodu.

Rozmiar rekomendowany: 1200×630 px (standard OG image dla większości platform).

Testowanie social media preview i OG Images

Najszybsza pętla developerska jest lokalna, więc przy konwencji plikowej otwórz w przeglądarce http://localhost:3000/blog/twoj-slug/opengraph-image, a przy endpoincie poprzez /api/og?title=Test. Zmieniasz JSX, odświeżasz kartę i widzisz obraz. Dopiero gotowy efekt weryfikuj narzędziami platform:

  • Facebook Sharing Debugger, czyli developers.facebook.com/tools/debug, który pokazuje preview i pełną listę odczytanych tagów OG.

  • LinkedIn Post Inspector, czyli linkedin.com/post-inspector.

  • Slack / Discord / WhatsApp. Wklejenie linku na testowym kanale to najlepszy test, bo komunikatory bywają najbardziej wybredne.

  • Dawny Twitter Card Validator nie renderuje już podglądu i na X po prostu sprawdź link w oknie tworzenia posta.

Platformy cache'ują pobrany podgląd po swojej stronie, więc jeśli poprawiłeś obraz, a LinkedIn czy Facebook dalej pokazują starą wersję, wymuś ponowny scrape w ich debuggerze („Scrape Again" u Facebooka).

Audyt techniczny i optymalizacja pod kątem SEO i GEO.
Audyt techniczny SEO

Często zadawane pytania

Czy OG images wpływają na SEO?

Nie bezpośrednio na ranking. Ich główna wartość to wyższy CTR z social media, komunikatorów i link preview. W Google liczą się przede wszystkim poprawne metadane i treść strony, a OG image działa na klikalność udostępnionego linku.

Czy mogę użyć zdjęć wewnątrz OG image?

Tak, przez <img> z pełnym, publicznie dostępnym URL (https://...). Pamiętaj, że obraz musi być osiągalny w momencie generowania OG image, inaczej wyrenderuje się bez niego.

Jak szybko generuje się OG image?

To zależy od runtime'u, platformy, fontów, obrazów i źródła danych. Nie zakładaj stałego czasu: zmierz endpoint w swoim środowisku. Dla obrazów, które nie zmieniają się często, ustaw cache CDN albo wybierz opengraph-image.tsx, które Next.js może wygenerować podczas builda.

Dlaczego mój layout w OG image się rozjeżdża?

Bo Satori (silnik pod ImageResponse) obsługuje tylko podzbiór CSS. Nie działa CSS Grid, a animacje i media queries nie mają tu zastosowania. Layout buduj na flexboksie; Satori używa go domyślnie, ale jawne display: flex ułatwia czytanie i przewidywanie układu.

Dlaczego polskie znaki wyświetlają się jako kwadraty?

Bo użyty font nie zawiera glifów ą, ę, ł, ś, ż. Dodaj przez pole fonts krój z zestawem latin-ext, np. Inter w formacie TTF. ImageResponse używa Twemoji domyślnie; opcją emoji wybierzesz inny obsługiwany zestaw emoji.

Czy endpoint /api/og trzeba jakoś zabezpieczyć?

Warto. Publiczny generator przyjmujący dowolny tekst z URL pozwala każdemu tworzyć obrazy z Twoim brandingiem i dowolną treścią. Najprościej zrezygnować z query params na rzecz konwencji opengraph-image.tsx (dane z CMS, a nie z URL-a). Jeśli endpoint musi zostać, ogranicz długość parametrów i rozważ podpisywanie URL-i (HMAC), żeby przyjmować tylko wygenerowane przez Ciebie kombinacje.

Czym ImageResponse różni się od @vercel/og?

ImageResponse to API Next.js importowane z next/og. Pod spodem korzysta z @vercel/og, Satori i Resvg. W aplikacji Next.js używaj next/og; pakiet @vercel/og pozostaje przydatny poza Next.js.

O autorze

Maciej Sala

Maciej Sala — konsultant technologiczny produktów cyfrowych i web developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwija własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
Optymalizacja obrazów w Next.js oraz next image i CDN

Obrazy są najcięższym bagażem większości stron internetowych. W skrajnych przypadkach, jedno niezoptymalizowane zdjęcie potrafi dorzucić kilka sekund do LCP, a brak zarezerwowanego miejsca rozjeżdża layout. Next.js daje gotowe narzędzie, next/image , ale to nie zawsze będzie to najlepszy wybór, dlatego porównuję trzy podejścia: komponent next/image , natywny srcset i dedykowane CDN do obrazów.

Maciej Sala

Maciej Sala

Founder StriveLab

Edge Runtime w Next.js — kiedy Edge, a kiedy Node?

Edge Runtime obiecuje błyskawiczne działanie kodu tuż przy użytkowniku. To ultralekkie środowisko, oparte na V8 Isolates i rozproszone globalnie, eliminuje cold start , bo nie wymaga rozgrzewania ciężkich maszyn wirtualnych. Przez lata wydawało się to jedynym słusznym kierunkiem, ale dzisiaj jednak, w Next.js 16, automatyczny wybór Edge może okazać się strategicznym błędem.

Maciej Sala

Maciej Sala

Founder StriveLab

Next.js i SEO oraz przewaga nad czystym Reactem

Next.js to framework React, który rozwiązuje największy problem klasycznych aplikacji SPA : serwer wysyła gotowy HTML zamiast pustej strony. Dzięki temu Google widzi treść w początkowym HTML i nie musi czekać na JavaScript, żeby odkryć najważniejsze elementy strony. Do tego dostajesz wbudowane API do metadanych, automatyczną optymalizację obrazów i code splitting podział kodu ładowanego osobno dla każdej podstrony , który realnie poprawia Core Web Vitals.

Maciej Sala

Maciej Sala

Founder StriveLab