# Next.js

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

Source: /en/integrate/nextjs/

This page describes how to implement the [SSR](/en/integrate/server-rendering.md) approach in the Next.js App Router. No additional package is required; `next` and `react` are sufficient.

## Environment variables

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

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

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

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

## 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](/en/integrate/server-rendering.md#structured-data-json-ld).

```tsx
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)

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

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

## 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](/en/integrate/signed-in-customers.md#order-detail-page). Take the session information from your own authentication library; `auth()` here is only an example.

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

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

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