Adding Semantic Reviews / Rich Snippets to your WordPress Site

by @edent | # # # #

This is a real “scratch my own itch” post. I want to add Schema.org semantic metadata to the book reviews I write on my blog. This will enable “rich snippets” in search engines.

There are loads of WordPress plugins which do this. But where’s the fun in that?! So here’s how I quickly built it into my open source blog theme.

Screen options

First, let’s add some screen options to the WordPress editor screen.

This is what it will look like when done:

Simple interface for adding details.

This is how to add a custom metabox to the editor screen:

//  Place this in functions.php
//  Display the box
function edent_add_review_custom_box()
{
   $screens = ['post'];
   foreach ($screens as $screen) {
      add_meta_box(
         'edent_review_box_id', // Unique ID
         'Book Review Metadata',    // Box title
         'edent_review_box_html',// Content callback, must be of type callable
         $screen                 // Post type
       );
   }
}
add_action('add_meta_boxes', 'edent_add_review_custom_box');

The contents of the box are bog standard HTML

//  Place this in functions.php
//  HTML for the box
function edent_review_box_html($post)
{
    $review_data = get_post_meta(get_the_ID(), "_edent_book_review_meta_key", true);
    echo "<table>";

    $checked = "";
    if ($review_data["review"] == "true") {
        $checked = "checked";
    }
    echo "<tr><td><label for='edent_book_review'>Embed Book Review:</label></td><td><input type=checkbox id=edent_book_review name=edent_book_review[review] value=true {$checked}></tr>";

    echo "<tr><td><label for='edent_rating'>Rating:</label></td><td><input type=range id=edent_rating name=edent_book_review[rating] min=0 max=5 step=0.5 value='". esc_html($review_data["rating"]) ."'></tr>";

    echo "<tr><td><label for=edent_isbn >ISBN:</label></td><td><input name=edent_book_review[isbn]  id=edent_isbn type=text value='" . esc_html($review_data["isbn"]) . "' autocomplete=off></tr>";

    echo "</table>";
}

Done! We now have a box for metadata. That data will be POSTed every time the blogpost is saved. But where do the data go?

Saving data

This function is added every time the blogpost is saved. If the checkbox has been ticked, the metadata are saved to the database. If the checkbox is unticked, the metadata are deleted.

//  Place this in functions.php
//  Save the box
function edent_review_save_postdata($post_id)
{
   if (array_key_exists('edent_book_review', $_POST)) {
        if ($_POST['edent_book_review']["review"] == "true") {
            update_post_meta(
                $post_id,
                '_edent_book_review_meta_key',
                $_POST['edent_book_review']
            );
        } else {
            delete_post_meta(
                $post_id,
                '_edent_book_review_meta_key'
            );
        }
    }
}
add_action('save_post', 'edent_review_save_postdata');

Nice! But how do we get the data back out again?

Retrieving the data

We can use the get_post_meta() function to get all the metadata associated with a blog entry. We can then turn it into a Schema.org structured metadata entry.

function edent_book_review_display($post_id){
    // https://developer.wordpress.org/reference/functions/the_meta/
    $review_data = get_post_meta($post_id, "_edent_book_review_meta_key", true);
    if ($review_data["review"] == "true")
    {
        $blog_author_data = get_the_author_meta();

        $schema_review = array (
            '@context' => 'https://schema.org',
            '@type'    => 'Review',
            'author' =>
            array (
                '@type' => 'Person',
                'name'  => get_the_author_meta("user_firstname") . " " . get_the_author_meta("user_lastname"),
                'sameAs' =>
                array (
                    0 => get_the_author_meta("user_url"),
                ),
            ),
            'url' => get_permalink(),
            'datePublished' => get_the_date('c'),
            'publisher' =>
            array (
                '@type'  => 'Organization',
                'name'   => get_bloginfo("name"),
                'sameAs' => get_bloginfo("url"),
            ),
            'description' => mb_substr(get_the_excerpt(), 0, 198),
            'inLanguage'  => get_bloginfo("language"),
            'itemReviewed' =>
            array (
                '@type'  => 'Book',
                'name'   => $review_data["title"],
                'isbn'   => $review_data["isbn"],
                'sameAs' => $review_data["book_url"],
                'author' =>
                array (
                    '@type'  => 'Person',
                    'name'   => $review_data["author"],
                    'sameAs' => $review_data["author_url"],
                ),
            'datePublished' => $review_data["book_date"],
            ),
            'reviewRating' =>
            array (
                '@type' => 'Rating',
                'worstRating' => 0,
                'bestRating'  => 5,
                'ratingValue' => $review_data["rating"],
            ),
            'thumbnailUrl' => get_the_post_thumbnail_url(),
        );
        echo '<script type="application/ld+json">' . json_encode($schema_review) . '</script>';

        echo "<div class='edent-review' style='clear:both;'>";
        if (isset($review_data["rating"])) {
            echo "<span class='edent-rating-stars' style='font-size:2em;color:yellow;background-color:#13131380;'>";
            $full = floor($review_data["rating"]);
            $half = 0;
            if ($review_data["rating"] - $full == 0.5)
            {
                $half = 1;
            }

            $empty = 5 - $half - $full;

            for ($i=0; $i < $full ; $i++) {
                echo "★";
            }
            if ($half == 1)
            {
                echo "⯪";
            }
            for ($i=0; $i < $empty ; $i++) {
                echo "☆";
            }
            echo "</span>";
        }
        echo "<ul>";
        if ($review_data["amazon_url"] != "") {
            echo "<li><a href='{$review_data["amazon_url"]}'>Buy it on Amazon</a></li>";
        }
        if ($review_data["author_url"] != "") {
            echo "<li><a href='{$review_data["author_url"]}'>Author's homepage</a></li>";
        }
        if ($review_data["book_url"] != "") {
            echo "<li><a href='{$review_data["book_url"]}'>Publisher's details</a></li>";
        }
        echo "</ul>";
    }
    echo "</div>";
}

In index.php, after the_content(); add:

edent_book_review_display(get_the_ID());

Then, on the website, it will look something like this:

Star rating and links displayed under a book cover.

Note the use of the Unicode Half Star for the ratings.

The source code of the site shows the output of the JSON LD:
Screenshot of JSON code in a web page.

When run through a Structured Data Testing Tool, it shows as a valid review:

Results of a Structured Data Test

And this means, when search engines access your blog, they will display rich snippets based on the semantic metadata.

Rich result from Google showing the star rating.

You can see the final blog post to see how it works.

ToDo

My code is horrible and hasn’t been tested, validated, or sanitised. It’s only for my own blog, and I’m unlikely to hack myself, but that needs fixing.

I want to add review metadata for movies, games, and gadgets. That will either require multiple boxes, or a clever way to only show the necessary fields.

Leave a Reply

Your email address will not be published. Required fields are marked *