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

Peter Jacxsens - Jun 4 '22 - - Dev Community

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;
  });
}
Enter fullscreen mode Exit fullscreen mode

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/"
  }
},
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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/"
  }
},
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
},
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player