Un mur d'avis n'a d'intérêt en référencement que si les moteurs comprennent ce qu'ils lisent : une note, un nombre d'avis, des témoignages. C'est le rôle du balisage schema.org (AggregateRating + Review), qui peut faire apparaître les étoiles sous votre lien dans les résultats de recherche.
AggregateRating
Review
Ce guide tient en trois idées :
La page portail /care/{votre-site}/avis est déjà balisée et indexable — rien à faire.
/care/{votre-site}/avis
Sur votre site, le balisage doit être posé côté serveur. Le script inodesk-reviews.js affiche les avis mais ne pose volontairement aucun JSON-LD.
inodesk-reviews.js
Le balisage doit dire exactement ce que la page affiche. Un balisage qui annonce une note différente de celle affichée est une pénalité, pas un avantage.
Chaque site INODESK dispose d'une page publique :
https://plateforme.inovertix.com/care/{votre-site}/avis
Elle est brandée à vos couleurs, affiche le formulaire de dépôt et le mur des avis publiés, et porte déjà le JSON-LD, construit côté serveur à partir des mêmes données que l'affichage.
C'est aussi la seule page publique d'INODESK qui soit indexable : le portail, les fils de tickets, les devis et les pages de satisfaction sont tous en noindex, nofollow. Ce n'est pas un oubli — un fil de ticket est privé même si son URL est publique.
noindex, nofollow
Si vous n'avez pas de besoin SEO propre, liez cette page depuis votre site (« Avis clients ») et arrêtez-vous là.
inodesk-reviews.js pourrait injecter le JSON-LD lui-même. Il ne le fait pas, et c'est délibéré : un balisage injecté après le chargement est lu de façon inégale selon les moteurs et les moments de passage. Le balisage doit être dans le HTML que le serveur renvoie.
Le script affiche donc les avis pour vos visiteurs ; votre serveur pose le balisage pour les moteurs. Les deux lisent la même API, donc les deux disent la même chose.
GET https://messagerie.inovertix.com/api/inodesk/reviews?k=INODSK_PUB_…
Appelée depuis votre serveur, avec votre clé publique. Réponse :
{ "site": { "name": "SORA Parfume", "slug": "sora" }, "branding": { "company_name": "SORA Parfume", "primary_color": "#8b5cf6" }, "aggregate": { "rating": 4.8, "count": 37 }, "reviews": [ { "id": "01J…", "author_name": "Awa K.", "rating": 5, "title": "Livraison rapide", "body": "Commande reçue en deux jours, exactement comme décrit.", "published_at": "2026-08-02T10:14:00+00:00", "featured": true, "owner_reply": "Merci Awa !" } ] }
La réponse porte Cache-Control: public, max-age=300. Respectez-le : un mur d'avis n'a pas besoin d'être frais à la seconde, et votre page ne doit pas attendre notre API à chaque visite.
Cache-Control: public, max-age=300
add_action('wp_head', function () { // Uniquement sur la page qui AFFICHE les avis : baliser une page qui ne les // montre pas est exactement ce que les moteurs sanctionnent. if (! is_page('avis')) { return; } $data = get_transient('inodesk_reviews'); if ($data === false) { $res = wp_remote_get( 39;https://messagerie.inovertix.com/api/inodesk/reviews?k=INODSK_PUB_…', array('timeout' => 5) ); // Échec silencieux : une API indisponible ne doit pas casser votre page. if (is_wp_error($res) || wp_remote_retrieve_response_code($res) !== 200) { return; } $data = json_decode(wp_remote_retrieve_body($res), true); set_transient('inodesk_reviews', $data, 5 * MINUTE_IN_SECONDS); } if (empty($data['aggregate']['count'])) { return; // aucun avis publié : pas de balisage } $ld = array( '@context' => 39;https://schema.org', '@type' => 'LocalBusiness', 'name' => $data['branding']['company_name'], 'url' => home_url('/'), 'aggregateRating' => array( '@type' => 'AggregateRating', 'ratingValue' => (string) $data['aggregate']['rating'], 'reviewCount' => $data['aggregate']['count'], 'bestRating' => '5', 'worstRating' => '1', ), 'review' => array_map(function ($r) { return array( '@type' => 'Review', 'author' => array('@type' => 'Person', 'name' => $r['author_name']), 'datePublished' => $r['published_at'], 'name' => $r['title'], 'reviewBody' => $r['body'], 'reviewRating' => array( '@type' => 'Rating', 'ratingValue' => (string) $r['rating'], 'bestRating' => '5', 'worstRating' => '1', ), ); }, array_slice($data['reviews'], 0, 10)), ); // wp_json_encode et non une concaténation : le contenu vient d'inconnus, il doit // être échappé par un sérialiseur, jamais à la main. echo '<script type="application/ld+json">' . wp_json_encode($ld, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . '</script>'; });
// app/avis/page.tsx — composant serveur : le JSON-LD part dans le HTML. export const revalidate = 300; // aligné sur le Cache-Control de l'API async function getReviews() { const res = await fetch( `${process.env.INODESK_ENDPOINT}/reviews?k=${process.env.INODESK_PUBLIC_KEY}`, { next: { revalidate: 300 } } ); if (!res.ok) return null; return res.json(); } export default async function AvisPage() { const data = await getReviews(); if (!data?.aggregate?.count) { return <p>Aucun avis pour le moment.</p>; } const jsonLd = { "@context": "https://schema.org", "@type": "LocalBusiness", name: data.branding.company_name, aggregateRating: { "@type": "AggregateRating", ratingValue: String(data.aggregate.rating), reviewCount: data.aggregate.count, bestRating: "5", worstRating: "1", }, review: data.reviews.slice(0, 10).map((r) => ({ "@type": "Review", author: { "@type": "Person", name: r.author_name }, datePublished: r.published_at, name: r.title, reviewBody: r.body, reviewRating: { "@type": "Rating", ratingValue: String(r.rating), bestRating: "5", worstRating: "1", }, })), }; return ( <> <script type="application/ld+json" // JSON.stringify : sérialiseur, pas concaténation. dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> <h1>Avis clients</h1> <p> {data.aggregate.rating}/5 — {data.aggregate.count} avis </p> <ul> {data.reviews.map((r) => ( <li key={r.id}> <strong>{r.author_name}</strong> — {r.rating}/5 <p>{r.body}</p> </li> ))} </ul> </> ); }
// app/Support/InoDeskReviews.php class InoDeskReviews { public static function fetch(): ?array { return Cache::remember('inodesk.reviews', 300, function () { $res = Http::timeout(5)->get(config('services.inodesk.endpoint').'/reviews', [ 'k' => config('services.inodesk.public_key'), ]); // null en cas d'échec : la vue s'affiche sans balisage plutôt que de // planter parce que notre API a hoqueté. return $res->successful() ? $res->json() : null; }); } /** @return array<string, mixed> */ public static function jsonLd(array $data): array { return [ '@context' => 39;https://schema.org', '@type' => 'LocalBusiness', 'name' => $data['branding']['company_name'], 'url' => config('app.url'), 'aggregateRating' => [ '@type' => 'AggregateRating', 'ratingValue' => (string) $data['aggregate']['rating'], 'reviewCount' => $data['aggregate']['count'], 'bestRating' => '5', 'worstRating' => '1', ], 'review' => collect($data['reviews'])->take(10)->map(fn (array $r) => [ '@type' => 'Review', 'author' => ['@type' => 'Person', 'name' => $r['author_name']], 'datePublished' => $r['published_at'], 'name' => $r['title'], 'reviewBody' => $r['body'], 'reviewRating' => [ '@type' => 'Rating', 'ratingValue' => (string) $r['rating'], 'bestRating' => '5', 'worstRating' => '1', ], ])->values()->all(), ]; } }
{{-- resources/views/avis.blade.php --}} @php($data = \App\Support\InoDeskReviews::fetch()) @if ($data && $data['aggregate']['count'] > 0) @push('head') {{-- @json échappe : jamais de concaténation à la main. --}} <script type="application/ld+json"> @json(\App\Support\InoDeskReviews::jsonLd($data), JSON_UNESCAPED_UNICODE) </script> @endpush <p>{{ $data['aggregate']['rating'] }}/5 — {{ $data['aggregate']['count'] }} avis</p> @foreach ($data['reviews'] as $review) <article> <strong>{{ $review['author_name'] }}</strong> — {{ $review['rating'] }}/5 <p>{{ $review['body'] }}</p> </article> @endforeach @endif
Baliser la page qui affiche. Le JSON-LD va sur la page où les avis sont visibles, pas sur l'accueil ni sur toutes les pages du site.
Dire la même chose. Note balisée = note affichée. Nombre balisé = nombre d'avis réellement montrés. Les deux viennent du même appel API — ne les recalculez pas séparément.
Ne rien baliser quand il n'y a rien. Zéro avis publié → pas de bloc JSON-LD. Un AggregateRating sans avis est une erreur de validation.
Ne jamais concaténer. JSON.stringify, wp_json_encode, @json : le contenu vient d'avis écrits par des inconnus. Un sérialiseur échappe, une concaténation ouvre une injection.
JSON.stringify
wp_json_encode
@json
Ne pas inventer d'avis. Seuls les avis réellement déposés et modérés apparaissent. Google supprime les extraits enrichis des sites pris en défaut — et ne les rétablit pas facilement.
https://search.google.com/test/rich-results
Collez l'URL de votre page (ou son code HTML si elle n'est pas encore en ligne). Attendu :
0 erreur. Les avertissements sur des champs facultatifs (address, priceRange, image) sont acceptables — remplissez-les si vous les avez.
address
priceRange
image
https://validator.schema.org — plus strict, utile pour vérifier les types eux-mêmes indépendamment de ce que Google en fait.
curl -s https://votre-site.tld/avis | grep -A 5 'application/ld+json'
Si rien ne sort, votre balisage est posé en JavaScript — c'est précisément ce que ce guide vous demande d'éviter.
Les étoiles n'apparaissent pas le lendemain. Comptez de quelques jours à quelques semaines après un nouveau passage du robot, et rien ne garantit leur affichage : Google décide au cas par cas. Un balisage valide est une condition, pas une promesse.
GET /api/inodesk/reviews?k=…
curl … | grep ld+json
reviewCount
0
count
origin_not_allowed
Voir aussi : README.md · wordpress.md · nextjs.md · laravel.md · html.md