Tüm yazılar
03 · Arama ve GEO · 2026.10 · 6 dk

Satori ile her sayfaya ve her dile ayrı paylaşım kartı

Bu sitede her sayfanın Türkçe ve İngilizce iki ayrı 1200×630 paylaşım kartı var ve hepsi build sırasında Satori ile çiziliyor. İşin zor kısmı düzen değil fontlar oldu: ş, ğ ve İ önce kutu olarak çıktı. Çözüm, font alt kümelerini bırakıp tam TTF dosyalarına geçmekti.

01Neden sayfa başına bir kart

Başlangıçta sitenin tek bir paylaşım görseli vardı, public/og.png. Hangi sayfanın bağlantısını paylaşırsanız paylaşın, sohbet uygulamasında aynı görsel çıkıyordu. Bir yazıyı paylaşan biri için bu pek bir şey söylemiyor. İstediğim, kartın sayfanın kendisini anlatmasıydı: bölüm etiketi, başlık, kısa açıklama ve sitede o sayfaya ait olan kapak çizimi. Site iki dilli olduğu için her sayfanın iki kartı olmalıydı. og.png hâlâ duruyor; 404 sayfası ve JSON-LD'deki Person görseli onu kullanıyor.

02Ağaç, SVG, PNG

Satori, React benzeri bir eleman ağacını ve font dosyalarını alıp bir SVG üretiyor. Ağacı JSX ile yazmak zorunlu değil; type ve props alanları olan düz nesneler yetiyor. scripts/og.mjs bu nesneleri küçük bir h yardımcısıyla kuruyor. Düzen, Satori'nin desteklediği flexbox alt kümesiyle yapılıyor. Ortaya çıkan SVG'yi @resvg/resvg-js 1200 piksel genişliğe sığdırıp PNG'ye çeviriyor. loadSystemFonts: false ayarı, build alan makinedeki fontların karta sızmasını engelliyor; kart her makinede aynı fontlarla çiziliyor.

scripts/og.mjs
const h = (type, style, ...children) =>
  ({ type, props: { style, children: children.length === 1 ? children[0] : children } })

const svg = await satori(tree, { width: 1200, height: 630, fonts })
return new Resvg(svg, {
  fitTo: { mode: 'width', value: 1200 },
  font: { loadSystemFonts: false }
}).render().asPng()

Kapak çizimi, sitenin tarayıcıda çizdiği prosedürel şekil listesinin aynısı. artOf ile gelen dikdörtgenler ve elipsler bir SVG'ye yazılıyor ve karta veri URI'si olarak, bir img elemanıyla yerleşiyor. Şekillerin içinde metin olanlar ayrılıyor ve Satori ile ayrıca diziliyor; böylece onlar da sitenin fontuyla çiziliyor. Çok ince çizgiler en az yaklaşık 1,2 piksel kalınlıkta tutuluyor ki rasterleştirmede kaybolmasınlar.

Hangi kartların çizileceğine scripts/seo-files.mjs içindeki ogEntries karar veriyor: her dil için, her herkese açık sayfaya bir kayıt. Kayıtta bölüm etiketi, başlık, açıklama ve kapak adı var. Listede karşılığı olmayan bir sayfa varsa fonksiyon hata fırlatıyor; kartı unutulmuş bir sayfa build'i durduruyor. Dosya adını ogKeyFor üretiyor: ana sayfa home, /work/yee-cms work-yee-cms, İngilizce kartlar en- önekiyle. prerender.mjs bunları dist/og altına yazıyor, useSeo da og:image ve twitter:image değerlerini aynı adrese bağlıyor.

03Türkçe harfler kutu olunca

İlk denemede fontları @fontsource paketlerinden aldım. Bu paketler fontları alfabeye göre alt kümelere bölüyor; temel Latin bir dosyada, ş, ğ ve İ gibi harflerin bulunduğu genişletilmiş Latin başka bir dosyada. Tarayıcıda bu bölme mantıklı, çünkü tarayıcı yalnızca gereken dosyayı indiriyor. Satori'ye ise her dosyayı ayrı bir font olarak vermek gerekiyor. Genişletilmiş alt kümeleri ayrı aile adlarıyla kaydettim ve eksik harflerin oraya düşmesini bekledim. Olmadı: ş, ğ ve İ kartlarda kutu olarak çıktı ya da yanlış yüzle çizildi. Alt kümelere verdiğim aile adları da beklediğim gibi ayrıştırılmadı. og.mjs içindeki yorum durumu kısaca özetliyor: Satori aileler arasında güvenilir biçimde geri düşmüyor.

