Skip to main content

Step 1 — Print the fragment from your server

This is the step that matters for SEO. Your server requests the fragment and prints the response into your HTML, exactly as it comes.

One URL for everything: saz-ssr-server.public.shopadvizor.com

It serves both the fragment/JSON endpoints and the optional browser bundle (Step 2). Shopadvizor deploys and versions it — you don't host it.

GET https://saz-ssr-server.public.shopadvizor.com/v1/fragment?ean=3175681302679&lang=fr&schema=full&name=Biscuit%20Amande%20Gerbl%C3%A9
Authorization: Basic <your credential>

The response is a self-contained HTML fragment: the widget with its styles, plus a <script type="application/ld+json"> block. Print it verbatim — do not parse it, do not re-wrap it, do not sanitise it.

The credential​

Shopadvizor gives you a credential, and you send it on every request as the Authorization header. It is checked by Shopadvizor's own system, not by the rating service, which simply passes it along.

It reaches you already encoded, ready to go after Basic : the header is Authorization: Basic <your credential>, with the value exactly as you received it. Do not encode it again: a re-encoded value is rejected, and the widget comes back empty.

Two things about it:

  • It is a whole credential on its own. Keep it where you keep your other API secrets — environment variable or secret manager, never in your repository, never in anything the browser can read. Sending it from server-side code only, as every example below does, is what keeps that true.
  • It may not be enforced yet. Some deployments accept requests without it during onboarding. Send it from day one anyway: the day it is enforced, an integration that never sent it gets 401 and the widget disappears from the page — silently, because the response body is empty on purpose so nothing broken is printed inside your HTML.

Where the credential is enforced, a missing header, or one that does not start with Basic , gets 401 with an empty body. Until then the same mistake goes unnoticed — the request is still answered — and only shows the day enforcement starts, so check your header now. A credential that is sent correctly but not accepted gives an empty 200 instead, like an outage. In either case check the header before suspecting anything else, and if the credential looks right, ask Shopadvizor: they are the ones who issued it and the only ones who can tell you whether it is still valid.

Examples​

<?php
$ean = get_post_meta(get_the_ID(), '_ean', true);
$name = get_the_title();
$url = 'https://saz-ssr-server.public.shopadvizor.com/v1/fragment?'
. http_build_query(['ean' => $ean, 'lang' => 'fr', 'schema' => 'full', 'name' => $name]);

$response = wp_remote_get($url, [
'timeout' => 5,
// SHOPADVIZOR_CREDENTIAL comes from wp-config.php or your secret manager —
// never from a file you commit, and never from anything the browser sees.
// The credential is given to you already encoded: send it after "Basic ", as it is.
'headers' => ['Authorization' => 'Basic ' . SHOPADVIZOR_CREDENTIAL],
]);
if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
echo wp_remote_retrieve_body($response); // print as-is
}
?>
Next.js: two things to know
  • Load hydrate.js on pages that use this component. On the first page load the browser reads the fragment's HTML and the widget shows. After a navigation inside your app, React inserts it with innerHTML, which does not read Declarative Shadow DOM, so without hydrate.js the widget stays blank. With it, the widget draws itself from the data embedded in the fragment.
  • Do not keep an empty response for long. During an outage the service answers 200 with an empty body, and Next.js caches it like any other. That is why the example uses a short revalidate (5 minutes) and prints nothing when the body is empty: a longer value would keep the widget missing for that long after the service is back.

Any other server-side template: fetch the URL from your server with the Authorization header, print the body into the page. That is all there is to it. Django, Rails, Laravel — the mechanism is the same.

Nginx / Apache SSI is the exception. An SSI directive in a template cannot add a header, so the header goes in the server configuration instead, on the location that proxies to the rating service. Then your SSI include points at your own path (/shopadvizor/fragment?ean=…&lang=fr, plus &variant=reviews for the carousel) and the credential is added on the way out.

The proxy forwards only /fragment, and only for SSI includes: a visitor who requests that path from a browser is refused, so nobody can use your credential through your site. Keep the credential itself out of any configuration file you commit, as the examples show.

There is deliberately no way to pass the credential in the query string — it would end up in access logs, in Referer headers and in analytics, and it would fragment every cache along the path.

If your page already has Product markup​

Same call, different parameters — and you pass the @id of your own node instead of a name:

GET https://saz-ssr-server.public.shopadvizor.com/v1/fragment?ean=3175681302679&lang=fr&schema=rating-only&productId=https%3A%2F%2Fexample.com%2Fp%2F123%23product
Authorization: Basic <your credential>

What we emit then is deliberately minimal — no @type, because re-declaring it would fork the entity instead of merging into yours. Each widget page shows the exact block: ratings widget · reviews carousel.

Our block carries the same @id as yours, so both collapse into one entity and the rating attaches to your product. If your Product node has no @id, add one first — the canonical product URL with a fragment, as above. It is a single line and it is what makes this work.

Why productId is not optional

Search engines show a review snippet only when the rating is attached to a supported entity, and separate JSON-LD blocks on a page do not merge on their own. A bare AggregateRating is an orphan node: valid JSON, ignored. Without productId we still emit that fallback — useful if you post-process it — but it will not earn a rich result.

A third option, if neither fits​

schema=none prints the widget with no JSON-LD, and you write the structured data yourself. GET /v1/data gives you the numbers as JSON to nest inside your own Product node:

GET https://saz-ssr-server.public.shopadvizor.com/v1/data?ean=3175681302679
Authorization: Basic <your credential>
{ "ean": "3175681302679", "average": 4.513776337115073, "total": 3085, "counters": [...],
"name": "Biscuit Amande Gerblé", "brand": "Gerblé" }
Reviews carousel

/v1/data returns the rating numbers but not the reviews. With the carousel, schema=none is only worth it if you already publish markup for these same reviews.

Same credential, same rule: call it from your server, not from the browser. It answers with Access-Control-Allow-Origin: * so a browser can call it, but doing so would mean shipping your credential to every visitor.

name is Shopadvizor's catalogue product name. It is absent when the catalogue has none for that EAN, so treat it as optional — and prefer your own title when you have one: the markup should match what your page displays. brand is the catalogue brand, with the same availability. There is no site field here: it only appears on the Spanish service.

Most control, most work — and immune to every caveat above. Two things to respect if you take this path: round the average the same way you display it (a rating that disagrees with the visible one is a policy problem, not a rounding detail), and use a dot decimal in the markup regardless of your display locale.