Blog
2026-06-07 - Artur Wilczek
Grafiki i obrazy OG — nowe SKILL-e

Grafiki i obrazy OG — nowe SKILL-e

Dwa SKILL-e Claude Code, które generują grafiki artykułów i obrazy Open Graph stron w powtarzalnym stylu — AI od generowania bazowej grafiki, skrypty od manipulacji obrazem.

Wstęp

W poprzednim artykule opisałem, jak zbudowałem zespół wyspecjalizowanych SKILL-i Claude Code do prowadzenia projektu — od planowania zadań po dokumentację. Tamten workflow dotyczył kodu. Tym razem chcę pokazać, że ta sama filozofia — wyspecjalizowany agent + skrypt pomocniczy — świetnie sprawdza się też tam, gdzie zwykle sięga się po grafika albo po godzinę grzebania w edytorze: przy grafikach nagłówkowych artykułów i obrazach Open Graph.

Problem był prozaiczny. Każdy nowy artykuł potrzebuje grafiki w nagłówku. Każda podstrona — /, /about, /contact, /blog — potrzebuje sensownego obrazu, który pokaże się, gdy ktoś wrzuci link na LinkedIn czy do Slack-a. Robienie tego ręcznie jest trudne i czasochłonne i — co gorsza — niespójne: za każdym razem trochę inny styl, inny font, inny kadr. A spójność wizualna to jedyne, co odróżnia stronę wyglądającą profesjonalnie od zlepka przypadkowych obrazków.

Postanowiłem to zautomatyzować. Efektem są dwa SKILL-e: generate-article-picture i generate-page-og-picture.

Czym jest Open Graph i po co go stosować

Open Graph (w skrócie OG) to protokół zaproponowany pierwotnie przez Facebooka, który mówi serwisom społecznościowym i komunikatorom, jak wyświetlić podgląd linku. Kiedy wklejasz adres strony na LinkedIn, X, Facebooku, w Slack-u czy iMessage, to nie magia podpowiada tytuł, opis i obrazek — robią to znaczniki <meta> w sekcji <head> strony:

<meta property="og:title" content="Spójne grafiki bloga i obrazy OG" />
<meta property="og:description" content="Dwa SKILL-e Claude Code…" />
<meta property="og:image" content="https://www.awfs.dev/og/0005.png" />
<meta property="og:url" content="https://www.awfs.dev/blog/…" />

Najważniejszy z perspektywy „klikalności” jest og:image — to ten duży obrazek w karcie podglądu. Dobrze dobrana grafika potrafi zauważalnie podnieść współczynnik kliknięć (CTR), bo link przestaje być nagą linijką tekstu, a staje się kafelkiem przyciągającym wzrok. Rekomendowany rozmiar to 1200 × 630 px (proporcje 1.91:1) — i tego rozmiaru trzymam się wszędzie.

Warto wiedzieć, że OG ma „kuzyna” — Twitter Cards (twitter:card, twitter:image). W praktyce większość platform potrafi awaryjnie sięgnąć po znaczniki OG, więc poprawny zestaw og:* załatwia większość przypadków.

Jak sprawdzić, czy strona ma poprawny OG

Są dwa sposoby — ręczny i przez dedykowane narzędzia.

Ręcznie. Najprościej otworzyć źródło strony (w przeglądarce Ctrl/Cmd + U lub View Source) i wyszukać og:. Zobaczysz wszystkie znaczniki tak, jak trafiają do robotów. Z konsoli można to samo zrobić jednym poleceniem — używam grep -o z wzorcem na sam tag, bo HTML bywa zminifikowany do jednej linii i zwykły grep wypisałby wtedy całą stronę zamiast samych meta tagów:

curl -s https://www.awfs.dev/contact | grep -o '<meta[^>]*og:[^>]*>'

