# Server-side rendering (SSR)

> Yorumların sayfanın ilk yanıtında, arama motorlarının okuyabileceği biçimde yer alması.

Kaynak: /tr/integrate/server-rendering/

Server-side rendering (SSR) kullanıldığında mağazanızın sunucusu, sayfayı oluştururken yorumların hazır HTML'ini Sayproof'tan alır ve sayfaya ekler. Tarayıcı sayfayı açtığında yorumlar sayfada zaten bulunur; script yüklendiğinde aynı içeriği yeni bir ağ isteği yapmadan devralır ve etkileşimi başlatır.

Sağladığı yararlar:

- Yorum metni sayfa kaynağında yer alır; arama motorları ve bağlantı önizlemeleri içeriği script'i beklemeden görür.
- Sayfa, ekrandaki değerlerle birebir aynı `Review` ve `AggregateRating` yapılandırılmış verisini (JSON-LD) içerir.
- Script yüklenene kadar yorum bölümü boş kalmaz ve sayfa düzeni kaymaz. HTML, yorum yazma alanını da içerir: alan `data-review-gate-state="pending"` durumunda ve düğmesi pasif olarak gelir, script devraldığında etkinleşir. Yıldız satırları, filtreler, arama, sıralama, oy düğmeleri ve fotoğraflar da script devraldığında çalışır; "Devamını oku" düğmesi gizli gelir; script metnin yüksekliğini ölçer ve metin 4 satırı aşıyorsa düğmeyi gösterir.

Bu yöntem her programlama dilinde ve framework'te uygulanabilir; sunucunuzun bir HTTP isteği yapıp dönen HTML'i sayfaya ekleyebilmesi yeterlidir. Next.js için ayrı bir sayfa bulunur: [Next.js](/tr/integrate/nextjs.md).

## Gerekenler

