Customizing Yoast SEO's structured data with schema API part 3
Tags: wordpress, yoast, schemaapi
Published: 2022-06-04 15:19:49

The [first part](https://dev.to/peterlidee/customizing-yoast-seos-structured-data-with-schema-api-part-1-1p76) of this series talked about how [Yoast SEO](https://yoast.com/wordpress/plugins/seo/) implements structured data. The [second part](https://dev.to/peterlidee/customizing-yoast-seos-structured-data-with-schema-api-part-2-591f) showed how to add, remove or edit properties of a schema and how to remove a schema/piece.

In the 3rd and [4th parts](https://dev.to/peterlidee/customizing-yoast-seos-structured-data-with-schema-api-part-4-2bdf), 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](https://github.com/Yoast/wordpress-seo/blob/trunk/src/generators/schema/abstract-schema-piece.php). The class that generates for example the [`Organization` piece](https://github.com/Yoast/wordpress-seo/blob/trunk/src/generators/schema/organization.php) is created by extending the `Abstract_Schema_Piece` class.

```php
// 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](https://dev.to/peterlidee/customizing-yoast-seos-structured-data-with-schema-api-part-4-2bdf).

```php
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:

```json
{
  "@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](https://wordpress.org/plugins/advanced-custom-fields/). 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:

```php
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`:

```php
// 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.

```php
// 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`.

```php
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:

```php
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.

```php
/**
  * 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: 

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`):

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

And later use this id in the `get_fields` function:

```php  
"name" => get_field( 'vehicle_name', "user_$post_author_id" ),
```
About the "id" prop. Yoast has a [standardized approach to IDs](https://developer.yoast.com/features/schema/technology-approach#ids-relationships-and-nesting). 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:

```php 
//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.

```php
// 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:

```php
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](https://dev.to/peterlidee/customizing-yoast-seos-structured-data-with-schema-api-part-2-591f) in this series. We add a property to `Person` that links to `Vehicle`.

```php
// 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. 

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](https://dev.to/peterlidee/customizing-yoast-seos-structured-data-with-schema-api-part-4-2bdf), 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.