Skip to content

Server-side rendering (SSR)

Markdown

Including reviews in the page's first response, in a form search engines can read.

With server-side rendering (SSR), your store’s server fetches the ready-made HTML of the reviews from Sayproof while building the page and adds it to the page. When the browser opens the page, the reviews are already present; once the script loads, it takes over the same content without a new network request and starts the interaction.

Benefits:

  • The review text is part of the page source; search engines and link previews see the content without waiting for the script.
  • The page contains Review and AggregateRating structured data (JSON-LD) that matches the values on screen exactly.
  • The review section is not empty while the script loads, and the page layout does not shift. The HTML also contains the review writing area: it arrives in the data-review-gate-state="pending" state with its button disabled, and becomes active when the script takes over. The star rows, filters, search, sorting, vote buttons and photos also start working when the script takes over; the “Read more” button arrives hidden; the script measures the height of the text and shows the button if the text exceeds 4 lines.

This approach can be implemented in any programming language and framework; all that is required is for your server to make an HTTP request and add the returned HTML to the page. Next.js has a separate page: Next.js.

  • A server key (in the panel under Channels › Keys). It can be the same key your order-sending system uses. The key is only kept on the server. See Channel, domain and keys.
  • The channel code, for example web.
  • The Sayproof script on the page. It is the script that makes the server-rendered HTML interactive.
GET https://api.sayproof.example/v1/ssr?product=PRODUCT-CODE&channel=web&lang=en&name=Product%20name
Authorization: Bearer YOUR-SERVER-KEY
Parameter Required Description
product Yes The product code (up to 128 characters); the same as the container’s data-review-product value.
channel Yes The channel code.
scope No variant (default) or group.
lang No tr or en. If omitted or not supported, the channel’s language is used.
name No The product name. Used as the product name in the structured data.
sort No One of the sort orders permitted by the market. If omitted, the default sort order (date) is used. A value that is not permitted receives a 400 response.
style No cards or sidebar: the layout the HTML is prepared with. If omitted, the layout chosen for the channel in the panel is used. If you use data-review-style on the container, send the same value.
theme No warm, trust, editorial, minimal, soft, bold, natural or night: the built-in design the HTML is prepared with. If omitted, the design chosen for the channel in the panel is used. If you use data-review-theme on the container, send the same value.
{
"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"
}
Field Use
html The HTML to be added inside the container as it is. It is escaped on the server and its style scope is defined. Its structure is the same as the component the script builds; for the elements, see DOM attributes.
state The same content as data: reviews, summary, sorting, required elements and interface texts. The fields are the same as in the review list response.
state_script A tag carrying only the state data. In headless use, this tag is used instead of html.
components The components and where they are built. Those whose where value is server (the summary and the review list) arrive ready in html; the review form, which is client, is not built on the server but by the script in the browser when the customer opens the form. That is why the script tag is also added to a page that uses SSR; without it the reviews show but the form does not open.
json_ld The structured data object.
json_ld_script The same object in a form that can be safely written inside a <script> tag.
stylesheet The address of the CSS file with the component’s base styles. the first child of the root element (data-review-root) in html is a <link rel="stylesheet" data-review-stylesheet> to this file, so the reviews look styled before the script loads. You can also add the same address to the page’s <head>.

Add the html field inside the container and add the script to the page. If no response was received, the container is left empty:

const container = `<div data-review-mount
data-review-product="PRODUCT-CODE"
data-review-product-name="Product name">${reviews?.html ?? ""}</div>`;
const script = `<script type="module" src="https://cdn.sayproof.example/sayproof.js"
data-review-site-key="YOUR-SITE-KEY"
data-review-base="https://api.sayproof.example"
data-review-locale="en"></script>`;
  • If your page does not contain a Product JSON-LD for the product, add the json_ld_script value as it is:

    <script type="application/ld+json">…json_ld_script…</script>
  • If your page already contains a Product JSON-LD, do not add a second Product. Add the json_ld.aggregateRating and json_ld.review fields to your own object.

If you convert the object to text yourself, escape the <, >, &, U+2028 and U+2029 characters in the \uXXXX form: for example, < becomes \u003c. Review texts are written by customers; a plain JSON.stringify result can break your page because of a review containing </script>.

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

Search engines and AI search tools read the HTML that comes from the server; they do not always see reviews drawn later by the script. Server-rendered reviews are part of the page’s first HTML:

  • On a purchase-verified review, Verified buyer is visible text, not only an icon or a colour.
  • The JSON-LD only uses schema.org’s Review, Rating and AggregateRating fields. Information schema.org has no field for, such as purchase verification, is not added to the structured data and stays in the visible text.
  • Reviews from other markets appear in the list with their source market but are not included in the review array or the aggregateRating value.

A call to Sayproof must never delay the opening of your page.

  • Give the call a short timeout; 1.5 seconds is recommended.
  • On a timeout, or on any response other than 200, leave the container empty. The script then fetches the reviews in the browser.
Response Meaning
403 refused / scope_missing The key is not permitted to prepare pages on the server (server keys created in the panel always are). Create a new server key in the panel.
403 refused / unknown_channel The channel code is wrong or the channel is off.
409 refused / no_site_key The channel has no site key in use and no page of your own is linked for the channel’s method statement; no response is given because the method statement link cannot be built.
400 invalid / product The product code is missing or too long.
400 sort_not_allowed The sort order is not on the market’s permitted list.
400 invalid / style The layout is not cards or sidebar.
400 invalid / theme The design is not warm, trust, editorial, minimal, soft, bold, natural or night.
429 rate_limited The limit of 6,000 calls in 5 minutes per key has been exceeded.

The response to a call made with a server key carries the Cache-Control: private, max-age=60 and ETag headers; if the same endpoint is called from the browser with a site key, Cache-Control: public, max-age=60 is returned. If the request includes an If-None-Match header with the last etag value received, a 304 response without a body is returned when the content has not changed. You can cache on your side for up to 60 seconds; a newly published review appears on the page after this period at the latest.

All examples do the same thing: they make the call with a timeout and return null on failure.

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

In a Jinja or Django template, add the HTML without escaping:

<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 %}
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;
}

When the user selects another variant on the page, update the container’s data-review-product value; the script fetches the new product’s reviews by itself. If the page is reloaded from the server (for example because each variant has its own address), new html arrives and the script takes over that content.