Pamiętaj tylko, że og:image powinien być adresem absolutnym (z https://…) — część crawler-ów odrzuca względne ścieżki. Na stronie pilnuje tego helper absoluteUrl z app/utils/seo.ts.

Narzędziami. Wygodniej sprawdzić podgląd „oczami platformy”. Używam głównie:

  • opengraph.xyz — pokazuje podgląd dla wielu serwisów naraz i wypisuje ostrzeżenia (np. zbyt krótki tytuł, zbyt długi opis, brak obrazu).
  • LinkedIn Post Inspector — kluczowy, bo publikuję głównie na LinkedIn; dodatkowo odświeża cache podglądu (platformy buforują OG i bez tego widzą starą wersję).
  • Meta Sharing Debugger i podgląd kart X — gdy chcę zweryfikować konkretną platformę.

To właśnie ostrzeżenia z opengraph.xyz (zbyt krótki og:title, zbyt długi og:description, brak call-to-action na obrazie) pchnęły mnie wcześniej do dopracowania metadanych podstron — ale to temat na osobną historię. Tutaj skupiam się na samych obrazach.

Pomysł: SKILL dostaje pomysł, skrypt pilnuje rygoru

Generowanie obrazu przez model AI ma jedną zaletę i jedną wadę. Zaleta: modele potrafią narysować ładne, klimatyczne tło w dowolnym stylu. Wada: są niedeterministyczne — poproś dwa razy o to samo, dostaniesz dwa różne kadry, inne wymiary, a jeśli każesz dopisać tekst, to z dużym prawdopodobieństwem przekręci polskie znaki albo „wymyśli” litery.

Dlatego oba SKILL-e zbudowałem na wyraźnym podziale ról:

  • Część kreatywna (tło, klimat, motyw) → model AI. To jedyne, w czym model jest naprawdę dobry, a niedeterminizm tu nie przeszkadza.
  • Część deterministyczna (dokładne wymiary, napisy, czcionka, logo, nazwa pliku) → skrypt pomocniczy w Node.js. Tu liczy się powtarzalność co do piksela.

W praktyce wygląda to tak, że SKILL (czyli Claude) odpowiada tylko za brief — krótki opis sceny pasującej do treści — a całą resztę egzekwuje skrypt .mjs. Skrypt jest źródłem prawdy: ma w sobie zaszyty stały prompt stylu, stałą czcionkę i stały rozmiar, więc agent nie ma jak „popłynąć”.

SKILL #1: grafiki artykułów — generate-article-picture

Pierwszy SKILL zlecam Claude'owi tak samo, jak wszystkie poprzednie — opisuję, czego chcę, a on przygotowuje gotowego SKILL-a wraz ze skryptem. Mój prompt brzmiał:

Przygotuj SKILL o nazwie **generate-article-picture** wraz ze skryptem pomocniczym:

- SKILL przyjmuje jeden parametr, który jest _slug-iem_ artykułu, którego plik MD znajduje się w folderze `./content/blog/`.
- Rolą jest przygotowanie obrazu o zadanym stylu i pasującego do treści artykułu.
- Czyta artykuł i przygotowuje streszczenie (opis czego ma dotyczyć obraz) dla modelu AI do generowania obrazów.
- Obraz musi być DOKŁADNIE o wymiarach **1200 x 630 pikseli**.
- Obraz musi być w stylu nowoczesnym, nawet futurystycznym, związany z programowaniem i technologiami AI.
- Obraz musi być w ciemniejszych kolorach.
- Jedyne napisy na obrazie to:
  - Numer artykułu w formie **#N**, gdzie `N` to wartość z pola `order` metadanych, kolor jasno szary, półprzeźroczysty, prawy dolny róg obrazu (dla 4 to #4).
  - Tytuł artykułu w formie, czyli wartość z pola `title` metadanych, kolor ciemniejszy szary, półprzeźroczysty, prawy dolny róg nad **#N**.
  - Zawsze ta sama czcionka dla wszystkich obrazów generowanych dla różnych artykułów.
- Prompt z wszystkimi wytycznymi dla modelu AI wysyłany jest przez portal _OpenRouter_:
  - Domyślny model: `google/gemini-3.1-flash-image-preview`.
  - Model i klucz API konfigurowane w pliku `.env`.
- Wygenerowany obraz trafia do folderu `./public/pics/`, do pliku `NNNN.png`, gdzie `NNNN` to numer z pola `order` artykułu z zerami wiodącymi (dla 4 to 0004.png).

Pierwsze efekty były technicznie poprawne, ale zbyt ponure — same szarości. Wystarczyła jedna korekta promptu, żeby ożywić paletę:

Generowane obrazy są dość ponure, mają być w ciemniejszych barwach,
ale niekoniecznie tylko odcienie szarości. Mogą być bardziej pozytywne.

Od tej pory bazą jest głęboka ciemność (granat, grafit, czerń), ale rozświetlona żywymi akcentami — turkus, zieleń, magenta, bursztyn, coral. Ciemno, ale nie ponuro.

Rola AI i sekret spójnego stylu

Mogłoby się wydawać, że spójność stylu zależy od modelu. Nie zależy. Spójność bierze się ze stałego promptu zaszytego w skrypcie, a nie z briefu, który zmienia się przy każdym artykule. W skrypcie znajduje się sekcja STYLE (ten sam klimat dla całej serii) i STRICT CONSTRAINTS, a brief od Claude'a dokleja się tylko jako sekcja SCENE:

STYLE (identical mood for every article in this series):
- Modern, even futuristic; theme: software development and AI technologies.
- Dark but NOT gloomy … vibrant, optimistic accent colors …

STRICT CONSTRAINTS:
- Do NOT render ANY text, letters, numbers, words, captions, logos …
- Keep the LOWER-RIGHT quadrant calmer and darker … so caption text stays legible.

SCENE (what this specific article is about):
<brief od Claude'a, np. „a pipeline turning AI-generated artwork into branded social cards">

Zwróć uwagę na dwie rzeczy w STRICT CONSTRAINTS. Po pierwsze: model ma kategoryczny zakaz rysowania jakiegokolwiek tekstu — bo napisy nałoży skrypt. Po drugie: proszę model, żeby prawy dolny róg był ciemniejszy i spokojniejszy — bo właśnie tam ląduje tytuł i numer artykułu, które muszą pozostać czytelne.

Rolą Claude'a jest więc tylko jedno: przeczytać artykuł i napisać 2–4 zdania po angielsku opisujące scenę. Cała tożsamość wizualna serii jest poza modelem.

Co dzieje się po wygenerowaniu obrazu

Surowy obraz z modelu to dopiero półprodukt. Reszta to deterministyczny post-processing w Node.js przy użyciu biblioteki sharp:

  1. Przycięcie do dokładnych wymiarów. Model bywa kapryśny co do proporcji, więc sharp skaluje wynik do 1200 × 630 przez resize(WIDTH, HEIGHT, { fit: 'cover' }). To gwarantuje, że każdy obraz w serii ma identyczny rozmiar.
  2. Nałożenie napisów. Tytuł i numer #N renderuję jako nakładkę SVG złożoną na obraz przez sharp.composite(). Tekst w SVG jest „twardy” — wpisany wprost w XML, więc nie ma mowy o przekręceniu liter.

Dlaczego napisy renderuje skrypt, a nie AI? Trzy powody, wszystkie praktyczne:

  • Poprawne polskie diakrytyki. Skrypt używa stałej czcionki DejaVu Sans, która ma pełny komplet polskich znaków. Model AI regularnie gubi „ł”, „ż” czy „ą” albo renderuje je jako artefakty.
  • Powtarzalność co do piksela. Tytuł zawsze tym samym fontem, w tym samym kolorze (ciemniejszy szary, półprzeźroczysty), w prawym dolnym rogu, z numerem #N pod spodem. Żadnych wahań między artykułami.
  • Determinizm nazwy i treści. Nazwa pliku (0005.png) i treść napisów pochodzą wprost z metadanych artykułu (title, order) — nie ma tu miejsca na kreatywność modelu.

W kodzie wygląda to mniej więcej tak (uproszczony fragment skryptu):

const background = await sharp(rawImage)
  .resize(1200, 630, { fit: 'cover', position: 'centre' })
  .toBuffer()

const finalPng = await sharp(background)
  .composite([{ input: buildOverlaySvg(title, order), top: 0, left: 0 }])
  .png()
  .toBuffer()

Sam obraz powstaje przez OpenRouter (domyślny model google/gemini-3.1-flash-image-preview - Nano Banana 2), a klucz API i model siedzą wyłącznie w pliku .env — nigdy w repozytorium. Koszt jest niewielki: jedno wygenerowanie obrazu domyślnym modelem Nano Banana 2 to około $0.07, więc nawet kilka podejść do briefu mieści się w rozsądnych kwotach.

SKILL #2: obrazy OG podstron — generate-page-og-picture

Drugi SKILL działa na tej samej zasadzie, ale dla podstron, nie artykułów. Zlecając go, dorzuciłem ważną decyzję projektową — chciałem wyłączyć generowanie OG w czasie build-u (moduł nuxt-og-image dawał niezadowalające efekty) i zastąpić je statycznymi obrazami tworzonymi raz, w czasie development-u. Fragment promptu:

1. Całkowicie wyłącz aktualny mechanizm generowania grafik OG w czasie build.
   Chcę mieć statyczne obrazy generowane w czasie development-u.

2. Przygotuj SKILL o **nazwie generate-page-og-picture** wraz ze skryptem:

- Analogicznie do SKILL-a **generate-article-picture**.
- SKILL przyjmuje dwa parametry:
  - URL względny strony, np.: `/contact`.
  - Główny tekst grafiki OG.
- Rolą jest przygotowanie obrazu typu _OG_ o zadanym stylu.
- Obraz musi być DOKŁADNIE o wymiarach **1200 x 630 pikseli**.
- Obraz musi mieć tło w kolorze tła strony w trybie ciemnym.
- Grafika obrazu to wygenerowane przez AI delikatne kształty geometryczne w stylu nowoczesnym, nawet futurystycznym.
- W lewym górny rogu grafiki musi być logo strony + napis AWFS.dev - identycznie jak jest z lewej strony górnej belki strony.
- Na środku tekst podany jak drugi argument SKILL-a. Jeśli użyto znaku \n to rozbij tekst na linie.
- Zawsze ta sama czcionka dla wszystkich obrazów generowanych tym SKILL-em.
- Prompt z wszystkimi wytycznymi dla modelu AI wysyłany jest przez portal _OpenRouter_:
  - Domyślny model: `google/gemini-3.1-flash-image-preview` (Nano Banana 2).
  - Model i klucz API konfigurowane w pliku `.env`.
- Wygenerowany obraz trafia do folderu `./public/og/`, do pliku o nazwie strony, np.: `contact.png` dla strony `/contact` (do nazwy bierz ostatni człon URL, czyli `test2.png` dla strony `/test1/test2`).
- Zaktualizuj meta dane strony tak, żeby `og:image` wskazywał na nowo utworzony obraz.

Różnice względem grafik artykułów wynikają z innego celu. Tu tłem jest dokładny kolor strony w trybie ciemnym (#111827), a model AI dorysowuje na nim tylko delikatne, geometryczne kształty (cienkie linie, wireframe, subtelne poświaty) — stonowane, bo nad nimi ląduje tekst i marka. Krycie tej warstwy AI reguluję opcją --opacity (domyślnie 0.7), więc kształty nigdy nie przebijają treści.

Tak wygląda gotowy efekt — obraz OG strony /contact wygenerowany tym SKILL-em: ciemne tło strony, subtelna geometryczna siatka od AI, logo „AWFS.dev” w lewym górnym rogu i wycentrowany tekst nałożony deterministycznie przez skrypt:

Najważniejszy element to logo AWFS.dev w lewym górnym rogu — odwzorowanie górnej belki strony. I tu pojawia się ciekawy szczegół architektoniczny.

Logo musi wyglądać identycznie na każdym obrazie OG. Żeby to wymusić, wydzieliłem je do współdzielonego modułu og-logo.mjs — jedynego źródła prawdy dla marki. Moduł odwzorowuje komponent AppLogo.vue co do koloru: box gray-800, „AW”/„dev” szare, „FS” błękitne (sky), kropka żółta. Sama ikona to natywny public/icon_32x32.png, dokładany osobno przez sharp (a nie rysowany w SVG), żeby zachować ostrość:

return [
  { input: buildLogoSvg(), top: 0, left: 0 }, // box + napis "AWFS.dev"
  { input: icon, top: MARGIN + ICON_PAD, left: MARGIN + ICON_PAD } // ikona PNG
]

Dzięki temu, że og-logo.mjs jest importowany w kilku miejscach, zmiana wyglądu logo w jednym pliku propaguje się wszędzie.

Grafiki artykułów też stają się obrazami OG

Zostało jeszcze jedno: artykuły bloga jako linki też powinny ładnie wyglądać w podglądzie — i to z brandingiem AWFS. Nie chciałem jednak generować dla nich osobnych obrazów. Rozwiązanie: wziąć istniejącą grafikę nagłówkową artykułu i doklejać do niej tylko logo.

Służy do tego trzeci skrypt — stamp-og-logo.mjs — który korzysta z tego samego og-logo.mjs, ale nakłada samo logo (bez AI i bez tekstu środkowego) na gotowy obraz, skalując go do 1200 × 630:

node .claude/skills/generate-page-og-picture/stamp-og-logo.mjs public/og/0005.png

Grafiki artykułów kopiuję więc z public/pics/NNNN.png do public/og/NNNN.png, stempluję logiem, a strona artykułu mapuje og:image prostą podmianą prefiksu ścieżki — /pics//og/:

const ogImage = page.value.image
  ? absoluteUrl(page.value.image.replace('/pics/', '/og/'))
  : undefined

W efekcie nagłówek artykułu i jego obraz OG to ta sama grafika, tyle że wersja OG nosi dodatkowo logo w rogu.

Jak wywołuję SKILL-e, gdy potrzebuję nowych grafik

Cała obsługa sprowadza się do kilku poleceń z poziomu Claude Code. Dla tego artykułu (order: 5) wyglądało to tak:

# 1. Grafika nagłówkowa artykułu → public/pics/0005.png
/generate-article-picture grafiki-artykulow-i-obrazy-og

# 2. Wersja OG artykułu: kopia grafiki + logo w rogu → public/og/0005.png
#    (kopiuję pics → og, po czym stempluję logiem)
node .claude/skills/generate-page-og-picture/stamp-og-logo.mjs public/og/0005.png

A dla podstrony — np. strony kontaktowej — jeden SKILL załatwia obraz i aktualizację metadanych:

/generate-page-og-picture /contact "Kontakt"

Pod spodem SKILL nie robi „magii” — po prostu uruchamia skrypt pomocniczy poleceniem node …/generate-page-og-picture.mjs "<url>" "<tekst>", podając opcjonalny --brief ze sceną. Zanim odpali API, warto rzucić okiem na pełny prompt trybem --dry-run:

node .claude/skills/generate-article-picture/generate-article-picture.mjs \
  grafiki-artykulow-i-obrazy-og \
  --brief "a pipeline turning AI artwork into branded social preview cards" \
  --dry-run
Podział na SKILL (decyzja) + skrypt (rygor) ma jeszcze jedną zaletę: skrypt można uruchomić ręcznie, bez agenta. SKILL jest wygodną nakładką, ale nie jest niezbędny — co bardzo upraszcza testowanie.

Wnioski

Trzy obserwacje z tego małego projektu:

  • Niedeterminizm AI trzymaj tam, gdzie nie szkodzi. Model genialnie maluje tło, ale nie zawsze radzi sobie z dokładnym rozmiarem, czcionką i polskimi znakami. Oddanie tła AI, a reszty deterministycznemu skryptowi, daje wynik, który jest jednocześnie ładny i powtarzalny co do piksela.
  • Spójność to nie kwestia modelu, lecz stałego promptu. Tożsamość wizualna serii siedzi w skrypcie (sekcje STYLE i STRICT CONSTRAINTS), a nie w zmiennym briefie. Dlatego dziesiąty artykuł będzie pasował do pierwszego, niezależnie od tego, co akurat wymyśli model.
  • Jedno źródło prawdy się opłaca. Wydzielenie logo do og-logo.mjs sprawia, że marka jest wszędzie identyczna, a jej zmiana to edycja jednego pliku. Ta sama zasada, którą stosuję w kodzie aplikacji, działa równie dobrze przy grafikach.

Najfajniejsze jest jednak to, że dodanie grafiki do nowego artykułu to teraz jedno polecenie. Nie szukam stock-ów, nie walczę z kadrowaniem, nie poprawiam fontów. Piszę treść, odpalam SKILL-a i mam spójną, profesjonalną grafikę — a obraz, który właśnie oglądasz w nagłówku tego artykułu, powstał dokładnie tą drogą.