Afficher ses avis

Afficher les avis Google sur un site Next.js (App Router, rendu serveur)

Intégrez vos avis Google dans un site Next.js en Server Component : fragment HTML ou API JSON, cache revalidate, clé API côté serveur, lisible par les IA.

Mis à jour le

Vous avez un site Next.js et vous voulez y afficher vos avis Google sans perdre ce qui fait l'intérêt de Next : le rendu serveur. La bonne nouvelle, c'est que l'App Router est fait pour ça. Un Server Component récupère vos avis côté serveur, les met en cache, et les envoie dans le HTML de la page.

Quelle méthode choisir dans Next.js ?

Deux méthodes gardent vos avis lisibles par Google et les IA ; une troisième est à éviter.

MéthodeAvis dans le HTML initialLisible par les IAClé APIPlan RevysContrôle du design
Fragment HTML (/api/embed/ID/html) dans un Server ComponentOuiOuiNonFree et ProMise en page réglée dans Revys
API JSON (/api/v1/reviews) rendue en React côté serveurOuiOuiOuiProTotal, vos propres composants
Snippet JavaScript ou useEffectNonNonNonFree et ProIframe Revys

Le fragment HTML est le plus rapide à mettre en place : une dizaine de lignes, et vous gardez les cinq mises en page du widget d'avis Google (grille, liste, carrousel, mur, badge). L'API JSON convient si votre design system impose vos propres composants.

Méthode 1 : le fragment HTML dans un Server Component

C'est la méthode que le tableau de bord Revys vous propose pour un site Next.js : l'assistant Mettre sur mon site reconnaît Next.js et présélectionne « Next.js (App Router) » sous « Votre site est fait avec… ». Revys renvoie le widget déjà rendu : le HTML des avis, un CSS limité au widget et le JSON-LD, sans aucun JavaScript.

  1. Récupérer l'ID public du widget

    Dans Revys, ouvrez Mes établissements › votre établissement › Sur mon site, cliquez sur Mettre sur mon site et saisissez l'adresse de votre site. Revys reconnaît Next.js (sinon, choisissez Next.js sous « Ce n'est pas ça ? ») et présélectionne « Next.js (App Router) ». Le code affiché contient déjà l'URL avec votre ID.

  2. Créer le composant serveur

    Créez components/revys-reviews.tsx. Pas de directive "use client" : c'est un Server Component.

    // components/revys-reviews.tsx (Server Component)
    export async function RevysReviews() {
      try {
        const res = await fetch("https://revys.io/api/embed/VOTRE_ID_WIDGET/html", {
          next: { revalidate: 3600 },
          signal: AbortSignal.timeout(2000),
        });
        if (!res.ok) return null;
        return <div dangerouslySetInnerHTML={{ __html: await res.text() }} />;
      } catch {
        return null;
      }
    }
  3. Placer le composant dans une page

    Importez-le où les avis doivent apparaître :

    // app/page.tsx
    import { RevysReviews } from "@/components/revys-reviews";
    
    export default function Page() {
      return (
        <main>
          <h1>Mon entreprise</h1>
          <RevysReviews />
        </main>
      );
    }
  4. Vérifier le HTML servi

    Après un build de production, lancez curl -s https://votre-site.fr/ | grep -c "revys-w" ou affichez le code source de la page : le texte de vos avis doit y figurer.

Trois choix dans ce code ne sont pas décoratifs :

  • next: { revalidate: 3600 } met la réponse en cache une heure. Selon la documentation Next.js, c'est la durée de vie maximale de la ressource, en secondes. Revys récupère vos avis Google environ toutes les heures, donc un cache plus court n'apporte rien.
  • AbortSignal.timeout(2000) coupe l'appel au bout de deux secondes. Votre page ne doit jamais attendre Revys.
  • return null en cas d'erreur : si Revys ne répond pas, la page s'affiche sans les avis plutôt que de planter.

L'utilisation de dangerouslySetInnerHTML est ici voulue : le fragment ne contient ni script exécutable ni gestionnaire d'événement, et le texte des avis y est échappé par Revys. Vous faites confiance à une seule source, revys.io, que vous écrivez en dur.

Avec cacheComponents activé (Next.js 16)

Si votre projet active cacheComponents dans next.config.ts, le cache se déclare avec la directive "use cache" et cacheLife, stabilisé en Next.js 16. Le profil "hours" revalide toutes les heures :

// 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/VOTRE_ID_WIDGET/html", {
    signal: AbortSignal.timeout(2000),
  });
  return res.ok ? res.text() : "";
}

Le composant appelle alors getRevysHtml() dans un try/catch et rend le HTML de la même façon.

Méthode 2 : l'API JSON rendue en React côté serveur

