# Headless kullanım

> Yorum özetinin ve listesinin kendi tasarımınızla gösterilmesi; yorum yazma akışının Sayproof'a bırakılması.

Kaynak: /tr/integrate/headless/

Yorum bölümünün görünümünü tamamen kendiniz belirlemek istiyorsanız veriyi Sayproof'tan alır; özeti, listeyi, sıralamayı ve filtreleri kendi bileşenlerinizle gösterirsiniz. Bu kullanım biçimine **headless** denir. Yöntem framework'ten bağımsızdır: düz JavaScript, React, Vue, Svelte ya da sunucu tarafında çalışan herhangi bir template engine ile uygulanabilir.

Görev dağılımı:

| Marka tarafında | Sayproof tarafında |
|---|---|
| Özet, liste, yorum kartı, sıralama, filtreler, sayfalama, oy ve bildirim düğmeleri | Sipariş doğrulaması, yorum formu, fotoğraf ve video yükleme, davet ve düzeltme bağlantıları |

Yorum yazma akışı script'te kalır. Böylece doğrulama kuralları, onay metinleri ve kötüye kullanım korumaları her markada aynı biçimde çalışır ve formun sipariş doğrulanmadan açılmaması güvence altına alınır.

:::caution[Zorunlu öğeler]
Varsayılan bileşende bazı öğeleri Sayproof gösterir ve bu öğeler kaldırılamaz. Headless kullanımda bu öğeleri sizin göstermeniz gerekir. Liste için: [Zorunlu öğeler](/tr/customize/required-elements.md).
:::

## Container ve script

Container'a `data-review-mode="headless"` attribute'unu ekleyin. Script bu container'da özet, liste ve sıralama oluşturmaz; yalnızca yorum yazma akışını yürütür ve davet bağlantılarını işler.

```html
<div id="reviews"
     data-review-mount
     data-review-mode="headless"
     data-review-product="URUN-KODU"
     data-review-scope="variant"
     data-review-locale="tr"></div>

<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>
```

Script bu container'ın içine herhangi bir içerik eklemez; container'ın sayfada bulunması yeterlidir. Yorum formu ve sipariş doğrulaması sayfanın üzerinde açılan bir dialog içinde gösterilir.

## Verinin alınması

Veri iki kaynaktan alınabilir; iki kaynağın alanları aynıdır:

- **Sunucuda:** [SSR](/tr/integrate/server-rendering.md) yanıtındaki `state` alanı. Liste sunucuda oluşturulur ve arama motorları metni ilk yanıtta görür. Aynı yanıttaki `state_script` etiketini container'ın **doğrudan alt öğesi** olarak ekleyin; script arayüz metinlerini ve zorunlu öğeleri ağ isteği yapmadan buradan alır.
- **Tarayıcıda:** `GET /v1/reviews/list` endpoint'i. Site anahtarı URL'de gönderilir ve özel bir header kullanılmaz; bu nedenle tarayıcı preflight isteği yapmaz.

```js
const base = "https://api.sayproof.example";
const siteKey = "SITE-ANAHTARINIZ";

export async function listReviews({ product, scope = "variant", lang = "tr", sort, page = 1, filters = {} }) {
  const query = new URLSearchParams({ key: siteKey, product, scope, lang, page: String(page) });
  if (sort) query.set("sort", sort);
  if (filters.rating) query.set("rating", String(filters.rating));
  if (filters.verified) query.set("verified", "1");
  if (filters.media) query.set("media", "1");
  if (filters.q) query.set("q", filters.q);
  if (filters.topic) query.set("topic", filters.topic);
  const answer = await fetch(`${base}/v1/reviews/list?${query}`, { credentials: "omit" });
  if (!answer.ok) throw new Error(`sayproof_list_${answer.status}`);
  return answer.json();
}
```

Yanıtta kullanacağınız başlıca alanlar:

