Yoast SEO schema uses the Article
type for posts, but you can extend this using plugins or your own custom code.

In Google’s Structured Data Guidelines, they recommend:
Try to use the most specific applicable type and property names defined by schema.org for your markup.
I’m working on a product review website, so most articles are better classified with the Review
type. I developed a plugin for:
- Marking posts as reviews in the backend
- Adding the additional review metadata to the post (in a custom metabox)
- Updating the Yoast SEO schema to mark this as a review
The article below is a guide for developers extending Yoast SEO schema using product reviews as the example.
If you only want product review schema on your website, you can install my Product Review Schema for Yoast SEO plugin. You do not need to make any code changes.
Install the plugin
“Product Review Schema for Yoast SEO” is available for free on GitHub.
Filtering schema pieces
The key to extending Yoast SEO schema is the wpseo_schema_graph_pieces
filter.
This filter lets you add (or remove) pieces of the schema graph. Yoast SEO creates the large, structured graph by assembling all of these pieces.
/**
* Adds Schema pieces to our output.
*
* @param array $pieces Graph pieces to output.
* @param \WPSEO_Schema_Context $context Object with context variables.
*
* @return array $pieces Graph pieces to output.
*/
public function schema_piece( $pieces, $context ) {
require_once( plugin_dir_path( __FILE__ ) . '/class-be-product-review.php' );
$pieces[] = new BE_Product_Review( $context );
return $pieces;
}
add_filter( 'wpseo_schema_graph_pieces', array( $this, 'schema_piece' ), 20, 2 );
Each graph piece is packaged as a class. In this case, I’m using my custom BE_Product_Review
class found in class-be-product-review.php
in my plugin.
Creating a class
All graph piece classes should be implemented using the Abstract_Schema_Piece
class.
use \Yoast\WP\SEO\Generators\Schema\Abstract_Schema_Piece;
class BE_Product_Review extends Abstract_Schema_Piece {
}
Your custom class should include two methods:
generate()
for generating the graph pieceis_needed()
for determining if this graph piece should be added
To make my customizations a bit more future-proof, I’ll extend an existing class rather than build my own. If a future update to Yoast SEO adds another required method, my class will automatically have it because I’m extending a core class that will also include it.
Extending an existing class
All of the Yoast SEO schema classes can be found in /wordpress-seo/src/generators/schema (see here). I’ll use the Article
schema type as my base since a review is a more specific type of article.
use \Yoast\WP\SEO\Generators\Schema\Abstract_Schema_Piece;
use \Yoast\WP\SEO\Generators\Schema\Article;
use \Yoast\WP\SEO\Config\Schema_IDs;
class BE_Product_Review extends Article {
}
Customize is_needed()
Use this method to determine whether or not this graph piece is needed in this specific context.
Since I’m extending the Article
class, I could leave this method out of my class and it would use is_needed()
from the parent class. But my review schema doesn’t apply in every instance that the article schema would, so I need to define my own display logic.
For my BE_Product_Review
class, I have a custom metabox that allows the editor to mark this as a product review using the be_product_review_include
meta key.
Rather than using get_the_ID()
to get the post ID, we’re using $this->context->id
.
/**
* Determines whether or not a piece should be added to the graph.
*
* @return bool
*/
public function is_needed() {
$post_types = apply_filters( 'be_product_review_schema_post_types', array( 'post' ) );
if( is_singular( $post_types ) ) {
$display = get_post_meta( $this->context->id, 'be_product_review_include', true );
if( 'on' === $display ) {
return true;
}
}
return false;
}
Generate the graph data
The generate()
method is where the actual magic happens. This is where you define the relevant schema data for this graph piece.
Make sure you read Google’s guidelines for your schema type, and use the Structured Data Testing Tool often to make sure you have everything correct.
In my case, the Product Review schema requirements include itemReviewed
, reviewRating
, reviewBody
, and more.
When building your graph piece, it’s important that you include an @id
so it can be referenced by other graph pieces, as well as isPartOf
so Yoast SEO knows where to place it in the graph. In my case, the review is part of the article so I have:
'isPartOf' => array( '@id' => $this->context->canonical . Schema_IDs::ARTICLE_HASH ),
I added a filter to the end of my $data
before returning it so plugins/themes can customize the output if needed.
To keep the generate()
method as clean as possible, you can move logic to separate methods which are referenced inside this one.
I created the get_review_meta( $key, $fallback )
method for retrieving the review metadata, and providing a fallback in case these required fields haven’t been filled out (name = post title; review body = post content).
Below is the completed class. I recommend you look at the Product Review Schema for Yoast SEO plugin on GitHub for more information on how this all comes together:
<?php
use \Yoast\WP\SEO\Generators\Schema\Abstract_Schema_Piece;
use \Yoast\WP\SEO\Generators\Schema\Article;
use \Yoast\WP\SEO\Config\Schema_IDs;
class BE_Product_Review extends Article {
/**
* A value object with context variables.
*
* @var WPSEO_Schema_Context
*/
public $context;
/**
* Determines whether or not a piece should be added to the graph.
*
* @return bool
*/
public function is_needed() {
$post_types = apply_filters( 'be_product_review_schema_post_types', array( 'post' ) );
if( is_singular( $post_types ) ) {
$display = get_post_meta( $this->context->id, 'be_product_review_include', true );
if( 'on' === $display ) {
return true;
}
}
return false;
}
/**
* Adds our Review piece of the graph.
*
* @return array $graph Review markup
*/
public function generate() {
$post = get_post( $this->context->id );
$comment_count = get_comment_count( $this->context->id );
$data = array(
'@type' => 'Review',
'@id' => $this->context->canonical . '#product-review',
'isPartOf' => array( '@id' => $this->context->canonical . Schema_IDs::ARTICLE_HASH ),
'itemReviewed' => array(
'@type' => 'Product',
'image' => array(
'@id' => $this->context->canonical . Schema_IDs::PRIMARY_IMAGE_HASH,
),
'name' => wp_strip_all_tags( $this->get_review_meta( 'name', get_the_title() ) ),
),
'reviewRating' => array(
'@type' => 'Rating',
'ratingValue' => esc_attr( $this->get_review_meta( 'rating', 1 ) ),
),
'name' => wp_strip_all_tags( $this->get_review_meta( 'name', get_the_title() ) ),
'description' => wp_strip_all_tags( $this->get_review_meta( 'summary', get_the_excerpt( $post ) ) ),
'reviewBody' => wp_kses_post( $this->get_review_meta( 'body', $post->post_content ) ),
'author' => array(
'@id' => get_author_posts_url( get_the_author_meta( 'ID' ) ),
'name' => get_the_author_meta( 'display_name', $post->post_author ),
),
'publisher' => array( '@id' => $this->get_publisher_url() ),
'datePublished' => mysql2date( DATE_W3C, $post->post_date_gmt, false ),
'dateModified' => mysql2date( DATE_W3C, $post->post_modified_gmt, false ),
'commentCount' => $comment_count['approved'],
'mainEntityOfPage' => $this->context->canonical . Schema_IDs::WEBPAGE_HASH,
);
$data = apply_filters( 'be_review_schema_data', $data, $this->context );
return $data;
}
/**
* Determine the proper publisher URL.
*
* @return string
*/
private function get_publisher_url() {
if ( $this->context->site_represents === 'person' ) {
return $this->context->site_url . Schema_IDs::PERSON_HASH;
}
return $this->context->site_url . Schema_IDs::ORGANIZATION_HASH;
}
/**
* Product review meta
*
* @param string $key
* @param string $fallback
* @return string $meta
*/
private function get_review_meta( $key = false, $fallback = false ) {
$meta = get_post_meta( $this->context->id, 'be_product_review_' . $key, true );
if( empty( $meta ) && !empty( $fallback ) )
$meta = $fallback;
return $meta;
}
}
Stephen says
Thanks for the awesome plugin.
Please how do i add a “price” filter to your plugin.
Thanks in advance.
Bill Erickson says
It doesn’t look like there’s a price element for the Review schema or Product schema.
If you can point to the schema element you’d like to add, I can show you how to do it.
Stephen says
how about this? https://schema.org/price
Bill Erickson says
Based on the example at the bottom of the page (click JSON-LD tab), it looks like the price can be attached to the product using the “offers” attribute.
Add this code to your theme’s functions.php file or a core functionality plugin to add support for the price field: https://gist.github.com/billerickson/243e877d06aee7e07fbaea3ba4eac191
You’ll also need a metabox to set the ‘be_price’ key equal to the price you want: https://www.billerickson.net/wordpress-metaboxes/
Stephen says
You are the boss man…
Thanks for your help, i really appreciate.
Marcin Biegun says
hi! having troubles using data from wp-postratings (aggregateRating) to yoast,
if you happened to play with that kind of rating – would be grateful for piece of code to compare.
btw – commenting for the first time but getting landed there plenty of times while looking for acf/woocommerce solutions. thanks for great blog.
Bill Erickson says
I’m not too familiar with that plugin, but based on briefly reviewing the code it looks like it’s designed for user ratings of your content, not you specifying a review rating to go along with your product review.
I haven’t researched the requirements for adding aggregateRating schema yet so am not comfortable advising you on what to add, but if you find the documentation on the specific schema you want added, I’ll show you how to add it.
Marcin Biegun says
yes that’s user rating – https://schema.org/AggregateRating – should be added to article like at example 3 i guess. if you don’t have anything ready, i’ll try to play with it!
Nicole says
Is there a way to use this code to just remove the date published?
I have disabled the published date in Genesis but it still appears via Yoast schema.
Bill Erickson says
While you may be able to filter the schema and remove the published date, I don’t recommend it. Publish date is required for valid schema so you would make your schema invalid, losing all SEO benefit.
Alex Kurniawan says
hello Bill,
How to use this plugin for custom post type?
Bill Erickson says
You can use the
be_product_review_schema_post_types
filter to add support for different post types.If you only want this added to your “product” post type, add the following to your theme’s functions.php or a core functionality plugin: https://gist.github.com/billerickson/bf0c7ee720e26a1e5623d4c9830053b1
Jason says
Thanks Bill, top plugin!
One thing I noticed and tried to change to no avail yet is the following:
For the Review part of the graph the Author schema appears in Structured Data Testing tool like this:
author
@type Thing
@id https://www.example.com/author/*wp-username*/
name *Full author name*
Yoast’s Article part of the graph actually shows the following (which is the correct way according to their documentation as it uses the unique ID of the Author and doesn’t make their username publicly available):
author
@type Person
@id https://www.example.com/#/schema/person/12345678910
name *Full author name*
Furthermore the above generates a Person instead of a Thing.
I’m assuming the following part of your plugin’s code needs to be amended:
‘author’ => array(
‘@id’ => get_author_posts_url( get_the_author_meta( ‘ID’ ) ),
‘name’ => get_the_author_meta( ‘display_name’, $post->post_author ),
I haven’t figured it out yet so would greatly appreciate your input on this 🙂
Tanner says
This is all really helpful though i’m still unsure how to disable product review count and aggregate review score – I have custom code outputting that already and when yoast tries to do it it’s causing an error. Any help would be appreciated.
Bill Erickson says
You want those fields disabled in the code provided in this post? Go to the
generate()
method and remove the elements you don’t want.But the product review schema won’t be valid without it. Even if something else is specifying a review count and aggregate review score, that likely won’t be tied to your custom product review schema.
Todd Adams says
Hey Bill, great article. I used this as a reference last year to develop a custom Speakable Schema plugin for one of my clients to inject specific Speakable Schema into Yoast. Worked fantastic up until the recent Yoast 14.0 update where they rewrote and changed all of the Schema https://developer.yoast.com/blog/yoast-seo-14-0-changing-the-yoast-schema-api/ I tried doing simple search/replaces for the changed parameters but it still causes critical errors for various reasons.
Any plans to update this article in the future to apply to the new Yoast Schema structure?
Bill Erickson says
Just updated it!
Todd Adams says
You’re amazing Bill! Thanks for the quick update.
Edo Dijkgraaf says
Thanks a million Bill! I had so much stress. It all works again.
Andreas says
Thank you bill for this plugin to extend the Yoast Schema functionality. The plugin outputs the snippet like this: Rating: 4,5 – 1 review
This is aggregated rating: https://schema.org/AggregateRating
However I would like to do editorial review posts where no one else is rating the product. The output in Google ideally should look like this: Rating: 4,5 – Review by Author Name
This is just rating: https://schema.org/Rating
Is there an easy way to change the code from AggregateRating to Rating?
Bill Erickson says
Yes, you can definitely change the schema to fit your needs. It might be as simple as changing
AggregateRating
toRating
here, but make sure you run it through Google’s structured data testing tool to ensure it is valid.Andreas says
Ok that’s easy, but it causes this error when validating with Google: “The property Rating is not recognized by Google for an object of type Product.”
Also the offer is required, but I can add that as mentioned in the previous comment.
Bill Erickson says
Right, that’s why we’re using AggregateRating, because it’s the recognized schema property for products.
If you want to use a different type of schema, you should first look at Google’s structured data guide for the structure they recognize, then you can customize the output in Yoast SEO to match.
Andreas says
Ok, like this it works:
‘reviewRating’ => array(
‘@type’ => ‘Rating’,
‘ratingValue’ =>
Mads says
Thanks a lot for going through the implementation like this. It helped me a lot with my own schema implementation.