Customizing Yoast SEO's structured data with schema API part 3

peterlidee

Peter Jacxsens

Posted on June 4, 2022

Customizing Yoast SEO's structured data with schema API part 3

The first part of this series talked about how Yoast SEO implements structured data. The second part showed how to add, remove or edit properties of a schema and how to remove a schema/piece.

In the 3rd and 4th parts, we will show 2 ways to add a custom schema pieces. On a sidenote: I'm not that proficient in php and some parts of this code are a bit foggy to me. So, there may be errors in here.

php class

Yoast uses php classes to create pieces. Each piece extends the basic class: Abstract_Schema_Piece. The class that generates for example the Organization piece is created by extending the Abstract_Schema_Piece class.

// https://github.com/Yoast/wordpress-seo/blob/trunk/src/generators/schema/organization.php

class Organization extends Abstract_Schema_Piece {
  // some code here
}
Enter fullscreen mode Exit fullscreen mode

This is the first method to create a custom piece. Create a class that extends Abstract_Schema_Piece. The second way is by building on any of the existing piece classes. This will be covered in part 4.

class MyCustomPiece extends Organization {
  // some code here
}
Enter fullscreen mode Exit fullscreen mode

Extending Abstract_Schema_Piece

As an example, we will make a new piece Vehicle and attach it to Person with the "owns" prop. This isn't valide structured data but it is a nice and simple example. This is our goal:

{
  "@type": "Person",
  "@id": "https://mycompany.com/#/schema/person/2fa9055e7ef234fa04dd717e6aaed799",
  "name": "Peter",
  "owns": {
    "@id": "person#vehicle"
  }
},
{
  "@type": "Vehicle",
  "@id": "person#vehicle",
  "name": "Ford",
  "numberOfDoors": 4,
  "weightTotal": {
    "value": 2000,
    "unitCode": "KGM"
  }
}
Enter fullscreen mode Exit fullscreen mode

Advanced Custom Fields

To have access to vehicle data we will use the advanced custom fields (ACF) plugin. Advanced Custom Fields is a WordPress plugin which allows you to add extra content fields to your WordPress edit screens. We use this plugin because I wanted to include an example that uses dynamic data.

In ACF we create a group vehicle and add it in the dashboard to each user. The vehicle group gets 3 fields: name, doors and weight. To access the content of these fields from the front end we use the get_field function that ACF provides:

get_field('vehicle_name', 'user_1');
Enter fullscreen mode Exit fullscreen mode

vehicle_name is the unique key we chose for the field "name" while user_1 tells ACF we are looking for a custom field added to a user with user_id = 1. This will return us the content we entered into this field, for example: "Ford".

I hope this didn't overwhelm you. Simply put, we used a plugin to add extra fields to each user in the dashboard. To access these fields in the front-end we use the function get_field.

Make a new piece Vehicle

Let's make a new piece Vehicle by by extending Abstract_Schema_Piece:

// functions.php

use Yoast\WP\SEO\Generators\Schema\Abstract_Schema_Piece;

class Vehicle extends Abstract_Schema_Piece {
}
Enter fullscreen mode Exit fullscreen mode

Context

Let's fill in the class Vehicle with some code.

// functions.php

class Vehicle extends Abstract_Schema_Piece {
  /**
  * A value object with context variables.
  *
  * @var WPSEO_Schema_Context
  */
  public $context;

  /**
  * Team_Member constructor.
  *
  * @param WPSEO_Schema_Context $context Value object with context variables.
  */
  public function __construct( WPSEO_Schema_Context $context ) {
    $this->context = $context;
  }
}
Enter fullscreen mode Exit fullscreen mode

We added a property $context and a method _construct. When this class gets called, the _construct method runs and populates the $context propery. I'm a bit foggy on what the WPSEO_Schema_Context does, sorry for that.

As the comments say, context is a Value object with context variables. These variables include a lot of specific methods and properties from Yoast SEO, which go way over my head. But, there is one property that we all know as WordPress users: post. This is an object that has all the properties you would expect: ID, post_author, post_data, post_content, post_title, post_type,...

We will be using this shortly but right now just remember that we have access to context inside our class.

is_needed method

We now add a is_needed method to our class Vehicle.

public function is_needed() {

  /**
    * Determines whether or not a piece should be added to the graph.
    *
    * @return bool Whether or not a piece should be added.
    */

  // copied from https://github.com/Yoast/wordpress-seo/blob/trunk/src/generators/schema/author.php

  if ( $this->context->indexable->object_type === 'user' ) {
    return true;
  }

  if (
    $this->context->indexable->object_type === 'post'
    && $this->helpers->schema->article->is_author_supported( $this->context->indexable->object_sub_type )
    && $this->context->schema_article_type !== 'None'
  ) {
    return true;
  }

  return false;
}
Enter fullscreen mode Exit fullscreen mode

As the comments state, this method determines whether or not a piece should be added to the graph. It returns a boolean. So, we are adding a Vehicle piece linked to Person. This means that Vehicle is needed only when there is a Person. By default, WordPress uses Person as "author" on blog posts and author profile pages. We could then do something like this:

public function is_needed() {

  if (
    is_single() || is_author()
  ) {
    return true;
  }
  return false;
}
Enter fullscreen mode Exit fullscreen mode

But, we won't do this because the Yoast team has done the work for us. They have provided a bunch of methods that make checks and validations. These methods are available through context as explained in the previous point.

As stated before, we only need Vehicle when there is a Person. So what I ended up doing was looking up the Person class in Yoast SEO sourcecode on gitHub: https://github.com/Yoast/wordpress-seo/blob/trunk/src/generators/schema/author.php and I just copy pasted the is_needed method from Person into my Vehicle piece.

