Next.js and Vercel Shop

@meisei/widgets/next renders the React widgets on the server. Reviews, ratings and their structured data are in the HTML your shopper's browser receives, and the browser makes no request for the first view.

This page covers the App Router, then the Pages Router, then what to change in Vercel Shop and in Next Commerce, the template it replaced.

Install

npm install @meisei/widgets

The package works with React 19 and the App Router. It does not import anything from next, so it also runs under the Pages Router, Remix or any other server.

Set your keys

Make both keys on Settings → API keys, then put them in .env.local:

MEISEI_API_URL=https://your-store.meisei.app/api
MEISEI_SECRET_KEY=sk_live_...
NEXT_PUBLIC_MEISEI_PUBLISHABLE_KEY=pk_live_...
NEXT_PUBLIC_SITE_URL=https://www.example.com

NEXT_PUBLIC_SITE_URL is your storefront's own address. The structured data on a product page names the page by URL, and a server component cannot see the address it is answering, so this is where it comes from. On Vercel you can leave it out, and the widgets use the production host Vercel sets instead.

The secret key stays on the server, and the server rendering uses it. It counts against your store's API allowance rather than the publishable key's per visitor limit, which server rendering for every shopper would use up in a minute. The publishable key is the one that reaches the browser, for the things a shopper does after the page loads: sorting, voting, writing a review. See Calling the widget API from a server and Rate limits.

MEISEI_API_URL is your store's own address with /api on the end. Copy it from Settings → General, under Address. There is no default, because your store's data is served from your store's own address. If this is missing, the widgets say what to set and stop. You can also pass it as the provider's baseUrl prop.

The widget endpoints need no particular permission on the secret key. Signing a shopper in does: give the key customers:write if you pass customerEmail below.

Add the provider

One provider in app/layout.tsx covers the whole app. It reads the environment variables above, so it usually takes no props at all.

import { MeiseiNextProvider } from "@meisei/widgets/next"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <MeiseiNextProvider>{children}</MeiseiNextProvider>
      </body>
    </html>
  )
}

It also links the stylesheet your appearance settings produce, so the widgets match your store without a build step. Pass theme={false} if you would rather style them yourself.

Pass agentTools to offer the widgets' tools to an AI agent in the shopper's browser. See AI agents on your storefront.

Put reviews on a product page

import { Rating, Reviews } from "@meisei/widgets/next"

export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params

  return (
    <>
      <Rating productSlug={slug} />
      <Reviews productSlug={slug} />
    </>
  )
}

Reviews, Rating, ReviewsCarousel, MediaGrid, Loyalty and LoyaltyPoints are server components: each one loads its own data, then hands it to the widget. They take the same props as the connected widgets in React widgets, plus revalidate and cache. Reviews passes context and pageContext through to the instant answer box. On the server there is no page to read, so pass pageContext yourself if the product copy is not in the page's structured data.

The page URL in the structured data is your site URL plus /products/{slug}. If your product pages live somewhere else, tell the provider once with productPath="/product/{slug}", or pass productUrl on a widget for one page. A relative productUrl is resolved against your site URL.

The widgets that only run in the shopper's browser are re-exported from the same place, because there is nothing for a server to render first: AddToWishlistWidget, CustomerWishlistsWidget, WishlistsModalWidget, WishlistBannerWidget and LoyaltyLauncherWidget.

What the server renders

  • The reviews, the summary and the rating are in the HTML, so a shopper on a slow connection reads them before the JavaScript arrives and nothing shifts when it does.
  • The Product structured data is in the HTML too, which is what a search engine that does not run JavaScript reads, with the page URL built as described above.
  • Every server widget on a page asks Meisei for its data in the same request, so a page with reviews, ratings and the loyalty panel is one call to Meisei, not one per widget.
  • The browser makes no request for that first view. Sorting, filtering, Load more, voting and writing a review still go straight to Meisei from the browser.

Signed-in shoppers

Give the provider the email address of whoever is signed in. Meisei signs it on the server and hands the browser only the signature, never the key that made it.

<MeiseiNextProvider customerEmail={session?.user.email}>{children}</MeiseiNextProvider>

That is what puts a shopper's own wishlists, points and rewards on the page, what fills their email address in when they write a review, and what lets a review be matched to their orders for the verified buyer badge. <Reviews> loads with it too, so a store that only accepts verified buyers renders the right form, or the note in its place, on the server. Leave it out for a guest.

Caching and revalidation

Server rendering asks Meisei for the data on every request unless you say otherwise:

<Reviews productSlug={slug} revalidate={60} />

revalidate and cache go straight to fetch, so everything Next.js does with them applies. Sixty seconds is a reasonable starting point for a product page, because reviews do not change often. A shopper who has just written one sees it right away either way, because that request comes from their browser.

The Pages Router

