Product Review Schema for Yoast SEO

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.

Download Now

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 WPSEO_Graph_Piece interface (see here).

class BE_Product_Review implements \WPSEO_Graph_Piece {

}

The interface requires your class contain two methods:

  1. generate() for generating the graph piece
  2. is_needed() for determining if this graph piece should be added

Yoast SEO could add additional methods to the interface in the future, which would also be required in your class.

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/frontend/schema (see here). I’ll use the Article schema type as my base since a review is a more specific type of article.

class BE_Product_Review extends \WPSEO_Schema_Article implements \WPSEO_Graph_Piece {

}

Setup the $context in __construct()

The $context variable is an object that contains important information about the current page. It contains site-wide data (site_name, site_url) and page-specific data (id is the post ID, canonical is the permalink).

See /wordpress-seo/frontend/schema/class-schema-context.php for more information.

We need to make $context accessible in our class. Since we are extending another class, we need to pull the $context from the parent.

If we were not extending an existing class, you could leave that line out and only have $this->context = $context;

class BE_Product_Review extends \WPSEO_Schema_Article implements \WPSEO_Graph_Piece {

	/**
	 * A value object with context variables.
	 *
	 * @var WPSEO_Schema_Context
	 */
	private $context;

	/**
	 * Product_Rating constructor.
	 *
	 * @param WPSEO_Schema_Context $context Value object with context variables.
	 */
	public function __construct( WPSEO_Schema_Context $context ) {
		parent::__construct( $context );
		$this->context   = $context;
	}
}

Customize is_needed()

is_needed() is one of the two methods required by the WPSEO_Graph_Piece. Use this method to determine whether or not this graph piece is needed in this specific context.

Since I’m extending the WPSEO_Schema_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 . WPSEO_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
/**
 * Class BE_Review
 */
class BE_Product_Review extends \WPSEO_Schema_Article implements \WPSEO_Graph_Piece {

	/**
	 * A value object with context variables.
	 *
	 * @var WPSEO_Schema_Context
	 */
	private $context;

	/**
	 * Product_Rating constructor.
	 *
	 * @param WPSEO_Schema_Context $context Value object with context variables.
	 */
	public function __construct( WPSEO_Schema_Context $context ) {
		parent::__construct( $context );
		$this->context   = $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 . WPSEO_Schema_IDs::ARTICLE_HASH ),
			'itemReviewed'     => array(
					'@type'    => 'Product',
					'image'    => array(
						'@id'  => $this->context->canonical . WPSEO_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 . WPSEO_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 . WPSEO_Schema_IDs::PERSON_HASH;
		}

		return $this->context->site_url . WPSEO_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;
	}
}

WordPress Development Yoast SEO

Bill Erickson

Bill Erickson is a freelance WordPress developer and a contributing developer to the Genesis framework. For the past 14 years he has worked with attorneys, publishers, corporations, and non-profits, building custom websites tailored to their needs and goals.

Ready to upgrade your website?

I build custom WordPress websites that look great and are easy to manage.

Let's Talk

Reader Interactions

Leave A Reply