İçeriğe geç

Headless kullanım

Markdown

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

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.

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.

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

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

  • Sunucuda: SSR 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.
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.

“Faydalı” oyu:

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.

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.

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.

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

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

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);
}
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.

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