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
}
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
}
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"
}
}
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');
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 {
}
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;
}
}
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;
}
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;
}
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;
}
}
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:
- The key of the custom field, f.e. "vehicle_name"
- 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;
And later use this id in the get_fields function:
"name" => get_field( 'vehicle_name', "user_$post_author_id" ),
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
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;
}
}
}
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;
}
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;
}
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.
- Extend the
Abstract_Schema_Piececlass. - Make context available.
- Add
is_neededmethod to determine when and where to render the piece. - Add
generatemethod 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.
Top comments (2)
Hi Peter, thanks for this !
I am getting the following error when using your code :
Error: Class "Abstract_Schema_Piece" not found.Any idea on how to solve this ?
Hi Fabien, Did you add
use Yoast\WP\SEO\Generators\Schema\Abstract_Schema_Piece;?