I polled a group of WordPress developers about schemas the other day and was surprised by the results. Even though almost all of them had heard of schemas and were aware of the potential benefits they provide, very few of them were actually using them on a project.
If you’re unfamiliar with schemas, they are HTML attributes that help search engines understand the content structure and know-how to display it correctly in search engine results. We’ve all worked on projects where SEO was a big ol’ concern, so schemas can be a key deliverable to help optimize and deliver search performance.
We’re going to dig into the concept of schemas a little more in this post and then walk through a real-life application of how to use them in a WordPress environment.
A Schema Overview
Schemas are a vocabulary of HTML attributes and values that describe the content of the document. The concept of this vocabulary was born out of a collaboration between members of Google, Microsoft, Yahoo, and Yandex and has since become a project that is maintained by those founding organizations, in addition to members from the W3C and individuals in the community. In fact, you can view the Schema community’s activity and connect with the group on their open community page.
You may see the term structured data tossed around when schemas are being discussed and that’s because it’s a good description of how schemas work. They provide a lexicon and hierarchy in the form of data that add structure and detail to HTML markup. That, in turn, makes the content of an HTML document much easier for search engines to crawl, read, index, and interpret. If you see structured data somewhere, then we’re really talking about schemas as well.
The Schema Format
Schema can be served in three different formats: Microdata, JSON-LD, and RDFa. RDFa is one we aren’t going to delve into in this post because Microdata and JSON-LD make up the vast majority of use cases. In fact, as we dive into a working example later in this post, we’re going to shift our entire focus on JSON-LD.
Let’s illustrate the difference between Microdata and JSON-LD with an example of a business listing website, where visitors can browse information about local businesses. Each business is going to be an item that has additional context, such as a business type, a business name, a description, and hours of operation. We want our search engines to read that data for the sake of being able to render that information cleanly when returning search results. You know, something like this:

Here’s how we would use Microdata to display business hours in a similar way:
<div itemscope="" itemtype="http://schema.org/Pharmacy">
<h1 itemprop="name">Philippa's Pharmacy</h1>
<p itemprop="description">
A superb collection of fine pharmaceuticals.
</p>
<p>Open:
<span itemprop="openingHours" content="Mo,Tu,We,Th 09:00-12:00">
Monday-Thursday 9am-noon
</span>
</p>
</div>
The same can be achieved via JSON-LD:
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "Pharmacy",
"name": "Philippa's Pharmacy",
"description": "A superb collection of fine pharmaceuticals.",
"openingHours": "Mo,Tu,We,Th 09:00-12:00"
}
</script>
How Schema Impacts SEO
The reason we’re talking about schema at all is that we care about how our content is interpreted by search engines, so it’s fair to wonder just how much impact schema has on a site’s actual search engine ranking and performance.
Google’s John Mueller participated in a video chat back in 2015 and gave a very clear indication of how important schemas are becoming in the field of search engine optimization. The fact that the schema project was founded and is maintained by giants in the search engine industry gives us a good idea that, if we want to rank and index well, then we’ll consider schema as part of our SEO strategy.
While there may be other sites and posts out there that have better data to back up the importance of schema, the thing we ought to point to is the impact it has on user experience. If someone were to look up “Tom Petty Concert Tickets” in Google and get a list of results back, it’s easy to assume that the result with upcoming dates nicely outlined in the results would be the one that stands out the most and is most identifiably useful, even if it is not the first result in the bunch.