L'API GET /api/v1/reviews renvoie vos avis, la note moyenne et un objet schemaOrg prêt à l'emploi. Elle est incluse dans le plan Pro (10 000 appels par mois et par fiche) et s'authentifie par une clé API.

  1. Créer une clé API

    Dans Revys, ouvrez API & développeurs (menu de gauche) et créez une clé. Elle a la forme rv_<préfixe>_<secret> et n'est affichée qu'une fois.

  2. La ranger côté serveur

    Ajoutez-la à .env.local (et aux variables d'environnement de votre hébergeur), avec l'ID public du widget :

    REVYS_API_KEY=rv_xxx_...
    REVYS_WIDGET_ID=VOTRE_ID_WIDGET

    Pas de préfixe NEXT_PUBLIC_ : la documentation Next.js précise que ces variables sont intégrées au JavaScript envoyé au navigateur.

  3. Écrire le composant

    Installez le garde-fou server-only (npm install server-only) : le build échoue si ce fichier est importé par erreur dans un composant client.

    // 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="avis-titre">
          <h2 id="avis-titre">Avis de nos clients</h2>
          <p>
            {aggregate.average.toFixed(1)}/5 sur {aggregate.count} avis Google
          </p>
          <ul>
            {reviews.map((r) => (
              <li key={r.id}>
                <p>
                  <strong>{r.authorName}</strong>{" "}
                  <span aria-label={`${r.rating} sur 5`}>
                    {"★".repeat(r.rating)}
                    {"☆".repeat(5 - r.rating)}
                  </span>
                  {r.reviewedAt ? (
                    <time dateTime={r.reviewedAt}>
                      {" "}· {new Date(r.reviewedAt).toLocaleDateString("fr-FR")}
                    </time>
                  ) : null}
                </p>
                {r.text ? <p>{r.text}</p> : null}
                {r.ownerReply ? (
                  <p>
                    <em>Réponse du propriétaire :</em> {r.ownerReply}
                  </p>
                ) : null}
              </li>
            ))}
          </ul>
          <script
            type="application/ld+json"
            dangerouslySetInnerHTML={{
              __html: JSON.stringify(schemaOrg).replace(/</g, "\\u003c"),
            }}
          />
        </section>
      );
    }

Quelques détails de l'API utiles en production :

  • ?widget= applique les filtres du widget (note minimale, nombre d'avis) ; les avis arrivent du plus récent au plus ancien, sans les avis que vous avez masqués.
  • aggregate est calculé sur tous les avis de la fiche : la moyenne correspond à votre vraie note Google, même si le widget n'affiche que les 4 et 5 étoiles.
  • schemaOrg est un objet Organization sans aggregateRating, conforme à la règle de Google sur les avis auto-servis. Le .replace(/</g, "\\u003c") suit la recommandation de la documentation Next.js contre l'injection de balises.
  • Si le quota mensuel est dépassé, l'API renvoie la dernière réponse réussie pour la même cible (en-tête X-Revys-Quota: exceeded) : vos avis restent affichés.

Avec revalidate: 3600, chaque instance de serveur fait au plus 24 appels par jour, soit environ 720 par mois.

Pourquoi éviter le snippet client dans Next.js ?

Parce qu'il annule l'avantage du rendu serveur. Un useEffect qui appelle une API, ou le snippet JavaScript de Revys, ajoute les avis après le chargement. Le HTML initial ne les contient pas.

  • Les robots IA lisent le HTML brut. L'analyse de Vercel (décembre 2024) n'a trouvé aucun grand robot IA qui exécute le JavaScript.
  • Google exécute le JavaScript, mais dans une file de rendu distincte du premier passage, comme l'explique sa documentation sur le SEO JavaScript.
  • Le snippet Revys insère une iframe qui pointe vers revys.io : le contenu n'est pas sur votre domaine.

Un appel côté client exposerait aussi votre clé API à n'importe quel visiteur. Pour aller plus loin sur ce que les moteurs IA lisent, voir avis clients et IA : être cité par ChatGPT.

Et pour Astro, Node ou PHP ?

Le même fragment HTML s'intègre dans n'importe quel serveur. Le tableau de bord donne le code pour Astro, Node.js/Express, Python, Ruby et un Cloudflare Worker ; la page PHP, HTML et autres CMS détaille PHP et le cas des sites statiques. Sur WordPress, préférez le plugin WordPress. Pour comparer toutes les méthodes, lisez le guide afficher les avis Google sur son site, et consultez les tarifs pour le plan Pro.

Questions fréquentes

Faut-il une clé API pour afficher les avis dans Next.js ?

Non pour le fragment HTML : l'URL /api/embed/ID/html est publique et fonctionne dès le plan Free. Oui pour l'API JSON /api/v1/reviews, incluse dans le plan Pro et appelée avec un en-tête Authorization: Bearer.

Où mettre la clé API Revys dans un projet Next.js ?

Dans une variable d'environnement serveur, par exemple REVYS_API_KEY dans .env.local, lue uniquement dans un Server Component. Ne la préfixez jamais par NEXT_PUBLIC_, sinon Next.js l'intègre au JavaScript envoyé au navigateur.

Pourquoi ne pas charger les avis avec useEffect ou un script client ?

Parce que les avis n'existent alors pas dans le HTML initial. Les grands robots IA n'exécutent pas le JavaScript, et le snippet Revys passe par une iframe hébergée sur revys.io, donc les avis ne sont pas attribués à votre site.

Combien d'appels API consomme un cache d'une heure ?

Au plus 24 appels par jour et par instance de serveur, soit environ 720 par mois, très en dessous du quota Pro de 10 000 appels par mois et par fiche.

Ça marche avec le Pages Router ?

Oui : faites le même appel dans getStaticProps avec revalidate: 3600 (régénération incrémentale), et passez le HTML ou les avis à la page en props.

Les avis affichés donnent-ils des étoiles dans Google ?

Non. Google n'affiche pas d'étoiles pour les avis qu'une entreprise publie sur son propre site. Revys fournit donc un JSON-LD Organization conforme, sans AggregateRating.

Sources

  1. fetch : options.next.revalidate (documentation Next.js)
  2. How to implement JSON-LD in your Next.js application (documentation Next.js)
  3. Environment Variables (documentation Next.js)
  4. cacheLife (documentation Next.js)
  5. Notions de base du SEO JavaScript (Google Search Central)
  6. The rise of the AI crawler (Vercel, décembre 2024)

Dans le même thème

Vos avis méritent d'être lus.

Par vos clients, par Google, et par les IA.