# Server-side rendering (SSR)

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

Source: /en/integrate/server-rendering/

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](/en/integrate/nextjs.md).

## 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](/en/integrate/channels-and-keys.md).
- The channel code, for example `web`.
- The [Sayproof script](/en/integrate/script.md) on the page. It is the script that makes the server-rendered HTML interactive.

## Request

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

## Response

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

| 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](/en/reference/dom-attributes.md). |
| `state` | The same content as data: reviews, summary, sorting, required elements and interface texts. The fields are the same as in the [review list](/en/reference/storefront-api.md#review-list) response. |
| `state_script` | A tag carrying only the `state` data. In [headless](/en/integrate/headless.md) 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

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

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

:::caution
Do not pass the `html` field through a second HTML sanitizer, and do not leave it to your template engine's automatic escaping. Sanitizers remove the `data-review-*` attributes and the state tag, and the script can then no longer take over the content. The text has already been escaped by Sayproof.
:::

## Structured data (JSON-LD)

- If your page does **not** contain a `Product` JSON-LD for the product, add the `json_ld_script` value as it is:

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

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

## 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`, `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.

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

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

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

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

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

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

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