I hope this makes sense. The simple conditionals like is_single() provide no validation of the data. The Yoast methods on the context object do have validation. But, since the conditions for Vehicle are the same as for Person, I just copied those. Don't worry if you don't really understand the content of this copied method.

generate method

This last method we need to add to our class and is generate. It handles the generation of the json-ld content. Let's first look at the full code and then explain it.

/**
  * Add Vehicle piece of the graph.
  *
  * @return mixed
  */
public function generate() {

  $post_author_id = $this->context->post->post_author;

  // we should probably add some data validation here
  $data = [
    "@type"         => "Vehicle",
    "@id"           => "author#vehicle",
    "name"          => get_field( 'vehicle_name', "user_$post_author_id" ),
    "numberOfDoors" => get_field( 'vehicle_doors', "user_$post_author_id" ),
    "weightTotal"   => [
      "value"     => get_field( 'vehicle_weight', "user_$post_author_id" ),
      "unitCode"  => "KGM"
    ]
  ];
  return $data;
  }
}
Enter fullscreen mode Exit fullscreen mode

Take a look at the $data variable first. It's clear that these hold all the properties we want in our Vehicle. The "@type" property refers to https://schema.org/Vehicle, that is obvious.

The "name", "numberOfDoors" and "weightTotal" props all get data from the ACF get_field function. get_field takes 2 parameters:

  1. The key of the custom field, f.e. "vehicle_name"
  2. An id, f.e. author_1 (the author with id 1)

How do we get access to the author id? Remember the "context" property on our class? Here is were we use it. We use "post" prop on context. Post holds all post data, includes "author_id". This should make sense now. We store the id in a variable ($this refers to our class Vehicle):

$post_author_id = $this->context->post->post_author;
Enter fullscreen mode Exit fullscreen mode

And later use this id in the get_fields function:

"name" => get_field( 'vehicle_name', "user_$post_author_id" ),
Enter fullscreen mode Exit fullscreen mode

About the "id" prop. Yoast has a standardized approach to IDs. However, I wasn't quite able to make this work for me, so I just hardcoded the "id": "author#vehicle".

Lastly, I added a comment line into this method:

//we should probably add some data validation here
Enter fullscreen mode Exit fullscreen mode

Data needs validation. For example, if the user didn't enter how many doors his car has, you shouldn't add the "numberOfDoors" prop. You can also use is_needed prop for validation. I left out validation because this tutorial is long enough already.

The entire class Vehicle

You should now be able to understand the entire class. First we make context available. Then we create the is_needed and generator methods.

// functions.php

class Vehicle extends Abstract_Schema_Piece {

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

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

  public function is_needed() {

    /**
      * Determines whether or not a piece should be added to the graph.
      *
      * @return bool Whether or not a piece should be added.
      */

    // copied from https://github.com/Yoast/wordpress-seo/blob/trunk/src/generators/schema/author.php

    if ( $this->context->indexable->object_type === 'user' ) {
      return true;
    }

    if (
      $this->context->indexable->object_type === 'post'
      && $this->helpers->schema->article->is_author_supported( $this->context->indexable->object_sub_type )
      && $this->context->schema_article_type !== 'None'
    ) {
      return true;
    }

    return false;
  }

  /**
  * Add Vehicle piece of the graph.
  *
  * @return mixed
  */
  public function generate() {

    $post_author_id = $this->context->post->post_author;

    // we should probably add some data validation here
    $data = [
      "@type"         => "Vehicle",
      "@id"           => "author#vehicle",
      "name"          => get_field( 'vehicle_name', "user_$post_author_id" ),
      "numberOfDoors" => get_field( 'vehicle_doors', "user_$post_author_id" ),
      "weightTotal"   => [
        "value"     => get_field( 'vehicle_weight', "user_$post_author_id" ),
        "unitCode"  => "KGM"
      ]
    ];
    return $data;
    }
  } 
}
Enter fullscreen mode Exit fullscreen mode

Register the class

Two more things. Now that we have created this class, we still have to register our class:

add_filter( 'wpseo_schema_graph_pieces', 'yoast_add_graph_pieces', 11, 2 );

/**
* Adds Schema pieces to our output.
*
* @param array                 $pieces  Graph pieces to output.
* @param \WPSEO_Schema_Context $context Object with context variables.
*
* @return array Graph pieces to output.
*/
function yoast_add_graph_pieces( $pieces, $context ) {
  $pieces[] = new Vehicle( $context );
  return $pieces;
}
Enter fullscreen mode Exit fullscreen mode

Link Person to Vehicle

Now our Vehicle piece is up and running. But, what is missing is the link from Person. Luckily, we already know how to do this from part 2 in this series. We add a property to Person that links to Vehicle.

// functions.php

add_filter( 'wpseo_schema_person', 'add_owns_property_to_person', 11, 1 );
function add_owns_property_to_person( $data ) {
  // we should again validate here first
  $data['owns'] = [ "@id" => "author#vehicle" ]; 
  return $data;
}
Enter fullscreen mode Exit fullscreen mode

Notice here how we are again hardcoding the "id" prop.

Summary

Glad to see you made it this far. This is a long article but I wanted to take the time and go over each code snippet. In the end isn't that hard.

  1. Extend the Abstract_Schema_Piece class.
  2. Make context available.
  3. Add is_needed method to determine when and where to render the piece.
  4. Add generate method to populate the properties of your piece.

In the next and last part of this series, we look into a second way to create a piece. Not by extending the Abstract_Schema_Piece class but by extending another piece. Don't worry, this will be shorter and simpler.

💖 💪 🙅 🚩
peterlidee
Peter Jacxsens

Posted on June 4, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related