Server-side rendering (SSR)
MarkdownIncluding 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
ReviewandAggregateRatingstructured 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.
Requirements
Section titled “Requirements”- 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.
Request
Section titled “Request”GET https://api.sayproof.example/v1/ssr?product=PRODUCT-CODE&channel=web&lang=en&name=Product%20nameAuthorization: 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. |
Response
Section titled “Response”{ "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>. |
Adding it to the page
Section titled “Adding it to the page”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>`;Structured data (JSON-LD)
Section titled “Structured data (JSON-LD)”-
If your page does not contain a
ProductJSON-LD for the product, add thejson_ld_scriptvalue as it is:<script type="application/ld+json">…json_ld_script…</script> -
If your page already contains a
ProductJSON-LD, do not add a secondProduct. Add thejson_ld.aggregateRatingandjson_ld.reviewfields 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
Section titled “Search engines and AI search”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,RatingandAggregateRatingfields. 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
reviewarray or theaggregateRatingvalue.
Timeouts and errors
Section titled “Timeouts and errors”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. |
Caching
Section titled “Caching”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.
Examples
Section titled “Examples”All examples do the same thing: they make the call with a timeout and return null on failure.
Node.js
Section titled “Node.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)
Section titled “Python (Django, Flask)”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 NoneIn 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;}Variant changes
Section titled “Variant changes”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.