Next.js
MarkdownSSR, 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.
Environment variables
Section titled “Environment variables”NEXT_PUBLIC_SAYPROOF_BASE=https://api.sayproof.exampleNEXT_PUBLIC_SAYPROOF_SITE_KEY=YOUR-SITE-KEYNEXT_PUBLIC_SAYPROOF_SCRIPT=https://cdn.sayproof.example/sayproof.jsSAYPROOF_SSR_KEY=YOUR-SERVER-KEYSAYPROOF_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_KEYis 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_BASEvariable.
Server call
Section titled “Server call”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; }}Product page
Section titled “Product page”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: Thehtmlfrom 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 thescope: "group"parameter to the server call.
Structured data
Section titled “Structured data”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) }} />Content Security Policy (CSP)
Section titled “Content Security Policy (CSP)”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).
Signed-in customers
Section titled “Signed-in customers”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.
Order detail page
Section titled “Order detail page”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.
Headless use
Section titled “Headless use”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.
Ready-made kit
Section titled “Ready-made kit”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.