| Alan | İçerik |
|---|---|
| `aggregate` | `count`, `average`, `stars_filled` (dolu yıldız sayısı; yarım değerler yukarı yuvarlanır), `distribution`, `verified_count`, `imported_count`, `imported_label`, `structured_count`, `structured_average` (son ikisi aktarılmış yorumlar hariç sayı ve ortalamadır; yapılandırılmış veri bunlardan hesaplanır) |
| `reviews[]` | `id`, `rating`, `title`, `body`, `author`, `published_at`, `verified`, `helpful_count`, `unhelpful_count`, `aspects`, `media[]` (`url`, `srcset`, `width`, `height`, `alt_text`, `kind`, `poster_url`), `seller_response`, `resolution`, `origin`, `experience` (`process` ise yorumu `words.experience_process` etiketiyle gösterin) |
| `sorts` | İzin verilen sıralama seçenekleri ve etiketleri; sıralama menünüz yalnızca bu seçenekleri sunmalıdır |
| `aspects`, `topics` | Değerlendirme başlıklarının ortalamaları ve yorumlarda en sık geçen kelimeler |
| `gallery`, `media_reviews` | Ürünün yorumlarındaki en yeni fotoğraf ve videolar (her biri yorumun puanı ve yazarıyla) ve fotoğraflı ya da videolu yorum sayısı |
| `core` | Zorunlu öğeler: `method_statement.url` ve `label`, `sort.allowed` ve `sort.default` |
| `words` | İstenen dildeki arayüz metinleri: `verified`, `seller_response`, `resolution`, `count`, `statement`, `search`, `filter_verified`, `filter_media`, `gallery`, `helpful_question`, `helpful_yes`, `helpful_no`… |
| `report_reasons` | Bildirim gerekçeleri ve etiketleri |
| `page`, `pages`, `page_size`, `total` | Sayfalama bilgileri |