Again, this is conjecture and other posts or sites may have data to support the impact that schema has on search result rankings, but having a little bit of influence on the way search engines read and display our content on their pages is a nice affordance for us as front-end developers and we’ll take what we can get.
Deciding Which Format to Use
It really comes down to your flavor preference at the end of the day. That said, Google’s schema documentation is nearly all centered around JSON-LD so if you’re looking for more potential impact in Google’s results, that might be your starting point. Google even has a handy Webmasters tool that generates data in JSON-LD making it perhaps the lowest barrier to entry if you’re getting started.
Knowing What Data Can Be Structured
Google’s guide to structured data is the most exhaustive and comprehensive resource on the topic and gives the best indication of what data can be structured with examples of how to do it.
The bottom line is that schema wants to categorize content into “types” and these are the types that Google currently recognizes as of this writing:
- Articles
- Books
- Courses
- Datasets
- Events
- Fact Check
- Job Postings
- Local Businesses
- Music
- Podcasts
- Products
- Recipes
- Reviews
- TV & Movies
- Videos
In addition to content type, Google will also look for structured data that serve as UI enhancements to the search results:
- Breadcrumbs
- Sitelinks Searchbox
- Corporate Contact Information
- Logos
- Social Profile Links
- Carousels
You can really start to see the opportunities we have to help influence search results as far as what is displayed and how it is displayed.
Managing Schema in WordPress
Alright, we’ve spent a good amount of time diving into the concept of schemas and how they can benefit a site’s search engine optimization, but how the heck do we work with it? I find the best way to tackle this is with a real-life example, so that’s what we’re going to do.
In this example, we’re using WordPress as our content management system and will put the popular Advanced Custom Fields (ACF) plugin to use. In the end, we will have a way to generate schema for our content on the fly using valid JSON-LD format.
Some readers may be tempted to stop me here and ask why we aren’t using the built-in schema management tools of popular WordPress SEO plugins, like Yoast and Schema. There are actually a ton of WordPress plugins that help add structured data to a site and going with any of them is a legitimate option that you ought to consider. In my experience, these plugins do not provide the level of detail I am looking for in projects that require access and control over every content type I need, such as opening hours and contact information for a local business.
That’s where ACF comes to my rescue! Not only can we create the exact fields we need to capture the data we want to generate and serve, but we can do it dynamically as part of our everyday content management in WordPress.
Let’s use a local business (spoiler alert on the Content-Type, am I right?!) website as an example. We’re going to create a custom page in WordPress that contains custom fields that allow us to manage the structured data for the business.
Here’s what that will look like:

I’ve put put all the working examples in this post together in a GitHub repo that you can use as a starting point or simply to follow along as we break down the steps to make it happen.
Step 1: Create the Custom Options Page
Setting up a custom admin page in WordPress can be done directly in our functions.php
file:
// Create a General Options admin page
// `options_page` is going to be the name of ACF group we use to set up the fields
// We can use that as a conditional statement to create the page against
if (function_exists('acf_add_options_page')) {
acf_add_options_page(array(
'page_title' => 'General Options',
'menu_title' => 'General Options',
'menu_slug' => 'general-options',
'capability' => 'edit_posts',
'redirect' => false
));
}
That snippet gives us a new link in the WordPress navigation called General Options, but only after hooking things up in ACF in the next step. Of course, you can call this whatever you’d like. The point is that we now have a method for creating a page and a way to access it.
Step 2: Create the Custom Fields
Well, our General Options page is useless if there’s nothing in it. With Advanced Custom Fields installed and activated, we now need to head over there and set up the fields needed to capture and store our structured data.
Here is how our custom fields will be organized:
- Company Logo
- Company Address
- Hours of Operation
- Closed Days
- Contact Information
- Social Media Links
- Schema Type
There are a lot of fields here and you can use the acf-export.json
file from the GitHub repo to import the fields into ACF rather than manually creating them all yourself. Note that some of the fields use a repeater functionality that is only currently supported with a paid ACF extension.
Step 3: Linking Custom Fields to General Options
Now that we have the custom fields set up in ACF, our next task is to map them to our custom General Options page. Really, this step comes as the custom fields are bring created. ACF provides settings for each field group that allows you to specify whether the fields should be displayed on specific pages.
In other words, for each field group we’ve created, be sure to go back in and confirm that the General Options page is selected so that the fields only display there in WordPress:

