Peter Jacxsens
Posted on June 4, 2022
Thus far we have looked into how Yoast SEO implements structured data, how we can use schema API to alter properties of a piece / schema and how to make a new custom piece.
In the final part of this series we take a look a second way to customize a piece. This will be a more pratical example. I did something very similar on a site I recently build.
The goal
Let's imagine we're building a little website for a plumber and we want to customize the structured data. By default Yoast SEO would give us the Organization
piece. But there are more specific schemas for a plumber. Upon reading the documentation we find an actual https://schema.org/Plumber schema. (LocalBusiness > HomeAndConstructionBusiness > Plumber) So, of course we want to use this.
A LocalBusiness
differs from an Organization
in that a LocalBusiness
gets these props: "currenciesAccepted", "openingHours", "paymentAccepted" and "priceRange". That's all. The HomeAndConstructionBusiness
and Plumber
schema's are identical to LocalBusiness
except for their type.
Because Plumber
and Organization
are so similar we are going to make the Plumber
class by extending the Organization
class. This lets us benefit from all the properties and methods that Organization
already has. We can then edit these props or add new ones if needed.
When building a custom piece, wherever possible, try to base it on an existing class. For example: make a TeamMember
or a Speaker
by extending Person
or make a CarReview
by extending Article
.
Let's start building.
Remove Organization
We already covered how to remove a piece in part 2.
add_filter( 'wpseo_schema_graph_pieces', 'remove_organization_from_schema', 11, 2 );
function remove_organization_from_schema( $pieces, $context ) {
return \array_filter( $pieces, function( $piece ) {
return ! $piece instanceof \Yoast\WP\SEO\Generators\Schema\Organization;
});
}
This will break the referral the chain but we will fix this later.
Add Plumber
We are going to extend Organization
so let's first look at what props Organization
actually renders:
{
"@type": "Organization",
"@id": "https://www.mycompany.com/#organization",
"name": "My company",
"url": "https://www.mycompany.com/",
"sameAs": [],
"logo": {
"@type": "ImageObject",
"@id": "https://www.mycompany.com/#/schema/logo/image/",
"url": "https://www.mycompany.com/wp-content/uploads/2022/05/logo.jpg",
"contentUrl": "https://www.mycompany.com/wp-content/uploads/2022/05/logo.jpg",
"width": 500,
"height": 200,
"caption": "My company"
},
"image": {
"@id": "https://www.mycompany.com/#/schema/logo/image/"
}
},
A sidenote: Notice how the logo prop has an "id" that gets referenced by the "image" prop. Yoast kept the logo nested for some (probably good) reason.
As I said before, Plumber
and Organization
are quite similar. To convert above piece to a Plumber
piece we would only have to change the type. The extra props like "currenciesAccepted" are "openingHours" are optional. They are not required for a valid Plumber
schema. Let's make the Plumber
class.
// functions.php
use Yoast\WP\SEO\Generators\Schema\Organization;
class Plumber extends Organization {
public function generate(){
// get the fields from Organization
$data = parent::generate();
// we overwrite @type
$data['@type'] = 'Plumber';
return $data;
}
}
As you can see, we only have the single method generate
. This line $data = parent::generate();
is where a lot of the magic happens. We call the generate
method from the parent class Organization
. The generate method returns a data object that we store in our $data property. In other words, the data variable now holds all the properties like "@type" and "id" from Organization
. We need to change "@type" so we just overwrite it to 'Plumber'. We will talk about "id" in a bit. The result:
{
"@type": "Plumber",
"@id": "https://www.mycompany.com/#organization",
"name": "My company",
"url": "https://www.mycompany.com/",
"sameAs": [],
"logo": {
"@type": "ImageObject",
"@id": "https://www.mycompany.com/#/schema/logo/image/",
"url": "https://www.mycompany.com/wp-content/uploads/2022/05/logo.jpg",
"contentUrl": "https://www.mycompany.com/wp-content/uploads/2022/05/logo.jpg",
"width": 500,
"height": 200,
"caption": "My company"
},
"image": {
"@id": "https://www.mycompany.com/#/schema/logo/image/"
}
},
What about the other methods like is_needed
? These are public methods and they are inherited from Organization
. In our case, Person
is needed in the same way Organization
is needed. If you are writing something else with a different need you can simply declare the in_needed
method and write your conditional.
A quick word about context. I'm not quite sure if and how context is inherited. As per the schema API documentation, you should declare the "$context" property and _construct
method in your child class when you need context. Like we did in part 3.
The "id" prop
The "id" prop links different pieces together. But, it holds no semantic meaning. If you were to use "ids" like "fluffybunny" or a hash like "dlg68sv98s", everything would still work. Yoast has a standardized approach to IDs that helps to make sense of this structured data mess.
Now, our Plumber
class will inherit it's "id" from Organization
and thus the id will be something like "url#organization". You might be tempted to go for "url#plumber". But this would unlink everything else. Website
for example would then link to a non-existing Organization
piece. So, you would have to make all other pieces refer to "url#plumber"! And I'm not sure how to do that. On top of that, in our case, we don't need to. "id" has no semantic meaning and if we keep using "url#organization" as "id" we don't have to update anything.
Hopefully this makes sense. We removed the Organization
piece but all the other pieces still link to Organization
. So, if we give our new piece Plumber
the same "id" as Organization
, everything is correctly linked.
What happens if you do need a different "id". That, you will have to figure out on your own. You may want to look into the wpseo_schema_graph filter.
Register Plumber
Let's register this piece:
// adds Schema pieces to our output.
add_filter( 'wpseo_schema_graph_pieces', 'yoast_add_graph_pieces', 11, 2 );
function yoast_add_graph_pieces( $pieces, $context ) {
$pieces[] = new Plumber( $context );
return $pieces;
}
This however leads to a strange result. As we talked about in part 1, Yoast SEO always renders 3 pieces on every page: Organization
(or a subtype), Website
and Webpage
, in that order. We removed Organization
and just now added Plumber
. But this led to this result: Website
, Webpage
and only then Plumber
. So, the order is now different.
Is this a problem? I'm not sure. Website
refers to Plumber
but Plumber
is only declared later? Problem or not? I don't know. To be on the safe side I decided to add the new Plumber
piece to the beginning of the array instead of to the end of the array. This worked and placed Plumber
as the first piece, in front of Website
. The new registration:
// adds Schema pieces to our output.
add_filter( 'wpseo_schema_graph_pieces', 'yoast_add_graph_pieces', 11, 2 );
function yoast_add_graph_pieces( $pieces, $context ) {
// add to beginning of the array
array_unshift( $pieces, new Plumber( $context ));
return $pieces;
}
Add some properties
While we're at it, let's add some props to our new Plumber
class' generate method.
// functions.php
use Yoast\WP\SEO\Generators\Schema\Organization;
class Plumber extends Organization {
public function generate(){
// get the fields from Organization
$data = parent::generate();
// we overwrite @type
$data['@type'] = 'Plumber';
// hardcoded
$data['priceRange'] = '$$$$';
// dynamic
$data['email'] = get_option('company-email');
return $data;
}
}
Remember, if you need to, you can also use context to retieve data as we saw in part 3. Let's take a look at the result of the above code:
{
"@type": "Plumber",
"@id": "https://www.mycompany.com/#organization",
"name": "My company",
"url": "https://www.mycompany.com/",
"sameAs": [],
"logo": {
"@type": "ImageObject",
"@id": "https://www.mycompany.com/#/schema/logo/image/",
"url": "https://www.mycompany.com/wp-content/uploads/2022/05/logo.jpg",
"contentUrl": "https://www.mycompany.com/wp-content/uploads/2022/05/logo.jpg",
"width": 500,
"height": 200,
"caption": "My company"
},
"image": {
"@id": "https://www.mycompany.com/#/schema/logo/image/"
},
"priceRange": "$$$$",
"email": "email@company.com"
},
And that's it, we're done.
Summary
In this final part we've shown how to create a custom piece by building on an existing piece class. This will save you time and a couple of headaches.
I hope this serie has given a solid introduction to Yoast SEO schema API. I will leave you with a few more tips.
Make sure to test your json-ld. Use google rich snippet test tool for this. It's a good idea to test before you actually start building.
Validate your data. Make sure you have actual values for your piece properties. If not, handle it.
Read the Schema API docs. They are not that long, have options I didn't cover here and if you made it this far, the docs will be easy to follow.
Good luck and good coding.
Posted on June 4, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.