Sonraki sayfaları isterken yanıttaki `page_size` değerini geri gönderin. Tüm alanlar ve filtre kuralları için: [Storefront API](/tr/reference/storefront-api.md#yorum-listesi).

:::danger[Yorum metnini metin olarak ekleyin]
`title`, `body`, `author` ve satıcı yanıtı, müşteri ya da mağaza tarafından yazılmış metinlerdir. Bu alanları `innerHTML` ile değil, `textContent` ile (React'te normal `{…}` ifadesiyle) ekleyin.
:::

## Oy ve bildirim

"Faydalı" oyu:

```js
export async function vote(reviewId, direction, visitorKey) {
  const answer = await fetch(`${base}/v1/reviews/${encodeURIComponent(reviewId)}/helpful`, {
    method: "POST",
    credentials: "omit",
    headers: { "Content-Type": "application/json", "X-Sayproof-Site-Key": siteKey },
    body: JSON.stringify({ vote: direction, visitor_key: visitorKey }),
  });
  if (!answer.ok) throw new Error(`sayproof_vote_${answer.status}`);
  return answer.json();
}
```

- `vote` değeri `up`, `down` ya da `none` olabilir. Seçili yöne yeniden basıldığında `none` gönderin; oy geri alınır.
- Ekranda ve kendi kayıtlarınızda yanıttaki `vote`, `helpful_count` ve `unhelpful_count` değerlerini kullanın; sonucu tarayıcıda tahmin etmeyin.
- `visitor_key`, aynı ziyaretçinin oylarını tekilleştirmek için kullanılır; tarayıcıda saklanan rastgele bir değer yeterlidir.
- Varsayılan bileşen ziyaretçinin oylarını `localStorage` içinde `sayproof-votes` anahtarında tutar (yorum kimliği → `up` / `down`). Aynı anahtarı kullanırsanız iki arayüz aynı oy kaydını paylaşır.

Bildirim: `POST /v1/reviews/<id>/report`, body `{"reason": "…", "note": "…"}`. Gerekçe listesini `report_reasons` alanından alın; client tarafında sabit olarak tanımlamayın.

## "Yorum yaz" düğmesi

Düğmeniz `window.Sayproof.write(container)` metodunu çağırır. Sonuç:

| Müşteri durumu | Açılan içerik |
|---|---|
| Oturum açmış müşteri yorum yazabilir (`can_write`) | Yorum formu, sipariş numarası sorulmadan |
| Bu ürün için zaten yorum yazmış (`written`) | Hiçbir şey; metot `false` döndürür |
| Bilinmiyor ya da oturum yok | Sipariş numarası ve e-posta adresi istenir; koşullar sağlanırsa form aynı dialog içinde açılır |

Container hazır değilse (`data-review-ready` attribute'u yoksa) ya da başka bir dialog açıksa metot `false` döndürür. Oturum açmış müşteri için: [Oturum açmış müşteri](/tr/integrate/signed-in-customers.md).

Gönderim başarılı olduğunda container, bubbling özellikli `sayproof:submitted` event'ini yayar. Yorum onaylandıktan sonra yayınlandığı için listeyi hemen yenilemeniz gerekmez.

```js
const container = document.querySelector("#reviews");
document.querySelector("#write-review").addEventListener("click", () => window.Sayproof?.write(container));
container.addEventListener("sayproof:submitted", (event) => showThanks(event.detail.product));
```

## Örnek: düz JavaScript

Özet, sıralama, liste ve zorunlu bağlantı. Metinler yanıttaki `words` alanından alınır.

```js
function node(tag, text, attributes = {}) {
  const element = document.createElement(tag);
  if (text) element.textContent = text;
  for (const [name, value] of Object.entries(attributes)) element.setAttribute(name, value);
  return element;
}

function fill(template, values) {
  return template.replace(/\{(\w+)\}/g, (_, name) => String(values[name] ?? ""));
}

function renderReviews(root, answer, onSort) {
  const { aggregate, core, words, sorts } = answer;

  const summary = node("p", `${aggregate.average} · ${fill(words.count, { count: aggregate.count })}`);
  const statement = node("a", core.method_statement.label, { href: core.method_statement.url, target: "_blank", rel: "noopener" });

  const select = node("select", "", { "aria-label": words.sort });
  for (const sort of sorts) {
    const option = node("option", sort.label, { value: sort.code });
    if (sort.code === answer.sort) option.selected = true;
    select.append(option);
  }
  select.addEventListener("change", () => onSort(select.value));

  const list = node("ol");
  for (const review of answer.reviews) {
    const item = node("li", "", { "data-review-id": review.id });
    item.append(node("strong", `${review.rating}/5`));
    if (review.verified) item.append(node("span", words.verified));
    if (review.title) item.append(node("h3", review.title));
    if (review.body) item.append(node("p", review.body));
    if (review.seller_response) item.append(node("blockquote", `${words.seller_response}: ${review.seller_response.body}`));
    if (review.resolution) item.append(node("aside", `${words.resolution}: ${review.resolution.body}`));
    list.append(item);
  }

  root.replaceChildren(summary, select, list, statement);
}
```

## Örnek: React

```tsx
import { useEffect, useState } from "react";
import { listReviews } from "./sayproof";

export function Reviews({ product, locale }: { product: string; locale: string }) {
  const [answer, setAnswer] = useState(null);
  const [sort, setSort] = useState<string | undefined>();

  useEffect(() => {
    let alive = true;
    listReviews({ product, lang: locale, sort }).then((next) => alive && setAnswer(next), () => alive && setAnswer(null));
    return () => {
      alive = false;
    };
  }, [product, locale, sort]);

  if (!answer) return null;
  const { aggregate, core, words, sorts, reviews } = answer;

  return (
    <section>
      <p>{aggregate.average} · {words.count.replace("{count}", String(aggregate.count))}</p>
      <select aria-label={words.sort} value={sort ?? answer.sort} onChange={(event) => setSort(event.target.value)}>
        {sorts.map((one) => (
          <option key={one.code} value={one.code}>{one.label}</option>
        ))}
      </select>
      <ol>
        {reviews.map((review) => (
          <li key={review.id}>
            <strong>{review.rating}/5</strong>
            {review.verified ? <span>{words.verified}</span> : null}
            {review.title ? <h3>{review.title}</h3> : null}
            {review.body ? <p>{review.body}</p> : null}
            {review.seller_response ? <blockquote>{words.seller_response}: {review.seller_response.body}</blockquote> : null}
            {review.resolution ? <aside>{words.resolution}: {review.resolution.body}</aside> : null}
          </li>
        ))}
      </ol>
      <a href={core.method_statement.url} target="_blank" rel="noopener">{core.method_statement.label}</a>
    </section>
  );
}
```

İlk görüntüleme için SSR kullanıyorsanız `answer` başlangıç değerini sunucudan gelen `state` ile verin; sayfa ilk yanıtta dolu olarak gelir.

## Görüntülenme sayımı

Script, headless kullanılan container'lar için de ürün görüntülenmelerini sayar; ek bir işlem gerekmez.
