Skip to content

Next.js

Markdown

SSR, script and signed-in customer integration in a store built with the Next.js App Router.

This page describes how to implement the SSR approach in the Next.js App Router. No additional package is required; next and react are sufficient.

NEXT_PUBLIC_SAYPROOF_BASE=https://api.sayproof.example
NEXT_PUBLIC_SAYPROOF_SITE_KEY=YOUR-SITE-KEY
NEXT_PUBLIC_SAYPROOF_SCRIPT=https://cdn.sayproof.example/sayproof.js
SAYPROOF_SSR_KEY=YOUR-SERVER-KEY
SAYPROOF_CHANNEL=web
  • Values with the NEXT_PUBLIC_ prefix are exposed to the browser; the site key and the addresses are not secret.
  • The server key SAYPROOF_SSR_KEY is defined without the prefix and must not be exposed to the browser.
  • If your server reaches Sayproof through a different address (for example an internal network), also define the SAYPROOF_BASE variable.

Keep the code that reads the server key in a separate file and add the line import "server-only" at the top of that file. If a client component tries to import this file, the build fails; this prevents the key from accidentally ending up in the browser bundle.

import "server-only";
const base = process.env.SAYPROOF_BASE || process.env.NEXT_PUBLIC_SAYPROOF_BASE;
export async function sayproofReviews(product: string, name: string, locale: string) {
const query = new URLSearchParams({
product,
name,
lang: locale,
channel: process.env.SAYPROOF_CHANNEL ?? "web",
});
try {
const answer = await fetch(`${base}/v1/ssr?${query}`, {
headers: { Authorization: `Bearer ${process.env.SAYPROOF_SSR_KEY}` },
next: { revalidate: 60 },
signal: AbortSignal.timeout(1500),
});
return answer.ok ? await answer.json() : null;
} catch {
return null;
}
}
import Script from "next/script";
import { sayproofReviews } from "@/lib/sayproof.server";
export default async function ProductPage({ params }) {
const product = await getProduct(params.slug);
const reviews = await sayproofReviews(product.sku, product.name, params.locale);
return (
<>
<div
key={product.sku}
data-review-mount
data-review-product={product.sku}
data-review-product-name={product.name}
data-review-locale={params.locale}
suppressHydrationWarning
dangerouslySetInnerHTML={{ __html: reviews?.html ?? "" }}
/>
<Script
id="sayproof"
type="module"
strategy="afterInteractive"
crossOrigin="anonymous"
src={process.env.NEXT_PUBLIC_SAYPROOF_SCRIPT}
data-review-site-key={process.env.NEXT_PUBLIC_SAYPROOF_SITE_KEY}
data-review-base={process.env.NEXT_PUBLIC_SAYPROOF_BASE}
data-review-locale={params.locale}
/>
</>
);
}

Points to note:

  • key={product.sku}: When the variant changes, React re-creates the container; the script detects and binds the new container.
  • suppressHydrationWarning: Disables React’s content mismatch warning, because the container’s content is managed by the script.
  • dangerouslySetInnerHTML: The html from the server is used here as it is; it must not be passed through a second sanitizer.
  • If React re-creates the container with new HTML after router.refresh(), the script detects the new content and takes it over again.
  • To show all reviews of a product group, use the group code, add the data-review-scope="group" attribute to the container and also add the scope: "group" parameter to the server call.

If your product page already contains a Product JSON-LD, do not add a second Product; add the rating and the reviews to your own object. For the escaping rule and an example, see SSR › Structured data.

const jsonLd = reviews?.json_ld?.aggregateRating
? { ...productJsonLd, aggregateRating: reviews.json_ld.aggregateRating, review: reviews.json_ld.review ?? [] }
: productJsonLd;
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: jsonLdScript(jsonLd) }} />

If you use a nonce, pass the nonce value to the <Script> component. The script also writes the same nonce value on the <style> elements it adds (#sayproof-style, #sayproof-style-form, #sayproof-style-lightbox).

To open the form for a signed-in customer without asking for an order number, a Route Handler is written and the data-review-viewer="pending" attribute is added to the container. For the complete approach and a Next.js example, see Signed-in customers.

To let customers review the items of an order inside the order detail page of the account area, ask Sayproof in a server component with the signed-in email address and the order number, and write the answer inside a data-review-order element. For the sayproofOrderState function and the answer’s values, see Signed-in customers › Order detail page. Take the session information from your own authentication library; auth() here is only an example.

import Script from "next/script";
import { auth } from "@/auth";
import { sayproofOrderState } from "@/lib/sayproof-order.server";
export default async function OrderPage({ params }) {
const session = await auth();
const state = await sayproofOrderState(params.id, session?.user?.email, params.locale);
return (
<>
{state ? (
<div
key={params.id}
data-review-order={params.id}
suppressHydrationWarning
dangerouslySetInnerHTML={{ __html: state }}
/>
) : null}
<Script
id="sayproof"
type="module"
strategy="afterInteractive"
crossOrigin="anonymous"
src={process.env.NEXT_PUBLIC_SAYPROOF_SCRIPT}
data-review-site-key={process.env.NEXT_PUBLIC_SAYPROOF_SITE_KEY}
data-review-base={process.env.NEXT_PUBLIC_SAYPROOF_BASE}
data-review-locale={params.locale}
/>
</>
);
}

The answer is personal and is fetched with cache: "no-store"; this part of the page is not cached. If the answer has no items, the element is not added at all.

If you display the review list with your own components, add the data-review-mode="headless" attribute to the container and use the state_script value instead of html. For an example, see Headless use.

The Sayproof team provides a Next.js kit that contains the approaches on this page as ready-made components: the server call and JSON-LD merging, a container for the default component, a container for headless use, the script tag, the signed-in customer route, a client for listing, voting and reporting, and for the order detail page the Sayproof call (fetchSayproofOrder()), the tag that writes the answer safely (sayproofOrderStateScript()) and the component (<SayproofOrderReviews>). fetchSayproofOrder() returns null if the answer has no items or the call fails; in that case <SayproofOrderReviews> draws nothing. The kit is copied into the store repository; it requires no package installation and compiles with es5-targeted TypeScript settings. Using the kit is optional; the code on this page does the same job.