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_Piece
class. - Make context available.
- Add
is_needed
method to determine when and where to render the piece. - 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.