There are no server components under the Pages Router, so load the data in getServerSideProps with the loader for the widget, and pass it to the widget as initialData:

import { MeiseiProvider, loadProductReviews, ProductReviewsWidget } from "@meisei/widgets"

export const getServerSideProps = async ({ params }) => ({
  props: {
    baseUrl: process.env.MEISEI_API_URL,
    initialData: await loadProductReviews({
      baseUrl: process.env.MEISEI_API_URL,
      apiSecretKey: process.env.MEISEI_SECRET_KEY!,
      productSlug: params.slug,
    }),
  },
})

export default function ProductPage({ baseUrl, initialData }) {
  return (
    <MeiseiProvider
      baseUrl={baseUrl}
      apiPublishableKey={process.env.NEXT_PUBLIC_MEISEI_PUBLISHABLE_KEY!}
    >
      <ProductReviewsWidget
        productSlug={initialData.reviews.product.slug}
        initialData={initialData}
      />
    </MeiseiProvider>
  )
}

loadProductReviews, loadProductRating, loadReviewsCarousel, loadMediaGrid, loadLoyalty and loadLoyaltyEarnable are plain async functions with no framework in them. loadProductRating takes a list of slugs, so a category page loads every rating on it in one request. See React widgets for the whole list.

Vercel Shop

Vercel Shop is Vercel's Shopify storefront template, the successor to Next Commerce. Its product pages live at /products/[handle], which is where the widgets look by default, and its product handle is the slug Meisei stores, so there is nothing to map. Against the repository as it stands on 17 October 2026 (Next.js 16.4, React 19.3):

  1. Install and set your keys. pnpm add @meisei/widgets, then the variables from Set your keys in .env.local and in your Vercel project's environment variables. Off Vercel, set NEXT_PUBLIC_SITE_URL to the same origin as the template's site.url.
  2. app/layout.tsx: wrap everything inside <CartProviderWrapper> in <MeiseiNextProvider>. Add <LoyaltyLauncherWidget> after <Footer /> if you want the Rewards button on every page.
  3. app/products/[handle]/page.tsx: in ProductPageContent, add <Reviews productSlug={product.handle} /> after <ProductDetailSection>. It is already inside the page's <Suspense>, which Cache Components asks of anything loaded per request.
  4. components/product-detail/product-detail-section.tsx: add <Rating productSlug={handle} /> under the <h1> in ProductInfoArea, and <AddToWishlistWidget productSlug={handle} /> beside the buy buttons if you use wishlists.
  5. Keep one Product block. ProductDetailSection renders the template's ProductSchema. Pass structuredData={false} to Reviews to keep it, or remove <ProductSchema> and keep Meisei's, which carries the rating and the reviews.

Vercel Shop turns on Cache Components, so the page's static shell is served first and anything loaded per request streams in after it. The widgets are part of what streams, so the reviews are fresh on every visit and each product page view is one request to Meisei against your store's API allowance.

If you turn on Shopify customer accounts (auth.isEnabled), pass the signed-in shopper's email from getCustomerProfile() as customerEmail, as in Signed-in shoppers. Reading the session is per request, so it belongs inside a <Suspense> boundary, not in the root layout's static shell.

Connect the same store in Settings → Shopify so review requests go out after each order. See Connecting Shopify.

Next Commerce

Next Commerce is Vercel's Shopify storefront. Its product handle is the same slug Meisei stores, so product.handle is what every widget takes. Three files, against the repository as it stands on 9 September 2026 (Next.js 15.6, React 19):

  1. app/layout.tsx: wrap the existing <main> in <MeiseiNextProvider productPath="/product/{slug}">. Next Commerce's product pages are singular, so the default path would name a page that is not there. Its site URL is Vercel's production host, so there is nothing else to set.
  2. app/product/[handle]/page.tsx: add <Reviews productSlug={product.handle} /> below the gallery and description. The page already emits its own product structured data, so pass structuredData={false} to keep one Product block on the page, or drop the template's and keep Meisei's, which carries the rating and the reviews.
  3. components/product/product-description.tsx: add <Rating productSlug={product.handle} /> under the title, beside the price.

Connect the same store in Settings → Shopify so the products, orders and customers behind those reviews stay in step. See Connecting Shopify.

If something isn't working

  • "Server rendering with the publishable key" in your server log. MEISEI_SECRET_KEY is not set, so the rendering is using the browser key and will be rate limited by visitor. Set it and restart.
  • The widgets are empty and your server log has a Meisei error. A loader that fails never fails the page. The widget falls back to loading in the browser. The log line says which request it was.
  • "only works in a Client Component". Import the widget from @meisei/widgets/next, or mark your own file "use client" if you are composing a widget from its parts.
  • No styling. Check that the theme stylesheet loads, or import @meisei/widgets/theme.css yourself if you turned it off.

Next steps