Display reviews
How to Embed Google Reviews in Next.js (App Router, Server-Rendered)
Render Google reviews in a Next.js Server Component: an HTML fragment or a JSON API, cached with revalidate, API key kept server-side, readable by AI crawlers.
Updated
Contents
Search for “embed Google reviews in Next.js” and you'll mostly find client-side widgets: a <script> tag or a useEffect that fetches reviews after the page loads. That throws away the main reason to use Next.js. With the App Router, a Server Component can fetch your reviews on the server, cache them, and ship them inside the HTML.
Which approach should you use?
Two approaches keep your reviews readable by Google and AI crawlers. The third one doesn't.
| Approach | Reviews in initial HTML | Readable by AI crawlers | API key | Revys plan | Design control |
|---|---|---|---|---|---|
HTML fragment (/api/embed/ID/html) in a Server Component | Yes | Yes | No | Free and Pro | Layout set in Revys |
JSON API (/api/v1/reviews) rendered with React on the server | Yes | Yes | Yes | Pro | Full: your own components |
JavaScript snippet or useEffect | No | No | No | Free and Pro | Revys iframe |
The HTML fragment is the fastest: about ten lines, and you keep the five layouts of the Google reviews widget (grid, list, carousel, wall, badge). Use the JSON API when your design system calls for your own components.
Option 1: render the HTML fragment in a Server Component
This is the code the Revys dashboard gives you for a Next.js site: the Put on my website assistant recognizes Next.js and preselects “Next.js (App Router)”. Revys returns the fully rendered widget: review markup, CSS scoped to the widget, and JSON-LD, with zero JavaScript.
Get your widget's public ID
In Revys, open My businesses › your business › On my website, click Put on my website and enter your site address. Revys recognizes Next.js (if it doesn't, pick Next.js under “Not right?”) and preselects “Next.js (App Router)” under “Your site is built with…”. The code shown already contains the URL with your ID.
Create the server component
Create
components/revys-reviews.tsx. No"use client"directive: this is a Server Component.// components/revys-reviews.tsx (Server Component) export async function RevysReviews() { try { const res = await fetch("https://revys.io/api/embed/YOUR_WIDGET_ID/html", { next: { revalidate: 3600 }, signal: AbortSignal.timeout(2000), }); if (!res.ok) return null; return <div dangerouslySetInnerHTML={{ __html: await res.text() }} />; } catch { return null; } }Drop it into a page
Import it wherever the reviews should appear:
// app/page.tsx import { RevysReviews } from "@/components/revys-reviews"; export default function Page() { return ( <main> <h1>Acme Plumbing</h1> <RevysReviews /> </main> ); }Check the HTML you ship
After a production build, run
curl -s https://your-site.com/ | grep -c "revys-w"or view the page source. The text of your reviews should be in it.
Three lines in that component matter:
next: { revalidate: 3600 }caches the response for an hour. Per the Next.js docs, the number is the resource's maximum cache lifetime in seconds. Revys refreshes your Google reviews about every hour, so a shorter cache buys you nothing.AbortSignal.timeout(2000)gives up after two seconds. Your page should never wait on Revys.return nullon failure: if Revys doesn't answer, the page renders without reviews instead of erroring.
dangerouslySetInnerHTML is intentional here. The fragment has no executable script and no event handlers, and Revys escapes the review text. You trust exactly one hard-coded source, revys.io.
With cacheComponents enabled (Next.js 16)
If your project turns on cacheComponents in next.config.ts, declare caching with the "use cache" directive and cacheLife, which became stable in Next.js 16. The "hours" profile revalidates every hour:
// lib/revys.ts
import { cacheLife } from "next/cache";
export async function getRevysHtml() {
"use cache";
cacheLife("hours");
const res = await fetch("https://revys.io/api/embed/YOUR_WIDGET_ID/html", {
signal: AbortSignal.timeout(2000),
});
return res.ok ? res.text() : "";
}
Your component then calls getRevysHtml() inside a try/catch and renders the HTML the same way.
Option 2: the JSON API, rendered with React on the server
GET /api/v1/reviews returns your reviews, the average rating and a ready-made schemaOrg object. It's included in the Pro plan (10,000 calls per month per listing) and authenticates with an API key.
Create an API key
In Revys, open API & developers (left-hand menu) and create a key. It looks like
rv_<prefix>_<secret>and is shown only once.Store it server-side
Add it to
.env.local(and to your host's environment variables) along with the widget's public ID:REVYS_API_KEY=rv_xxx_... REVYS_WIDGET_ID=YOUR_WIDGET_IDNo
NEXT_PUBLIC_prefix: the Next.js docs explain that those variables are inlined into the JavaScript bundle sent to the browser.Write the component
Install the
server-onlyguard (npm install server-only) so the build fails if this file ever gets imported into a client component.// components/revys-reviews.tsx (Server Component) import "server-only"; type RevysReview = { id: string; authorName: string; rating: number; text: string | null; reviewedAt: string | null; ownerReply: string | null; }; type RevysData = { reviews: RevysReview[]; aggregate: { count: number; average: number }; schemaOrg: Record<string, unknown>; }; async function getRevysReviews(): Promise<RevysData | null> { try { const res = await fetch( `https://revys.io/api/v1/reviews?widget=${process.env.REVYS_WIDGET_ID}`, { headers: { Authorization: `Bearer ${process.env.REVYS_API_KEY}` }, next: { revalidate: 3600 }, signal: AbortSignal.timeout(2000), }, ); if (!res.ok) return null; const json = (await res.json()) as { data: RevysData }; return json.data; } catch { return null; } } export async function RevysReviews() { const data = await getRevysReviews(); if (!data || data.reviews.length === 0) return null; const { reviews, aggregate, schemaOrg } = data; return ( <section aria-labelledby="reviews-title"> <h2 id="reviews-title">What our customers say</h2> <p> {aggregate.average.toFixed(1)}/5 from {aggregate.count} Google reviews </p> <ul> {reviews.map((r) => ( <li key={r.id}> <p> <strong>{r.authorName}</strong>{" "} <span aria-label={`${r.rating} out of 5`}> {"★".repeat(r.rating)} {"☆".repeat(5 - r.rating)} </span> {r.reviewedAt ? ( <time dateTime={r.reviewedAt}> {" "}· {new Date(r.reviewedAt).toLocaleDateString("en-US")} </time> ) : null} </p> {r.text ? <p>{r.text}</p> : null} {r.ownerReply ? ( <p> <em>Response from the owner:</em> {r.ownerReply} </p> ) : null} </li> ))} </ul> <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaOrg).replace(/</g, "\\u003c"), }} /> </section> ); }
API details that matter in production:
?widget=applies the widget's filters (minimum rating, number of reviews). Reviews come newest first, without the ones you've hidden.aggregatecovers all of the listing's reviews, so the average matches your real Google rating even if the widget only shows 4- and 5-star reviews.schemaOrgis anOrganizationobject with noaggregateRating, in line with Google's self-serving review rule. The.replace(/</g, "\\u003c")follows the Next.js docs' advice against tag injection.- If you go over the monthly quota, the API returns the last successful response for the same target (with an
X-Revys-Quota: exceededheader), so your reviews stay up.
With revalidate: 3600, each server instance makes at most 24 calls a day, roughly 720 a month.
Why skip client-side widgets in Next.js?
Because they cancel out server rendering. A useEffect that calls an API, or Revys's own JavaScript snippet, adds reviews after load. The initial HTML doesn't have them.
- AI crawlers read raw HTML. Vercel's December 2024 analysis found no major AI crawler that executes JavaScript.
- Google does execute JavaScript, but in a rendering queue separate from the first crawl, as its JavaScript SEO documentation explains.
- The Revys snippet injects an iframe that points to
revys.io, so the content isn't on your domain.
A client-side call would also expose your API key to every visitor. For more on what AI search engines pick up, see how reviews get you cited in AI search.
What about Astro, Node or PHP?
The same HTML fragment works with any server. The dashboard has ready-made code for Astro, Node.js/Express, Python, Ruby and a Cloudflare Worker; the PHP, HTML and other platforms page covers PHP and static sites. On WordPress, use the WordPress plugin instead. To compare every method, read how to embed Google reviews on your website. Pro starts at $7.90/month for your first listing (pricing).
FAQ
Do I need an API key to show Google reviews in Next.js?
Not for the HTML fragment: /api/embed/ID/html is public and works on the Free plan. The JSON API at /api/v1/reviews is part of the Pro plan and needs an Authorization: Bearer header.
Where should the Revys API key live in a Next.js project?
In a server-only environment variable such as REVYS_API_KEY in .env.local, read only inside a Server Component. Never prefix it with NEXT_PUBLIC_, or Next.js inlines it into the JavaScript sent to browsers.
Why not load reviews with useEffect or a client-side widget?
Because the reviews then aren't in the initial HTML. Major AI crawlers don't execute JavaScript, and Revys's JavaScript snippet loads an iframe hosted on revys.io, so the content isn't on your domain.
How many API calls does a one-hour cache use?
At most 24 calls a day per server instance, roughly 720 a month, far below the Pro quota of 10,000 calls per month per listing.
Does this work with the Pages Router?
Yes. Make the same request in getStaticProps with revalidate: 3600 (incremental static regeneration) and pass the HTML or the reviews to the page as props.
Is there a React component or npm package for Google reviews?
Revys doesn't publish an npm package. The component on this page is about 20 lines (HTML fragment) or 70 lines (JSON API) of plain React that you copy into your codebase.
Sources
Related
- Best Google reviews widgets in 2026: 6 tools testedElfsight, EmbedSocial, Featurable, Smash Balloon, Trustindex and Revys: verified pricing, free plans, and HTML vs JavaScript rendering tested Oct 19, 2026.Read
- Elfsight alternative for Google reviews: an honest comparisonElfsight vs Revys for Google reviews: pricing, view limits, tested HTML vs JavaScript rendering and AI review replies. Checked October 19, 2026.Read
- EmbedSocial alternative for Google reviews: an honest comparisonEmbedSocial vs Revys for Google reviews: per-source pricing, view limits, a tested iframe and AI review replies. Checked October 19, 2026.Read
- Trustindex alternative: an honest comparison (pricing, HTML rendering, AI replies)Trustindex vs Revys: public pricing, platforms, AI review replies and whether Google and AI crawlers can read your reviews. Tested October 19, 2026.Read
- Google reviews widget that Google and AI can readFree Google reviews widget for one profile: 5 layouts, reviews in your site's HTML, compliant JSON-LD, refreshed nightly through Google's official API.Read
- How to embed Google reviews on your website (5 methods compared)Copy-paste, JavaScript widget, WordPress plugin, server include or API: how to add Google reviews to your site so Google and AI can read them.Read