Çözüm, alt kümelerle uğraşmayı bırakmak oldu. npm'deki @expo-google-fonts paketleri Google Fonts ailelerini tam karakter setli TTF dosyaları olarak dağıtıyor. Her ağırlık için tek bir tam dosya olunca geri düşmeye gerek kalmıyor. Bu paketler yalnızca devDependencies içinde; tarayıcıya hiçbir zaman gönderilmiyorlar.

scripts/og.mjs
async function ttf(pkg, file) {
  const dir = path.dirname(require.resolve(`${pkg}/package.json`))
  return fs.readFile(path.join(dir, file))
}

{ name: 'Syne', weight: 800, style: 'normal',
  data: await ttf('@expo-google-fonts/syne', '800ExtraBold/Syne_800ExtraBold.ttf') }
Bir fontu parçalara bölmek tarayıcıda tasarruftur, kart üreticisinde ise hata kaynağı.

04g ve ğ için küçük bir yama

Sitede başlıklardaki g ve ğ harfleri Syne'den değil Archivo Expanded'dan geliyor; bunun hikâyesini ayrı bir yazıda anlattım. Kartların da aynı harfleri göstermesi gerekiyordu. archivoPatch fonksiyonu build sırasında Google Fonts'tan yalnızca bu iki harfi içeren bir CSS istiyor. İstekte User-Agent olarak Mozilla/4.0 gönderiyor; Google eski bir istemciye WOFF2 yerine TrueType veriyor. Satori TTF, OTF ve WOFF okuyabiliyor ama WOFF2 okuyamıyor. İki istek de 8 saniyelik bir zaman aşımına bağlı. Ağ yoksa fonksiyon bir uyarı yazıp null döndürüyor ve kartlar Syne'nin g harfiyle çiziliyor; build kırılmıyor.

Satori'de tarayıcıdaki gibi harf başına font seçimi olmadığı için displayText başlığı kendisi bölüyor. Her kelime ayrı bir flex öğesi oluyor, kelimenin içindeki g ve ğ dizileri fontFamily: 'Archivo' olan ayrı bir span'e konuyor. Kelime başına bir flex öğesi kullanmanın ikinci bir faydası var: satır hiçbir zaman bir kelimenin ortasından kırılmıyor. Kelimeler arasındaki boşluk, yazı boyutunun 0,24 katı olan columnGap ile veriliyor.

05Düzenin sınırları ve denetim

Başlıklar uzunluğa göre dört boyuttan birini alıyor: 14 karaktere kadar 88 piksel, 22'ye kadar 72, 36'ya kadar 60, daha uzunsa 50. Açıklama 150 karakterde, bir kelime sınırında kesiliyor ve sonuna üç nokta ekleniyor. Metin sütunu 610 piksel genişliğinde, sağda 420 piksellik kare bir kapak var. Alt satırda devilyxrd kelime işareti (x harfi iris renginde) ve SITE_URL'den gelen alan adı duruyor. Bölüm etiketi büyük harfle yazılıyor ve Türkçe İ kuralına uyuyor.

Build'in son adımı check-seo, her sayfadaki og:image ve twitter:image adreslerinin mutlak olduğunu ve dosyanın var olduğunu kontrol ediyor. Sonra PNG başlığındaki genişlik ve yükseklik değerlerini okuyup 1200×630 olup olmadığına bakıyor. Geliştirme sırasında da kartlar görülebiliyor: vite.config.ts içindeki eklenti /og/<anahtar>.png isteklerini aynı renderOg fonksiyonuyla anında çiziyor ve fontları ilk yüklemeden sonra bellekte tutuyor.

İlgili yazılar
  1. 04Syne'nin g harfi: hata değil tasarım, yine de iki harfi değiştirdimTasarım ve etkileşim2026.105 dk
  2. 05Yükleme ekranındaki takılmayı compositor'a taşımakArayüz2026.105 dk