Now our General Options page has an actual set of options we can manage!
Please Note:: The way the data is organized in the example files is how I’ve grown accustomed to managing scheme. You may find it easier to organize the fields in other ways, and that’s totally cool. And, of course, if you are working with a different content type than this local business example, then you may not need all of the fields we are working with here or be required to use others.
Step 4: Enter Data
Alright, without data, our structured data would just be … um, structured? Whatever that would be, let’s enter the data.
- Company Logo: Google specifies the ideal size to be
151px
square. Google will use this image if it displays company information to the right of the search results. You can see this in action by searching a well-known company, like Google itself. - Building Photo: This can add some interest to the same company profile card where the Company Logo is displayed, but this field also impacts search results within maps. Google recommends a square
200px
image. - Schema Type: Select the content type for the schema. In this example, we are dealing with a local business, so that is the content type.
- Address: These are pretty straight-forward text fields and will be used both in search results and the same profile card as the Company Logo.
- Openings: The specification for opening hours can be found on the schema.org website. The way we’ve set this up in the example is by using a repeater field that contains four sub-fields to specify the days of the week, the starting open time, the ending open time, and a toggle to distinguish between open and closed time ranges. This should cover all our bases, according to the schema documentation.
- Special Days: These are holidays (e.g. Christmas) where the business might not be open during its regular operating hours. It’s nice that schema provides this flexibility because it allows users to see those exceptions if they happen to be searching on those days.
- Contact: There are a lot of settings available for contact data. We are putting three of them use here with this example, namely Type (which is used like a business Department, say, Sales or Customer Service), Phone (which is the number to call), and Option (which supports options for
TollFree
andHearingImpairedSupported
Step 5: Generate the the JSON-LD
This is where the rubber meets the road. If so far we have created a place to manage our data, made the fields for that data, and actually entered the data, then we now need to take that collected data and spit it out into a format that search engines can put to use. Again, the GitHub repo has the finished result of what we’re dealing with, but let’s dig into that code to see how that data is fetched from ACF and converted to JSON-LD.
To read all the values and create the JSON-LD tag, we need to go into the functions.php
file and write a snippet that injects our JSON data to the site header. We’re going to inject the content type, address, and some data about the site that already exists in WordPress, such as the site name and address:
// Using `wp_head` to inject to the document <head>
add_action('wp_head', function() {
$schema = array(
// Tell search engines that this is structured data
'@context' => "http://schema.org",
// Tell search engines the content type it is looking at
'@type' => get_field('schema_type', 'options'),
// Provide search engines with the site name and address
'name' => get_bloginfo('name'),
'url' => get_home_url(),
// Provide the company address
'telephone' => '+49' . get_field('company_phone', 'options'), //needs country code
'address' => array(
'@type' => 'PostalAddress',
'streetAddress' => get_field('address_street', 'option'),
'postalCode' => get_field('address_postal', 'option'),
'addressLocality' => get_field('address_locality', 'option'),
'addressRegion' => get_field('address_region', 'option'),
'addressCountry' => get_field('address_country', 'option')
)
);
}
The logo is not really a required bit of information, we we’re going to check whether it exists, then fetch it if it does and add it to the mix:
// If there is a company logo...
if (get_field('company_logo', 'option')) {
// ...then add it to the schema array
$schema['logo'] = get_field('company_logo', 'option');
}
Working with repeater fields in ACF requires a little extra consideration, so we’re going to have to write a loop to fetch and add the social media links:
// Check for social media links
if (have_rows('social_media', 'option')) {
$schema['sameAs'] = array();
// For each instance...
while (have_rows('social_media', 'option')) : the_row();
// ...add it to the schema array
array_push($schema['sameAs'], get_sub_field('url'));
endwhile;
}
Adding the data from the Opening Hours fields is a little tricky, but only because we added that additional differentiation between open and closed time ranges. Basically, we need to check for the $closed
variable we set up as part of the field then output the times so they fall in right group.
// Let's check for Opening Hours rows
if (have_rows('opening_hours', 'option')) {
// Then set up the array
$schema['openingHoursSpecification'] = array();
// For each row...
while (have_rows('opening_hours', 'option')) : the_row();
// ...check if it's marked "Closed"...
$closed = get_sub_field('closed');
// ...then output the times
$openings = array(
'@type' => 'OpeningHoursSpecification',
'dayOfWeek' => get_sub_field('days'),
'opens' => $closed ? '00:00' : get_sub_field('from'),
'closes' => $closed ? '00:00' : get_sub_field('to')
);
// Finally, push this array to the schema array
array_push($schema['openingHoursSpecification'], $openings);
endwhile;
}
We can use almost the same snippet to output our Special Days data:
// Let's check for Special Days rows
if (have_rows('special_days', 'option')) {
// For each row...
while (have_rows('special_days', 'option')) : the_row();
// ...check if it's marked "Closed"...
$closed = get_sub_field('closed');
// ...then output the times
$special_days = array(
'@type' => 'OpeningHoursSpecification',
'validFrom' => get_sub_field('date_from'),
'validThrough' => get_sub_field('date_to'),
'opens' => $closed ? '00:00' : get_sub_field('time_from'),
'closes' => $closed ? '00:00' : get_sub_field('time_to')
);
// Finally, push this array to the schema array
array_push($schema['openingHoursSpecification'], $special_days);
endwhile;
}
The last piece is our Contact Information data. Again, we’re working with a loop that creates and array that then gets injected into the schema array which, in turn gets injected into the document .
Notice that the phone number needs the country code, which you can swap out for your own:
// Let's check for Contact Information rows
if (get_field('contact', 'options')) {
// Then create an array of the data, if it exists
$schema['contactPoint'] = array();
// For each row of contact information...
while (have_rows('contact', 'options')) : the_row();
// ...fetch the following fields
$contacts = array(
'@type' => 'ContactPoint',
'contactType' => get_sub_field('type'),
'telephone' => '+49' . get_sub_field('phone')
);
// Let's not forget the Option field
if (get_sub_field('option')) {
$contacts['contactOption'] = get_sub_field('option');
}
// Finally, push this array to the schema array
array_push($schema['contactPoint'], $contacts);
endwhile;
}
Let’s Marvel at Out Work!
We now can encode our data in JSON and put it into a script tag right before the closing of our add_action
function.
echo '<script type="application/ld+json">' . json_encode($schema) . '</script>';
Reader Mark Gombar suggests json_encode($schema, JSON_UNESCAPED_SLASHES)
to avoid escaped slashes in URLs.
The final script might look something like this:
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "Store",
"name": "My Store",
"url": "https://my-domain.com",
"telephone": "+49 1234 567",
"address": {
"@type": "PostalAddress",
"streetAddress": "Musterstraße",
"postalCode": "13123",
"addressLocality": "Berlin",
"addressRegion": "Berlin",
"addressCountry": "Deutschland"
},
"sameAs": ["https://facebook.com/my-profile"],
"openingHoursSpecification": [{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Mo", "Tu", "We", "Th", "Fr"],
"opens": "07:00",
"closes": "20:00"
}, {
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Sa", "Su"],
"opens": "00:00",
"closes": "00:00"
}, {
"@type": "OpeningHoursSpecification",
"validFrom": "2017-08-12",
"validThrough": "2017-08-12",
"opens": "10:00",
"closes": "12:00"
}],
"contactPoint": [{
"@type": "ContactPoint",
"contactType": "customer support",
"telephone": "+491527381923",
"contactOption": ["HearingImpairedSupported"]
}]
}
</script>
Conclusion
Hey, look at that! Now we can enhance a website’s search engine presence with optimized data that allows search engines to crawl and interpret information in an organized way that promotes a better user experience.
Of course, this example was primarily focused on JSON-LD, Google’s schema specifications, and using WordPress as a vehicle for managing and generating data. If you have written up ways of managing and handling data on other formats, using different specs and other content management systems, please share it here in the comments and we can start to get a bigger picture for improving SEO all around.
Brilliant recap,
thanks!
Wow, thanks so much for going into schemas in depth.
I have been looking around a lot lately and thought there must be some solution using ACF… But there really is not much out there, with or without ACF.
Excited… Will try it as soon as possible. :D
While I find your article very interesting and will definitely try that, I’m thinking of accessibility and block rendering.
Does anyone that uses screen readers take advantage of those microdata tags? I don’t know, just guessing. There might be a reason why microdata is still being developed and mantained (I find it easier to manage than JSON-LD).
Also, if you put that script in the document head, doesn’t it block the rendering of the webpage?
SEO stuff is not necessarly related with acessibility. AFAIK, screen readers ignore these tags, and are mostly focused on ARIA HTML attributes, which essentially are working the same way as Schemas do, but are created specifically with a11y in mind.
HTML5 Doctor features a helpful introduction, while MDN offers a nice overview and resources to the topic.
cu, w0lf.
If you are concerned about render blocking, you should be able to use ‘wp_footer’ in the action hook to output the code to the footer instead.
As of rendering blocking, This code should be in the head section and MUST be in the head for AMP pages etc etc.
This is exactly what I was looking for to implement on my website. I’m using a theme from WordPress & having a difficulty. May I email you if I run into problems?
Thanks for your help!
Yea, sure.
email(at)artofmyself.com
But I might not be familiar with your problem since I don’t use premade themes at all, but let’s see.
Awesome, thanks Pascal! I was just yesterday looking into doing schema markup for a restaurant website built in WordPress, so this is a perfect starter kit for me :)
Curious to know the answers to João’s questions about accessibility and render blocking.
Moz has a restaurant-specific schema article if anyone’s interested. Writing markup for the menu sounds… not fun.
You got me curious!
I’ve tried ACF -finally- for the first time, really cool.
By the way, I am the developer of Schema plugin, I landed here while looking for a good read.
I’ve been thinking about this! The Schema plugin has a built-in post meta fields generator that allow you to create post meta fields and automatically uses its value to filter the markup output. But, of course… There is no support for special types of fields (example: address, working hours…etc)
ACF -and other similar plugins- comes really handy to fill the missing pieces. I like how you put together the address fields.
So, I’ve decided to give it a try, and gratefully… I was able to replace the old post meta generator with a new ACF generator :) , and now it’s time to develop some custom fields and conditionally change the markup output based on field type. By this way, we don’t have to write any additional code, well… If this is complete though.
Thanks again for the good writ up, and inspiration!
Great work with the Shema plugin btw
A) As a “Schema plugin developer” I would Consider: Learn and jump into ACFs fundamentional world and take the achitecture ideas you find. All “map variables” solution can be done so beatiful with ACF, and improve the plugin with that logic. But make shure you have the PRO version with repeater field.
B) Go towards as ACF extension instead, there was some projects on Github but they faded away. But I can see a huge interest in Forums of ACF, as Im with developer licence. But make your own repeater version. Or Ask/ Look into their Licence agreement, you are allowed to “use” version of code as free plugin.
Last 6 month(s) we been trying out diffrent Schema solutions for our projects. We tested both free and paid plugins. Schema plugin (!) was the choice we pick for general purposes even if WPSSO has great possibilities -and even if Schema plugin’s “flexible field building process” is of absolutley no use.
Hey Jonas, glad to hear that, and many thanks for your feedback.
I am ahead of this now, I got a pro ACF license to play with, it’s settled in one of my plugins (extensions) already for testing, and it’s going really good.
This is a great article. I do have a question though. My business have 4 locations and I’m trying to figure out how to handle this.