When you create a web page, post or item, search engines find it hard to understand specific elements. You want to keep control on how it is displayed, but how a search engine sees it can be very different.
Structured Data helps search engines to better understand what the content is specifically about. But it will also allow users to see the value of a website before they visit, via rich snippets, which are rich data that are displayed in the Search Enige Results Pages.
If you need to read up on what Structured Data is and why you need it, you can read more here.
How YOU can add Structured Data using Toolbox
Structured Data is telling search engines very specifically what they want to know. So first you will need to determine what type of Schema you will need to add to your page/post/archive. Let’s take a recipe for example.
You can find the needed markup on Google’s Structured Data documentation page: https://developers.google.com/search/docs/data-types/recipe
<script type="application/ld+json">
{
"@context": "http://schema.org/",
"@type": "Recipe",
"name": "",
"image": [ ... ],
"author": {
"@type": "Person",
"name": ""
},
"datePublished": "",
"description": "",
"prepTime": "",
"cookTime": "",
"totalTime": "",
"keywords": "",
"recipeYield": "",
"recipeCategory": "",
"recipeCuisine": "",
"nutrition": {
"@type": "NutritionInformation",
"calories": ""
},
"recipeIngredient": [ ... ],
"recipeInstructions": [ ... ],
"review": {
"@type": "Review",
"reviewRating": {
"@type": "Rating",
"ratingValue": "",
"bestRating": ""
},
"author": {
"@type": "Person",
"name": ""
},
"datePublished": "2018-05-01",
"reviewBody": "",
"publisher": ""
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "",
"ratingCount": ""
},
"video": [ ... ]
}
</script>
This may look daunting but all there’s to it, is filling the fields with field-data you have probably already added to your Custom Post Type, or should add because you want your recipe to be found and displayed using this extra information!
Let’s add a Macro
You can add your schema script inline using the Timber Posts Module, but a better way would be to save your schema template as a macro in the Twig Templates CPT. Macro’s are parts of a Twig Template that can be re-used after importing them into a variable. We’ll first show you the macro, and then how you can re-use it.
This macro below is written for a specific set of data, so your situation could be a little different.
{% macro schema( item ) %}
{# prep variables for use in the template #}
{# get acf-field values #}
{% set recipe = toolbox_get_fields([ 'yield', 'ingredients', 'instructions','preptime', 'cooktime', 'totaltime', 'keywords', 'api_response' ], item.ID ) %}
{# external api data #}
{% set api_response = recipe.api_response|to_json %}
{# Reviews #}
{% set comment_data = function( 'get_comments' , {'post_id': item.ID , 'order': 'comment_date' , 'number' : null } )|cast_to_array %}
{# taxonomy based #}
{% set tax_category = function( 'wp_get_object_terms' , item.ID , 'category' )|cast_to_array %}
{% set tax_cuisine = function( 'wp_get_object_terms' , item.ID , 'cuisine' )|cast_to_array %}
{# used to calculate value for aggregateRating #}
{% set comment_aggregate_total = 0 %}
{% for comment in comment_data %}
{% set comment_aggregate_total = comment_aggregate_total + function( 'get_comment_meta' , comment.comment_ID , 'rating' , true )%}
{% endfor %}
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "Recipe",
"name": "{{item.title}}",
"image": [
"{{item.thumbnail?:TimberImage( 706 )}}"
],
"author": {
"@type" : "Person",
"name": "{{function('the_author_meta' , 'nickname', item.post_author )}}"
},
"datePublished": "{{item.post_date|date('Y-m-d')}}",
"description": "{{item.post_content|truncate( 60 )}}",
"prepTime": "{{item.preptime}}",
"cookTime": "{{item.cooktime}}",
"totalTime": "{{item.totaltime}}",
"keywords": "{% for keyword in recipe.keywords %}{{keyword.keyword}}{{loop.last?'':", "}}{% endfor %}",
"recipeYield": "{{recipe.yield}}",
"recipeCategory": "{{tax_category|first.name}}",
"recipeCuisine": "{{tax_cuisine|first.name}}",
"nutrition": {
"@type": "NutritionInformation",
"calories": "{{ ( api_response.totalNutrients.ENERC_KCAL.quantity / api_response.yield)|round }}"
},
"recipeIngredient": [
{% for ingredient in recipe.ingredients %}
"{{ingredient.ingredient}}"{{loop.last?'':","}}
{% endfor %}
],
"recipeInstructions": [
{% for instruction in recipe.instructions %}
{ "@type": "HowToStep" , "text": "{{instruction.instruction}}" }{{loop.last?'':","}}
{% endfor %}
],
"review": {
"@type": "Review",
"reviewRating": {
"@type": "Rating",
"ratingValue": "{{function( 'get_comment_meta' , comment_data|first.comment_ID , 'rating' , true )}}"
},
"author": {
"@type": "Person",
"name": "{{comment_data|first.comment_author}}"
},
"datePublished": "{{comment_data|first.comment_date|date('Y-m-d')}}",
"reviewBody": "{{comment_data|first.comment_content}}"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "{{comment_aggregate_total}}",
"ratingCount": "{{comment_data|length}}"
}
}
</script>
{% endmacro %}
Save the Twig template with a suiting title. After saving it, check and make note of the the slug of the created Twig Template. Do this by enabling it via the “screen options” (top right corner of your Dashboard). For this example we are going to assume the slug is schema-recipe
. We will need this a little later on.
In this Twig Template you see there are quite a few variables to fill. That means that for every piece of information you want to provide you will probably need to add a custom field for. This example is taken from a real-world recipe site, where the list of ingredients and the number of servings is sent to an API and the Nutrition Information is stored in a json-string (the custom field called ‘api_response’). Toolbox is able to convert that data back to an array so that we may access that information in our template.
There are also a few taxonomies being stored, comments/reviews are being used, and because there is also a Post rating plugin installed we are able to calculate the ratings for the recipe.
Most of the values though are custom fields, like the yield (number of servings), ingredients, instructions, preptime, cooktime, and a few others. They all have their different methods of rendering the values to the script, but once you get the hang of it you will probably learn very quickly that every schema has the same structures and same techniques, so getting the data from YOUR custom fields is applying the same principals over and over again.
Adding the schema to the Post
After you’ve created the Twig Template you need to add it to the layout. open your Themer Layout for the Singular Recipe and drag a Timber Posts Module into view, preferably below a module that sits in a near empty column.
You can leaver the Content as is. We are going to use the current posts data.
On the Template Tab add the following template:
{% import "schema-recipe.twig" as org %}
{{ org.schema( post ) }}
{{ (function('is_user_logged_in' ) and 'fl_builder' in GETvars|keys) ?'schema-data'}}
The first line imports the Twig Template schema-recipe.twig
and stores it as the variable org
.
The second line calls and render our macro using org.schema( post )
The third line makes sure we have a way to see the module in the editor, since the <script> data is not visual to the visitor, only to the Search Robots. By checking if the user is logged in and in a fl_builder session we can add a little line of text for some help.
Adding the schema to the archive page
This same technique can be applied to the archive page for the recipes. This time you can use a loop to display schema for all the recipes on that page:
{% import "schema-recipe.twig" as org %}
{% for item in posts %}
{{ org.schema( item ) }}
{% endfor %}