# Obrazki OG w Next.js bez ograniczeń @vercel/og

> next/og nie obsłuży CSS grid, woff2 ani paczki ponad 500 KB. Gdy to przeszkadza, użyj prawdziwego Chrome'a: podpisany og:image w generateMetadata.

Opublikowano 2026-11-04 · https://shareglint.com/pl/guides/nextjs-og-images-without-vercel-og-limits

**next/og** (API `ImageResponse`, zbudowane na `@vercel/og`) to domyślny sposób na obrazki Open Graph w Next.js i przy tytule na gradiencie sprawdza się świetnie. Nie jest to jednak przeglądarka: zamienia JSX na SVG za pomocą [Satori](https://github.com/vercel/satori) i rasteryzuje wynik przez Resvg. Gdy karta potrzebuje CSS grid, twojego prawdziwego arkusza stylów, fontów woff2 albo więcej niż 500 KB zasobów, rozwiązaniem nie jest walka z Satori — tylko wyrenderowanie karty prawdziwym Chrome'em i wstawienie gotowego adresu do `generateMetadata`.

Poniżej dokładna lista ograniczeń, sytuacje, w których dają o sobie znać, i zamiennik, który działa w runtime'ach Node.js i Edge.

## Ograniczenia według dokumentacji Next.js

[Dokumentacja ImageResponse](https://nextjs.org/docs/app/api-reference/functions/image-response) wymienia je wprost:

- **Tylko flexbox i część CSS.** „Zaawansowane układy (np. `display: grid`) nie zadziałają”. Pełna lista obsługiwanych właściwości jest w [README Satori](https://github.com/vercel/satori#css).
- **Limit paczki 500 KB**, liczony łącznie dla JSX, CSS, fontów, obrazków i innych zasobów.
- **Fonty: tylko `ttf`, `otf` i `woff`** — bez `woff2`, a właśnie ten format dają większość serwisów z fontami i `next/font`.
- Domyślny wynik to **PNG 1200×630**, czyli właściwy rozmiar dla Open Graph.

W praktyce wygląda to tak: dwukolumnowa karta, która potrzebuje gridu; font firmowy dostępny tylko jako woff2 (trzeba go przekonwertować, inaczej po cichu podmieni się na zastępczy); dwie grubości fontu CJK, które same przekraczają 500 KB; logo w SVG z filtrami, których Resvg nie obsługuje; i ogólny koszt przepisywania projektu na style inline, bo arkusz CSS strony jest niedostępny.

Uwaga, jeśli czytasz README samego Satori: nowsze samodzielne wydania Satori dodały CSS grid i WOFF2, ale `@vercel/og` dołączane do Next.js (1.0.x w Next 16, oparte na starszym Satori) ich nie ma — dla `next/og` obowiązują ograniczenia z dokumentacji Next.js podane wyżej.

To nie są błędy. Satori zamienia kompletność na to, że działa wszędzie i w milisekundach. Jeśli twoja karta się w nim mieści, zostań przy nim.

## Opcja A: zostań przy next/og, ale w jego granicach

Jeśli problemem są tylko fonty i rozmiar, popraw je i zostań przy `ImageResponse`:

```tsx
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';

export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';

// ttf/otf parse fastest; subset the font to Latin to stay far below 500 KB.
const inter = readFile(join(process.cwd(), 'assets/Inter-Bold-latin.ttf'));

export default async function Image({ params }: { params: Promise<{ slug: string }> }) {
  const post = await getPost((await params).slug);

  return new ImageResponse(
    (
      <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
                    width: '100%', height: '100%', padding: 72, background: '#0b0a09', color: '#fff' }}>
        <div style={{ fontSize: 64, fontWeight: 700, lineHeight: 1.1 }}>{post.title}</div>
        <div style={{ fontSize: 28, opacity: 0.7 }}>yourblog.com</div>
      </div>
    ),
    { ...size, fonts: [{ name: 'Inter', data: await inter, weight: 700, style: 'normal' }] },
  );
}
```

Okrojenie fontu do potrzebnych znaków (np. narzędziem `pyftsubset` z pakietu fonttools) to najskuteczniejszy sposób na limit 500 KB. Pamiętaj tylko, że polskie litery poza „ó” (ą, ć, ę, ł, ń, ś, ź, ż) leżą w Unicode w bloku Latin Extended-A, nie w podstawowym zakresie Latin (U+0000–U+00FF) — jeśli zostawisz sam ten zakres, tytuły z polskimi znakami wyrenderują się fontem zastępczym albo pustymi kwadratami.

## Opcja B: prawdziwy Chrome, jako adres w generateMetadata

Gdy potrzebujesz gridu, swojego arkusza stylów, woff2, fontów emoji albo złożonych systemów pisma, wyrenderuj kartę z HTML-a i CSS w prawdziwej przeglądarce. Uruchomienie Chrome'a w funkcji serverless jest możliwe (okrojony Chromium, np. `@sparticuz/chromium` z `puppeteer-core`), ale zimne starty trwające kilka sekund i limity pamięci czynią to kruchym — a robot podglądu linków nie poczeka.

Lżejsza droga to **podpisany adres obrazka**: obliczasz go w `generateMetadata` (bez zapytania sieciowego i bez klucza API na stronie), a serwis renderuje obrazek przy pierwszym żądaniu i potem serwuje gotowy plik z cache. Poniżej format adresu Shareglint z wbudowanym szablonem `blog-og` — podmień slug na własny szablon HTML, żeby użyć własnego projektu. Dzięki Web Crypto ten sam kod działa w Node.js i Edge:

```ts
// lib/og.ts
const enc = (v: string) =>
  encodeURIComponent(v).replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase());

export async function ogImageUrl(template: string, fields: Record<string, string>) {
  const path = `/i/${process.env.OG_ACCOUNT_ID}/${template}.png`;
  const query = Object.keys(fields).sort().map((k) => `${enc(k)}=${enc(fields[k])}`).join('&');

  const key = await crypto.subtle.importKey(
    'raw', new TextEncoder().encode(process.env.OG_SIGNING_SECRET!),
    { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
  );
  const mac = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(`${path}?${query}`));
  const s = Array.from(new Uint8Array(mac), (b) => b.toString(16).padStart(2, '0')).join('').slice(0, 32);

  return `https://shareglint.com${path}?${query}&s=${s}`;
}
```

```tsx
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
import { ogImageUrl } from '@/lib/og';

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
  const post = await getPost((await params).slug);
  const image = await ogImageUrl('blog-og', { title: post.title, author: post.author, site: 'yourblog.com' });

  return {
    title: post.title,
    openGraph: { images: [{ url: image, width: 1200, height: 630, alt: post.title }] },
    twitter: { card: 'summary_large_image', images: [image] },
  };
}
```

`encodeURIComponent` nie koduje znaków `! ' ( ) *`, a RFC 3986 tak — `replace` to poprawia, żeby podpis się zgadzał. Polskie znaki w tytule `encodeURIComponent` koduje jako UTF-8, więc podpis obejmuje je bez dodatkowej pracy. Sprawdziliśmy tę funkcję na wektorze testowym z [dokumentacji API](https://shareglint.com/pl/docs#signed). `OG_SIGNING_SECRET` nie ma przedrostka `NEXT_PUBLIC_`, więc Next.js nigdy nie wstawi go do paczek klienta; w HTML-u ląduje tylko podpis.

Dwie rzeczy w Next.js, o których trzeba wiedzieć:

- **Pliki wygrywają.** Jeśli segment trasy ma `opengraph-image.tsx` albo `.png`, Next.js używa go zamiast `openGraph.images` z `generateMetadata` — według [dokumentacji generateMetadata](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) metadane z plików „mają wyższy priorytet i nadpiszą” konfigurację. Po przejściu na adres usuń plik.
- **Adresy bezwzględne.** Podpisany adres już jest bezwzględny. Jeśli zostawiasz jakieś względne ścieżki obrazków, ustaw `metadataBase` w głównym layoucie, inaczej Next.js nie zbuduje pełnego adresu.

## Opcja C: renderowanie przy buildzie dla eksportu statycznego

Przy `output: 'export'` w czasie żądania nie ma serwera. Podpisany adres i tak działa — to napis obliczany podczas buildu. Jeśli wolisz wdrażać gotowe pliki, wywołaj API ze skryptu budującego i zapisz zwrócone adresy:

```ts
// scripts/og-images.ts — run before `next build`
const res = await fetch('https://shareglint.com/v1/images?wait=true', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.OG_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ template: 'blog-og', modifications: { title: post.title, site: 'yourblog.com' } }),
});
const { url } = await res.json();   // same fields again = same file, no extra render
```

Te same pola dają ten sam plik, więc ponowny build nie zużywa limitu na karty, które już istnieją. Ceny: plan Free to 100 obrazków miesięcznie, płatne plany od $9 miesięcznie ([cennik](https://shareglint.com/pl/pricing)). Gotowe przykłady dla tego frameworka są też na stronie [generator obrazków OG dla Next.js](https://shareglint.com/pl/og-image-generator/nextjs).

## Co wybrać

| Potrzebujesz | Użyj |
|---|---|
| Tytuł, logo, gradient; fonty poniżej 500 KB | `next/og` |
| CSS grid, własny arkusz stylów, woff2, złożone systemy pisma | Prawdziwy Chrome — podpisany adres (B) |
| Zero serwera (`output: 'export'`) | Podpisany adres obliczony przy buildzie albo skrypt (C) |
| Jedna ogólna karta dla wszystkich stron | Statyczny `opengraph-image.png` |

Cokolwiek wybierzesz, sprawdź wynik tam, gdzie będzie oglądany: wklej produkcyjny adres do [Post Inspectora LinkedIna](https://www.linkedin.com/post-inspector/) i [Sharing Debuggera Facebooka](https://developers.facebook.com/tools/debug/) — adresów `localhost` żaden z nich nie pobierze. Jeśli podgląd mimo to się nie pojawia, przejdź przez [poradnik o niedziałającym podglądzie linku](https://shareglint.com/pl/guides/og-image-not-showing).

## FAQ

### Jakie ograniczenia ma next/og (ImageResponse)?

Według dokumentacji Next.js: tylko flexbox i część CSS (bez display: grid), limit paczki 500 KB łącznie z JSX, CSS, fontami i obrazkami oraz fonty wyłącznie ttf, otf i woff. Renderuje Satori i Resvg, nie przeglądarka.

### Czy w obrazku OG mogę użyć Tailwinda albo moich komponentów React?

W next/og tylko częściowo: Satori przyjmuje style inline i atrybut tw, ale nie twój arkusz stylów, CSS grid ani dowolne komponenty. Renderer oparty na prawdziwym Chrome przyjmuje zwykły HTML i CSS, więc karta może korzystać z twojego design systemu.

### Czy opengraph-image.tsx nadpisuje generateMetadata?

Tak. Next.js daje metadanym z plików wyższy priorytet niż metadanym z konfiguracji, więc plik opengraph-image w segmencie trasy zastępuje openGraph.images zwracane z generateMetadata.

### Czy do obrazków OG potrzebuję runtime'u Edge?

Nie. ImageResponse działa w runtime'ach Node.js i Edge, a podpisany adres obrazka to po prostu napis, który obliczasz — dzięki Web Crypto ten sam kod działa w obu.