- Bir **sunucu anahtarı** (panelde **Kanallar › Anahtarlar**). Sipariş gönderen sisteminizin kullandığı anahtarla aynı anahtar olabilir. Anahtar yalnızca sunucuda tutulur. Bkz. [Kanal, alan adı ve anahtarlar](/tr/integrate/channels-and-keys.md).
- Kanal kodu; örneğin `web`.
- Sayfada [Sayproof script'i](/tr/integrate/script.md). SSR ile hazırlanan HTML'i etkileşimli hâle getiren script'tir.

## İstek

```http
GET https://api.sayproof.example/v1/ssr?product=URUN-KODU&channel=web&lang=tr&name=Ürün%20adı
Authorization: Bearer SUNUCU-ANAHTARINIZ
```

| Parametre | Zorunlu | Açıklama |
|---|---|---|
| `product` | Evet | Ürün kodu (en fazla 128 karakter); container'daki `data-review-product` değeriyle aynı. |
| `channel` | Evet | Kanal kodu. |
| `scope` | Hayır | `variant` (varsayılan) ya da `group`. |
| `lang` | Hayır | `tr` ya da `en`. Belirtilmezse ya da desteklenmiyorsa kanalın dili kullanılır. |
| `name` | Hayır | Ürün adı. Yapılandırılmış veride ürün adı olarak kullanılır. |
| `sort` | Hayır | Pazarın izin verdiği sıralamalardan biri. Belirtilmezse varsayılan sıralama (tarih) kullanılır. İzin verilmeyen bir değer `400` yanıtı alır. |
| `style` | Hayır | `cards` ya da `sidebar`: HTML'in hangi düzenle hazırlanacağı. Belirtilmezse kanalın panelde seçilen düzeni kullanılır. Container'da `data-review-style` kullanıyorsanız aynı değeri gönderin. |
| `theme` | Hayır | `warm`, `trust`, `editorial`, `minimal`, `soft`, `bold`, `natural` ya da `night`: HTML'in hangi hazır tasarımla hazırlanacağı. Belirtilmezse kanalın panelde seçilen tasarımı kullanılır. Container'da `data-review-theme` kullanıyorsanız aynı değeri gönderin. |

## Yanıt

```json
{
  "html": "<section data-review-root data-review-ssr …>…</section>",
  "state": { "reviews": [], "aggregate": {}, "core": {}, "words": {} },
  "state_script": "<script type=\"application/json\" data-review-state>…</script>",
  "components": [
    { "name": "summary", "where": "server" },
    { "name": "list", "where": "server" },
    { "name": "form", "where": "client" }
  ],
  "json_ld": { "@type": "Product", "aggregateRating": {}, "review": [] },
  "json_ld_script": "{…}",
  "total": 200,
  "version": "3",
  "etag": "W/\"…\"",
  "stylesheet": "https://api.sayproof.example/storefront/sayproof.css"
}
```

| Alan | Kullanımı |
|---|---|
| `html` | Container'ın içine olduğu gibi eklenecek HTML. Sunucu tarafında escape edilmiş ve stil kapsamı tanımlanmıştır. Yapısı script'in oluşturduğu bileşenle aynıdır; öğeler için: [DOM attribute'ları](/tr/reference/dom-attributes.md). |
| `state` | Aynı içeriğin veri hâli: yorumlar, özet, sıralama, zorunlu öğeler ve arayüz metinleri. Alanlar [yorum listesi](/tr/reference/storefront-api.md#yorum-listesi) yanıtıyla aynıdır. |
| `state_script` | Yalnızca `state` verisini taşıyan etiket. [Headless](/tr/integrate/headless.md) kullanımda `html` yerine bu etiket kullanılır. |
| `components` | Bileşenler ve nerede oluşturuldukları. `where` değeri `server` olanlar (özet ve yorum listesi) `html` içinde hazır gelir; `client` olan yorum formu sunucuda oluşturulmaz, müşteri formu açtığında tarayıcıdaki script tarafından oluşturulur. Bu yüzden SSR kullanılan sayfaya da script etiketi eklenir; eklenmezse yorumlar görünür ama form açılmaz. |
| `json_ld` | Yapılandırılmış veri nesnesi. |
| `json_ld_script` | Aynı nesnenin `<script>` etiketi içine güvenle yazılabilecek hâli. |
| `stylesheet` | Bileşenin temel stillerini taşıyan CSS dosyasının adresi. `html` içindeki kök öğenin (`data-review-root`) ilk alt öğesi bu dosyaya giden bir `<link rel="stylesheet" data-review-stylesheet>` etiketidir; böylece yorumlar script yüklenmeden biçimli görünür. İsterseniz aynı adresi sayfanın `<head>` bölümüne de ekleyebilirsiniz. |

## Sayfaya ekleme

`html` alanını container'ın içine ekleyin ve sayfaya script'i ekleyin. Yanıt alınamadıysa container boş bırakılır:

```js
const container = `<div data-review-mount
     data-review-product="URUN-KODU"
     data-review-product-name="Ürün adı">${reviews?.html ?? ""}</div>`;

const script = `<script type="module" src="https://cdn.sayproof.example/sayproof.js"
        data-review-site-key="SITE-ANAHTARINIZ"
        data-review-base="https://api.sayproof.example"
        data-review-locale="tr"></script>`;
```

:::caution
`html` alanını ikinci bir HTML sanitizer'dan geçirmeyin ve template engine'inizin otomatik escape işlemine bırakmayın. Sanitizer'lar `data-review-*` attribute'larını ve durum etiketini siler; bu durumda script içeriği devralamaz. Metin Sayproof tarafından zaten escape edilmiştir.
:::

## Yapılandırılmış veri (JSON-LD)

- Sayfanızda ürün için bir `Product` JSON-LD'si **yoksa** `json_ld_script` değerini olduğu gibi ekleyin:

  ```html
  <script type="application/ld+json">…json_ld_script…</script>
  ```

- Sayfanızda zaten bir `Product` JSON-LD'si **varsa** ikinci bir `Product` eklemeyin. `json_ld.aggregateRating` ve `json_ld.review` alanlarını kendi nesnenize ekleyin.

Nesneyi kendiniz metne dönüştürüyorsanız `<`, `>`, `&`, U+2028 ve U+2029 karakterlerini `\uXXXX` biçiminde escape edin: örneğin `<` karakteri `\u003c` olur. Yorum metinleri müşteriler tarafından yazılır; düz `JSON.stringify` sonucu, `</script>` içeren bir yorum nedeniyle sayfanızı bozabilir.

```js
function jsonLdScript(value) {
  return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (character) =>
    `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`
  );
}

const product = { "@context": "https://schema.org", "@type": "Product", name, sku };
if (reviews?.json_ld?.aggregateRating) {
  product.aggregateRating = reviews.json_ld.aggregateRating;
  product.review = reviews.json_ld.review ?? [];
}
const markup = `<script type="application/ld+json">${jsonLdScript(product)}</script>`;
```

## Arama motorları ve yapay zekâ aramaları

Arama motorları ve yapay zekâ arama araçları sayfanın sunucudan gelen HTML'ini okur; betikle sonradan çizilen yorumları her zaman görmez. Sunucuda hazırlanan yorumlar sayfanın ilk HTML'inde yer alır:

- Satın alması doğrulanan yorumda **Doğrulanmış alıcı** ibaresi görünür metindir; yalnız simge ya da renk değildir.
- JSON-LD yalnız schema.org'un `Review`, `Rating` ve `AggregateRating` alanlarını kullanır. Satın alma doğrulaması gibi schema.org'da karşılığı olmayan bir bilgi yapılandırılmış veriye eklenmez, görünür metinde kalır.
- Diğer pazarlardan gelen yorumlar listede kaynak pazarıyla görünür, ama `review` dizisine ve `aggregateRating` değerine girmez.

## Zaman aşımı ve hata

Sayproof'a gönderilen istek sayfanızın açılmasını hiçbir zaman geciktirmemelidir.

- İsteğe kısa bir zaman aşımı verin; **1,5 saniye** önerilir.
- Zaman aşımında ya da `200` dışındaki yanıtlarda container'ı boş bırakın. Bu durumda script yorumları tarayıcıda kendisi getirir.

| Yanıt | Anlamı |
|---|---|
| `403 refused / scope_missing` | Anahtarın sayfayı sunucuda hazırlama izni yok (panelden oluşturulan sunucu anahtarlarında bu izin her zaman vardır). Panelden yeni bir sunucu anahtarı oluşturun. |
| `403 refused / unknown_channel` | Kanal kodu yanlış ya da kanal kapalı. |
| `409 refused / no_site_key` | Kanalın kullanımda bir site anahtarı yok ve kanalın yöntem beyanı için kendi sayfanız bağlı değil; yöntem beyanı bağlantısı oluşturulamadığı için yanıt verilmez. |
| `400 invalid / product` | Ürün kodu eksik ya da çok uzun. |
| `400 sort_not_allowed` | Sıralama, pazarın izin verilen listesinde değil. |
| `400 invalid / style` | Düzen `cards` ya da `sidebar` değil. |
| `400 invalid / theme` | Tasarım `warm`, `trust`, `editorial`, `minimal`, `soft`, `bold`, `natural` ya da `night` değil. |
| `429 rate_limited` | Anahtar başına 5 dakikada 6.000 istek sınırı aşıldı. |

## Önbellek

Sunucu anahtarıyla gönderilen isteğin yanıtı `Cache-Control: private, max-age=60` ve `ETag` header'larını taşır; aynı endpoint'e tarayıcıdan site anahtarıyla istek gönderilirse `Cache-Control: public, max-age=60` döner. İsteğe son alınan `etag` değeriyle `If-None-Match` header'ı eklenirse, içerik değişmediğinde body içermeyen bir `304` yanıtı döner. Kendi tarafınızda en fazla 60 saniyelik önbellek kullanabilirsiniz; yeni yayınlanan bir yorum en geç bu süre sonunda sayfada görünür.

## Örnekler

Tüm örnekler aynı işi yapar: isteği zaman aşımıyla gönderir ve hata durumunda `null` döndürür.

### Node.js

```js
export async function sayproofReviews({ product, name, language }) {
  const query = new URLSearchParams({
    product,
    name,
    lang: language,
    channel: process.env.SAYPROOF_CHANNEL,
  });
  try {
    const answer = await fetch(`${process.env.SAYPROOF_BASE}/v1/ssr?${query}`, {
      headers: { Authorization: `Bearer ${process.env.SAYPROOF_SSR_KEY}` },
      signal: AbortSignal.timeout(1500),
    });
    return answer.ok ? await answer.json() : null;
  } catch {
    return null;
  }
}
```

### Python (Django, Flask)

```python
import os

import requests


def sayproof_reviews(product, name, language):
    try:
        answer = requests.get(
            f"{os.environ['SAYPROOF_BASE']}/v1/ssr",
            params={
                "product": product,
                "name": name,
                "lang": language,
                "channel": os.environ["SAYPROOF_CHANNEL"],
            },
            headers={"Authorization": f"Bearer {os.environ['SAYPROOF_SSR_KEY']}"},
            timeout=1.5,
        )
    except requests.RequestException:
        return None
    return answer.json() if answer.ok else None
```

Jinja ya da Django template'inde HTML'i escape etmeden ekleyin:

```jinja
<div data-review-mount data-review-product="{{ product.sku }}" data-review-product-name="{{ product.name }}">
  {% if reviews %}{{ reviews.html|safe }}{% endif %}
</div>
{% if reviews %}<script type="application/ld+json">{{ reviews.json_ld_script|safe }}</script>{% endif %}
```

### PHP

```php
function sayproof_reviews(string $product, string $name, string $language): ?array
{
    $query = http_build_query([
        'product' => $product,
        'name' => $name,
        'lang' => $language,
        'channel' => getenv('SAYPROOF_CHANNEL'),
    ]);
    $curl = curl_init(getenv('SAYPROOF_BASE') . '/v1/ssr?' . $query);
    curl_setopt_array($curl, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT_MS => 1500,
        CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('SAYPROOF_SSR_KEY')],
    ]);
    $body = curl_exec($curl);
    $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    curl_close($curl);
    return ($body !== false && $status === 200) ? json_decode($body, true) : null;
}
```

## Varyant değişikliği

Kullanıcı sayfada başka bir varyant seçtiğinde container'ın `data-review-product` değerini güncelleyin; script yeni ürünün yorumlarını kendisi getirir. Sayfa sunucudan yeniden yükleniyorsa (örneğin her varyantın ayrı bir adresi varsa) yeni `html` gelir ve script bu içeriği devralır.
