The plugin is installed, the stars render on the homepage, and six weeks later Search Console shows nothing. No error, no enhancement, no rich result. The markup is valid JSON-LD and the rating is real. It is still ineligible.
That outcome is common enough that it deserves a name: self-serving review markup. Your site is reviewing your own business, and Google’s review snippet documentation says plainly that pages using LocalBusiness or any type of Organization structured data are ineligible for the star feature when the reviewed entity controls the reviews about itself.
The rule is narrow, and most of the panic around it is unnecessary. Product reviews on your own store are still eligible. Recipes, courses, books, and software are still eligible. What stops working is the testimonial block dressed up as a LocalBusiness rating, including the version copied from a Google reviews widget.
Stars Are a Rich Result, Not a Trust Badge
A review snippet is a short excerpt of a review or an average rating, and Google may show it with stars and summary information. It changes how the result looks. It is not a ranking factor, and it does not verify that the reviews are good.
That distinction matters because it sets the right goal. You are not trying to add stars to every page. You are trying to make the pages that genuinely contain reviews eligible, and to stop the pages that cannot be eligible from carrying markup that sends conflicting signals.
Google supports review snippets for a defined list of item types: Book, Course, Event, HowTo, LocalBusiness, Movie, Product, Recipe, SoftwareApplication, and a set of creative-work and media types. It also supports Organization and its subtypes, but only for sites that capture reviews about other organizations. The same page lists the self-serving exception directly beside those types, which is why reading only the list produces the wrong conclusion.
The Self-Serving Rule in One Sentence
If the entity being reviewed controls the reviews about itself, a page using LocalBusiness or any other Organization type is not eligible for star reviews.
Google gives the example explicitly: a review about entity A placed on entity A’s website, either in the structured data or through an embedded third-party widget such as a Google Business reviews widget or a Facebook reviews widget. The widget does not launder the relationship. The review is still about the site that hosts it.
There is a second sentence in the same section that catches agencies and directories: ratings must be sourced directly from users, not created, curated, or compiled by human editors. A marketing team that selects five glowing testimonials and assigns them an average of 4.9 has produced a rating, but not a user-sourced one.
The rule does not say the reviews are fake. It says the page is not eligible for the feature. Those are different claims, and conflating them is what leads store owners to remove perfectly good testimonial content.
What Is Still Eligible
The easiest way to apply the rule is to ask what is being reviewed.
Eligible: a product sold on your store. The reviewed item is the Product, not your Organization. Customers write the reviews, the reviews are visible on the product page, and the markup describes the product. WooCommerce does this by default when ratings are enabled.
Eligible: a recipe, book, course, movie, software application, or event. These are the item types Google lists, and the reviewed entity is the thing, not the site owner.
Eligible: reviews of other businesses. A review site, a comparison directory, or an agency that evaluates vendors can mark up LocalBusiness or Organization reviews, provided the reviews are about those other entities and are sourced from users.
Not eligible: your own business reviewed on your own site. A homepage block of client testimonials, a Google reviews widget, or an Organization node with an aggregateRating on the about page. The content can stay; the star markup cannot.
Not eligible: a service business relabeled as a Product to keep stars. Changing @type from LocalBusiness to Product does not change what is being reviewed. It changes whether the markup matches the page, which is a separate problem with worse consequences.
That last point is where structured-data spam policy enters. Ineligibility is the usual outcome of self-serving markup. Deliberately misrepresenting the reviewed item can be treated as spam, and Google’s structured data policies allow manual actions for markup that is misleading or hidden from users.
Review and AggregateRating Are Two Different Objects
A Review is one person’s evaluation. An AggregateRating is a summary of many ratings. They have different required properties, and mixing them is the second most common source of errors.
For a Review, Google requires:
author— a Person or Organization with a valid name. A promotional phrase is not a valid author name, and the field must be under 100 characters.itemReviewed— required unless the review is nested inside the item it reviews, in which case omit it and Google assumes the parent.reviewRating.ratingValue— a number, fraction, or percentage.
For an AggregateRating, Google requires itemReviewed unless nested, plus ratingValue and at least one of ratingCount or reviewCount. The AggregateRating definition on schema.org describes the underlying vocabulary; Google’s page describes what it will actually use.
Two optional properties are worth setting when they apply: datePublished on the review, and bestRating / worstRating when your scale is not 1–5. If you omit them, Google assumes a 5-point scale starting at 1, which is wrong for a 10-point or percentage rating.
Decimal values must use a dot. A ratingValue of 4,8 is a string that Google cannot read as a number, and it is the kind of error that produces a warning rather than a visible failure.
The Minimum Valid Product Review Block
This is a complete, self-contained example for a product page. It uses a nested review, so itemReviewed is omitted on the child and inherited from the Product.
{
"@context": "https://schema.org/",
"@type": "Product",
"name": "Wool winter coat",
"brand": {
"@type": "Brand",
"name": "Good brand"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": 4.6,
"reviewCount": 38
},
"review": [
{
"@type": "Review",
"author": {
"@type": "Person",
"name": "Sam Rivera"
},
"datePublished": "2026-10-14",
"reviewRating": {
"@type": "Rating",
"ratingValue": 5,
"bestRating": 5,
"worstRating": 1
},
"reviewBody": "Warm without the bulk, and the sleeves are actually long enough."
}
]
}
Four things to notice.
The review array is nested under Product. That is what allows the child reviews to omit itemReviewed, and it is also the shape WooCommerce emits.
reviewCount is a count of reviews; ratingCount is the broader number that includes star-only ratings. Use the one that matches what you display, and do not use both to inflate the total.
ratingValue is a number, not a string, and it uses a dot. The bestRating and worstRating are explicit even though they match the default, because a theme that changes the star scale later will otherwise silently invalidate the markup.
The reviews in the JSON must be visible on the page. Marking up content that a visitor cannot see is a guidelines violation, not a clever way to add more reviews than the template renders.
Why Testimonial Blocks Fail
A testimonial slider is content, not review markup. It becomes a problem when a plugin wraps it in schema that claims your organization has a 4.9 rating.
The failure has a recognizable shape in the page source: an Organization or LocalBusiness node with aggregateRating, often added site-wide by an SEO plugin’s “local SEO” module or by a reviews plugin that imports a Google Business Profile feed. The same node may carry review entries whose author values are first names only, or whose text is copied from the Google widget.
Three things go wrong at once:
- The reviewed entity is the site owner, so the self-serving rule applies.
- The rating may not be user-sourced if an editor selected the testimonials.
- The visible content often shows five reviews while the markup claims 212 ratings.
None of those is fixed by editing the JSON. The correct fix is to remove the review markup from that page and leave the testimonials as ordinary content. If you want third-party review signals, the Google Business Profile guide covers the profile side, where the reviews actually belong.
What WooCommerce Emits, and When
WooCommerce core adds aggregateRating to a product’s structured data when the product has a rating count and product review ratings are enabled. It includes ratingValue as the average and reviewCount as the count. It also marks up up to five approved reviews with a rating, each with an author and a rating value.
The gate is in the structured-data class: $product->get_rating_count() && wc_review_ratings_enabled(). If either side is false, there is no aggregate rating. That is why a store that collects reviews but has star ratings disabled sees no stars in search.
The review settings live under WooCommerce → Settings → Products. The product reviews documentation covers enabling reviews, requiring a verified owner, and moderation. Two settings matter for markup:
- Enable star rating on reviews. Without it, reviews exist but ratings do not, and the aggregate node has nothing to summarize.
- Reviews can only be left by verified owners. This does not affect eligibility, but it changes volume, and volume is what makes an aggregate rating representative.
WooCommerce does not mark up your homepage testimonials, and it does not add Organization review markup. If those stars appear, a plugin or theme added them, and that is the code to inspect.
Audit the Markup You Already Have
You can check a handful of URLs by hand in the Rich Results Test. For a whole site, parse the JSON-LD and flag the patterns that cannot be eligible.
This script takes a file of URLs, fetches each page, extracts every application/ld+json block, and reports self-serving organization markup, incomplete reviews, and incomplete aggregate ratings. It does not need WordPress.
<?php
/**
* Usage: php review-audit.php urls.txt
* One URL per line. Read-only.
*/
if ( $argc < 2 || ! is_readable( $argv[1] ) ) {
fwrite( STDERR, "Usage: php review-audit.php urls.txt\n" );
exit( 1 );
}
$urls = array_filter( array_map( 'trim', file( $argv[1] ) ) );
$issues = 0;
function audit_node( $node, $url ) {
global $issues;
if ( ! is_array( $node ) ) {
return;
}
if ( isset( $node['@graph'] ) && is_array( $node['@graph'] ) ) {
foreach ( $node['@graph'] as $child ) {
audit_node( $child, $url );
}
return;
}
$types = isset( $node['@type'] ) ? (array) $node['@type'] : array();
$types = array_map( 'strval', $types );
$flag = false;
foreach ( $types as $type ) {
if ( 'Review' === $type ) {
$author = isset( $node['author'] ) ? $node['author'] : null;
$author_name = '';
if ( is_array( $author ) && isset( $author['name'] ) ) {
$author_name = $author['name'];
} elseif ( is_string( $author ) ) {
$author_name = $author;
}
if ( '' === trim( (string) $author_name ) ) {
echo "$url Review missing author name\n";
$flag = true;
}
$rating = isset( $node['reviewRating']['ratingValue'] )
? $node['reviewRating']['ratingValue']
: null;
if ( null === $rating || '' === $rating ) {
echo "$url Review missing reviewRating.ratingValue\n";
$flag = true;
}
}
if ( 'AggregateRating' === $type ) {
$has_count = isset( $node['ratingCount'] ) || isset( $node['reviewCount'] );
if ( ! isset( $node['ratingValue'] ) || ! $has_count ) {
echo "$url AggregateRating missing ratingValue or count\n";
$flag = true;
}
if ( isset( $node['ratingValue'] ) && is_string( $node['ratingValue'] )
&& false !== strpos( $node['ratingValue'], ',' ) ) {
echo "$url AggregateRating uses a comma decimal\n";
$flag = true;
}
}
if ( in_array( $type, array( 'Organization', 'LocalBusiness' ), true )
&& ( isset( $node['aggregateRating'] ) || isset( $node['review'] ) ) ) {
echo "$url self-serving $type review markup\n";
$flag = true;
}
}
if ( $flag ) {
$issues++;
}
foreach ( $node as $value ) {
if ( is_array( $value ) ) {
audit_node( $value, $url );
}
}
}
foreach ( $urls as $url ) {
$context = stream_context_create(
array(
'http' => array(
'timeout' => 20,
'user_agent' => 'ReviewSchemaAudit/1.0',
'ignore_errors' => true,
),
)
);
$html = @file_get_contents( $url, false, $context );
if ( false === $html ) {
echo "$url fetch failed\n";
$issues++;
continue;
}
if ( ! preg_match_all(
'#<script[^>]+type=["\']application/ld\+json["\'][^>]*>(.*?)</script>#is',
$html,
$matches
) ) {
continue;
}
foreach ( $matches[1] as $json ) {
$data = json_decode( trim( $json ), true );
if ( null === $data ) {
echo "$url invalid JSON-LD\n";
$issues++;
continue;
}
audit_node( $data, $url );
}
}
printf( "Checked %d URLs; %d issue blocks.\n", count( $urls ), $issues );
exit( $issues > 0 ? 1 : 0 );
Run it before a migration and after any schema-plugin change:
php review-audit.php urls.txt
The output is deliberately terse so it can be sorted and counted. The first two checks catch incomplete reviews; the third catches the self-serving organization pattern; the fourth catches a comma decimal that will fail parsing in some tools.
This is a static check, not a substitute for the Rich Results Test. It cannot tell you whether a review is visible on the page, whether the author name is real, or whether the rating matches what the template renders. It finds the structural failures that are cheap to fix and easy to miss across hundreds of URLs.
Nesting, Standalone Markup, and the itemReviewed Trap
A standalone Review must name the item it reviews:
{
"@context": "https://schema.org/",
"@type": "Review",
"itemReviewed": {
"@type": "Product",
"name": "Wool winter coat"
},
"author": {
"@type": "Person",
"name": "Sam Rivera"
},
"reviewRating": {
"@type": "Rating",
"ratingValue": 4
}
}
That is valid for a single review page. It is the wrong shape for a product page that already has a Product node, because the page then describes the same product twice. Nest the review under the Product instead, and let the parent supply itemReviewed.
The trap is mixing the two. A nested review that still carries itemReviewed is not automatically invalid, but it duplicates the relationship and makes it harder to tell which node is authoritative. A standalone review that omits itemReviewed is missing a required property. If your plugin has a “review” widget and a “product” widget, check which one is rendering before editing either.
For aggregate ratings, the same logic applies. Nest aggregateRating under the reviewed item and omit itemReviewed. Put it at the top level only when the page is about a single reviewed entity and there is no parent node to attach it to.
Visible Content Is Part of the Markup
Google’s guidelines require the marked-up content to be visible to users. That is not a formality. A page that shows no reviews but carries an aggregateRating of 4.9 is misrepresenting the page, and the mismatch is easy for a reviewer or a competitor to report.
The practical checks:
- The rating shown in the template matches
ratingValue. If the theme rounds to the nearest half star, the JSON should carry the unrounded average, not the rounded display value. - The review count matches what is displayed. “Based on 38 reviews” and
reviewCount: 212is a defect. - Every marked-up review exists on the page. WooCommerce marks up five recent reviews because five are rendered. Do not expand that list in JSON alone.
- The author name is a name. “Happy customer” and “50% off until Saturday” are not valid reviewer names.
These are the same requirements whether the markup comes from core, a plugin, or a hand-written block.
Test, Deploy, and Watch
The workflow that catches problems before they reach the index:
- Validate one page per template in the Rich Results Test. A product page, a review page, and the homepage are three different templates.
- Deploy to a staging URL and confirm the rendered source, not the editor preview. Caching plugins and consent tools can strip or delay JSON-LD.
- Use URL Inspection on a few live URLs to see the rendered HTML Google receives.
- Submit the URLs for recrawl if the markup changed on pages already indexed.
- Watch the enhancement report in Search Console. Review snippet errors appear there, and the counts tell you whether a fix propagated.
Allow days to weeks for recrawling. Removing invalid markup does not produce an instant change in the report, and adding valid markup does not guarantee stars; Google decides whether to show the feature.
If stars disappear after a theme update, check three things before rewriting anything: the JSON-LD is still in the rendered source, the product still has a rating count, and the page is still indexable. A noindex tag or a robots block makes the markup irrelevant.
A Rollout Checklist
Use this order when adding review schema to a site that does not have it.
- Inventory the templates that contain reviews: product pages, review pages, testimonial sections, directory listings.
- Classify each one by the reviewed entity. Product, Recipe, Course, and similar types are eligible. Your own Organization or LocalBusiness is not.
- Remove self-serving markup first. Leaving it in place while adding new markup creates two conflicting claims on the same site.
- Add aggregate ratings only where the count is real. A single review is not an aggregate, and a five-star testimonial is not a rating count.
- Nest reviews under their item. Use standalone Review only on pages that review exactly one thing.
- Align the visible page with the JSON. Rating, count, author, and review text must match.
- Validate and monitor. Rich Results Test for syntax, Search Console for propagation.
Two adjacent pieces are worth reading alongside this one. The semantic SEO guide covers how entities and schema fit together beyond reviews, and the SEO ranking guide puts rich results in the context of the rest of the page. If you are choosing the plugin that will output the markup, the free SEO plugin roundup is the hub for that decision, and the DefinitiveSEO setup guide covers the beginner configuration. Review markup is one switch among many, and it is easier to get right when the surrounding schema is not fighting it.
FAQ
Is review schema a ranking factor?
No. It can change how a result looks and may affect click-through rate, but stars are not a ranking signal. The value is presentation and trust, not position.
Can I mark up reviews from my Google Business Profile on my own site?
Not as LocalBusiness or Organization markup on the same site those reviews are about. Google names embedded Google Business reviews widgets as an example of self-serving markup, and those pages are ineligible for stars. Keep the widget as content if it helps visitors; do not expect the star feature.
Are product reviews on my own store eligible?
Yes. The reviewed item is the Product, not your Organization, so the self-serving rule does not apply. WooCommerce core emits aggregateRating and review markup when ratings are enabled and the product has a rating count.
Do I need author on an AggregateRating?
No. AggregateRating requires ratingValue and at least one of ratingCount or reviewCount. author is required on an individual Review, not on the aggregate.
ratingCount or reviewCount — which one?
Use ratingCount when you are counting all ratings, including star-only submissions. Use reviewCount when you are counting reviews. Pick the number that matches what the page displays and do not add them together.
Why did my stars disappear?
Common causes: the page is noindexed or blocked, the product lost its rating count, ratings were disabled, a theme update removed the JSON-LD, or Google decided the page is ineligible. Check the rendered source and the enhancement report before changing the markup.
Is a single five-star testimonial an AggregateRating?
No. An aggregate summarizes multiple ratings. A single testimonial is a Review, and marking it as an aggregate with a count of one is a misrepresentation of the page.
Can I mark up reviews that are not visible on the page?
No. Google requires the marked-up content to be visible to users. Marking up hidden reviews or additional ratings that the template does not render violates the guidelines and can be treated as spam.
Leave a Reply