Annotated JSON-LD Structured Data Examples
Want to see a some real-world examples of JSON-LD Structured Data “in the wild”? This article has that, plus annotation.
Andrew Welch / nystudio107
Structured data allows you to tell search engines contextual information about what is on your web pages, rather than leaving it up to them to guess. It uses the standardized vocabulary from schema.org expressed via JSON-LDto convey the context and connections between data.
Many articles talk about structured data; this article is going to focus on real-world examples of using structured data “in the wild”. It uses the SEOmatic plugin for Craft CMS in the examples, but this article provides tooling-agnostic annotated JSON-LD structured data examples that anyone can use.
I’m not going to try to sell you on the benefits of using structured data in this article. I’m going to assume that you’re already on board, and want to see some real-world, annotated examples.
For more information on JSON-LD structured data in general, check out the JSON-LD, Structured Data and Erotica article.
A General Approach to Structured Data
While there are several formats for structured data, Google, Apple, and other major players in the industry have made it clear that JSON-LD is the way forward. JSON-LD is the same JSON we’re used to from JavaScript, with the LD standing for “Linked Data”.
Schema.org defines a large vocabulary of structured data schemas to choose from; how do we decide what to implement? My approach is twofold:
- Define schema types as explicitly as possible using a “best fit” approach
- Use schema types that Google has publicly supported
After all, we’re putting structured data onto our website so that search engines can understand it, but we also want to be pragmatic about it, and focus our energy on the schema types that we know Google will consume.
Check out the schema types that Google has publicly supported and also explore the Google Search Gallery for an interactive look at how schema types can affect not just the knowledge graph, but also the Search Engine Results Page (SERP).
Node Identifiers and mainEntityOfPage
Before we get into the specific examples, there are a couple of general concepts that I want to cover.
The first is that when representing JSON-LD, I make heavy use of Node Identifiers (the "@id " properties) to reference specific schemas without repeating data needlessly. We can have an Organization schema and then just reference it from multiple places, rather than replicating the entire schema data each time.
Here’s a truncated example:
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@id": "our-organization",
"@type": "Organization"
}
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "Article",
"publisher": {
"@id": "our-organization"
}
}
</script>
In this case we used our-organization as a Node Identifier, but often just as a convention we can use URLs or URLs with hashes so that it feels name spaced. It really doesn’t matter, as long as it is locally unique on your webpage.
Think of Node Identifiers as a way to define a label or alias to another node in your local schema graph. You assign a locally unique "@id " to a JSON-LD schema, and then you can refer to that "@id " from other JSON-LD schemas.
More on Node Identifiers can be found in the What is the use of @id in json-ld syntax? and Schema.org JSON-LD reference articles.
The mainEntityOfPage property also deserves special mention:
So if a web page is an article like the one you’re reading right now, it’ll have Article JSON-LD with the mainEntityOfPage property’s value being the URL to the article itself:
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "Article",
"mainEntityOfPage": "https://nystudio107.com/blog/implementing-custom-json-ld-structured-data"
}
</script>
It’s saying “Hey, I’m the main subject of the web page at this URL!”
Now let’s dive into looking at some specific examples.
nystudio107.com Article Page
Let’s start by looking at the structured data for the page you’re reading right now. Just by choosing a few settings of how to map data, here’s what you’ll get by default from SEOmatic:
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "Article",
"author": {
"@id": "https://nystudio107.com/#identity"
},
"copyrightHolder": {
"@id": "https://nystudio107.com/#identity"
},
"copyrightYear": "2019",
"creator": {
"@id": "https://nystudio107.com/#creator"
},
"dateModified": "2019-05-28T17:33:57-04:00",
"datePublished": "2019-05-27T08:45:00-04:00",
"description": "Want to see a some real-world examples of JSON-LD structured data \"in the wild\"? This article has that, plus annotation.",
"headline": "Implementing Custom JSON-LD Structured Data",
"image": {
"@type": "ImageObject",
"url": "https://nystudio107.com/img/blog/_1200x630_crop_center-center_82_none/json-ld-structured-data-in-the-wild.jpg"
},
"inLanguage": "en",
"mainEntityOfPage": "https://nystudio107.com/blog/implementing-custom-json-ld-structured-data",
"name": "Implementing Custom JSON-LD Structured Data",
"publisher": {
"@id": "https://nystudio107.com/#creator"
},
"url": "https://nystudio107.com/blog/implementing-custom-json-ld-structured-data"
}
</script>
Most things in this Article schema are pretty straightforward; but why did we choose Article to begin with? Why not BlogPosting?
The answer is simple. Google lists Article as an explicitly supported type. Additionally, if we have Google AMP versions of our pages (and we do for this article), this Article JSON-LD is required for your article to appear in the carousel of AMP stories.
Note that we are using Node Identifiers with "@id " in several places to refer to other JSON-LD structured data nodes rather than repeating the content.
The other key thing to note is the mainEntityOfPage property, which is identifying the fact that this Article JSON-LD structured data is the primary subject of the web page at the listed URL.
This helps tell Google what you consider to be the important part of or thing on the page, rather than having it try to guess it.
This Organization schema is for the entity that owns the website (note the "@id
": "https://nystudio107.com/#identity" Node Identifier):
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@id": "https://nystudio107.com/#identity",
"@type": "Organization",
"address": {
"@type": "PostalAddress",
"addressCountry": "US",
"addressLocality": "Webster",
"addressRegion": "NY",
"postalCode": "14580"
},
"alternateName": "nys",
"description": "We do technology-based consulting, branding, design, and development. Making the web better one site at a time, with a focus on performance, usability & SEO",
"email": "info@nystudio107.com",
"founder": "Andrew Welch, Polly Welch",
"foundingDate": "2013-05-02",
"foundingLocation": "Webster, NY",
"image": {
"@type": "ImageObject",
"height": "2048",
"url": "https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/site/nys_logo_seo.png",
"width": "2048"
},
"logo": {
"@type": "ImageObject",
"height": "60",
"url": "https://nystudio107.com/img/site/_600x60_fit_center-center_82_none/nys_logo_seo.png",
"width": "600"
},
"name": "nystudio107",
"sameAs": [
"https://twitter.com/nystudio107",
"https://www.facebook.com/newyorkstudio107",
"https://plus.google.com/+nystudio107com",
"https://www.youtube.com/channel/UCOZTZHQdC-unTERO7LRS6FA",
"https://github.com/nystudio107"
],
"url": "https://nystudio107.com/"
}
</script>
It exists primarily so that other JSON-LD structured data such as the Article can link to it and say “this is the author” and “this is the copyright holder” and so on and so forth.
You could take this much further and add a list of people who work at the organization via the employee property, or list people who used to work at the organization via the alumni property.
In this way, you build up the context and relationship between things to help Google understand the connections between them.
This Organization schema is for the entity that created the website (note the "@id
": "https://nystudio107.com/#creator" Node Identifier):
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@id": "https://nystudio107.com/#creator",
"@type": "Organization",
"address": {
"@type": "PostalAddress",
"addressCountry": "US",
"addressLocality": "Webster",
"addressRegion": "NY",
"postalCode": "14580"
},
"alternateName": "nys",
"description": "We do technology-based consulting, branding, design, and development. Making the web better one site at a time, with a focus on performance, usability & SEO",
"email": "info@nystudio107.com",
"founder": "Andrew Welch, Polly Welch",
"foundingDate": "2013-05-02",
"foundingLocation": "Webster, NY",
"image": {
"@type": "ImageObject",
"height": "1042",
"url": "https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/site/nys_logo_seo.png",
"width": "1042"
},
"logo": {
"@type": "ImageObject",
"height": "60",
"url": "https://nystudio107.com/img/site/_600x60_fit_center-center_82_none/nys_logo_seo.png",
"width": "600"
},
"name": "nystudio107",
"url": "https://nystudio107.com/"
}
</script>
It exists primarily so that other JSON-LD structured data such as the Article can link to it and say “this is the creator” and so on and so forth.
Just as with the identity Organization, you can take this much further to add additional information about employees, alumni, etc.
Finally we have the automatically generated BreadcrumbList schema that indicates the page’s position in the site hierarchy:
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "BreadcrumbList",
"description": "Breadcrumbs list",
"itemListElement": [
{
"@type": "ListItem",
"item": {
"@id": "https://nystudio107.com/",
"name": "Homepage"
},
"position": 1
},
{
"@type": "ListItem",
"item": {
"@id": "https://nystudio107.com/blog",
"name": "Blog Index"
},
"position": 2
},
{
"@type": "ListItem",
"item": {
"@id": "hthttps://nystudio107.comt/blog/tips-for-using-seomatic-effectively",
"name": "Advanced SEOmatic Tips"
},
"position": 3
}
],
"name": "Breadcrumbs"
}
</script>
This is a supported Google type that helps it understand the structure of your website, and also can show up as actual breadcrumb links on your Search Engine Results Page (SERP).
So this is a nice starting point, but we can do a bit better. Let’s use the SEOmatic Twig API to add a little bit to our mainEntityOfPage JSON-LD:
{#
# Add Article schema for the this plugin as per:
# https://schema.org/Article
#
# @param article the Entry for the blog article
#}
{# Merge in the additional properties to the Article mainEntityOfPage #}
{% set mainEntity = seomatic.jsonLd.get('mainEntityOfPage') %}
{% do mainEntity.setAttributes({
'articleSection': article.blogCategory[0].title,
'genre': 'Technology',
'headline': article.title,
'speakable': {
'type': 'SpeakableSpecification',
'cssSelector': [
'.blog-wrapper',
],
},
}) %}
This adds a few properties, resulting in the following JSON-LD structured data:
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "Article",
"articleSection": "Insights",
"author": {
"@id": "https://nystudio107.com/#identity"
},
"copyrightHolder": {
"@id": "https://nystudio107.com/#identity"
},
"copyrightYear": "2019",
"creator": {
"@id": "https://nystudio107.com/#creator"
},
"dateModified": "2019-05-28T17:33:57-04:00",
"datePublished": "2019-05-27T08:45:00-04:00",
"description": "Want to see a some real-world examples of JSON-LD structured data \"in the wild\"? This article has that, plus annotation.",
"genre": "Technology",
"headline": "Implementing Custom JSON-LD Structured Data",
"image": {
"@type": "ImageObject",
"url": "https://nystudio107.com/img/blog/_1200x630_crop_center-center_82_none/json-ld-structured-data-in-the-wild.jpg"
},
"inLanguage": "en",
"mainEntityOfPage": "https://nystudio107.com/blog/implementing-custom-json-ld-structured-data",
"name": "Implementing Custom JSON-LD Structured Data",
"publisher": {
"@id": "https://nystudio107.com/#creator"
},
"speakable": {
"@type": "SpeakableSpecification",
"cssSelector": [
".blog-wrapper"
]
},
"url": "https://nystudio107.com/blog/implementing-custom-json-ld-structured-data"
}
</script>
Everything should be pretty straightforward here in terms of the properties we’ve added. We’re just fleshing things out a bit by adding some additional information.
Probably the coolest part is the speakable property, which lets you tell screen readers which text is particularly “speakable” via the SpeakableSpecification. In our case, the .blog-wrapper CSS class selector is on the <div>s around our textual article content.
This aids accessibility in terms of screen readers, and also is used by Google Assistant and smart devices.
Additional things that could be done:
- Instead of just listing the identity Organization as the author, have information on the actual person who wrote it
- Add employee and alumni properties to the identity & creator Organization
- Add InteractionCounter via interactionStatistic for user comments on articles, retweets, etc.
- Add HowTo steps for parts of articles that have specific steps to accomplish a task
Go through the schema.org specification for the mainEntityOfPage schema type (Article in this case) and see what properties it makes sense to add. Then validate your page using the Google Structured Data Testing Tool.
Google SERP Preview → nystudio107 Article Page
Google Structured Data Testing Tool link → nystudio107.com Article Page
nystudio107.com Plugin Page
Next up let’s have a look at the JSON-LD structured data for the plugin pages on nystudio107.com, specifically the SEOmatic plugin.
We’re going to skip the identity, creator, and breadcrumbs mentioned earlier, since they’ll be the same here as well.
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "WebApplication",
"author": {
"@id": "https://nystudio107.com/#identity"
},
"copyrightHolder": {
"@id": "https://nystudio107.com/#identity"
},
"copyrightYear": "2017",
"creator": {
"@id": "https://nystudio107.com/#creator"
},
"dateModified": "2019-05-21T11:35:49-04:00",
"datePublished": "2017-12-31T19:48:00-05:00",
"description": "SEOmatic facilitates modern SEO best practices & implementation for Craft CMS 3. It is a turnkey SEO system that is comprehensive, powerful, and flexible.",
"headline": "SEOmatic Craft CMS Plugin",
"image": {
"@type": "ImageObject",
"url": "https://nystudio107.com/img/plugins/seomatic/_1200x630_crop_center-center_82_none/plugin-logo.png"
},
"inLanguage": "en",
"mainEntityOfPage": "https://nystudio107.com/plugins/seomatic",
"name": "SEOmatic Craft CMS Plugin",
"publisher": {
"@id": "https://nystudio107.com/#creator"
},
"url": "https://nystudio107.com/plugins/seomatic"
}
</script>
Here we again get some pretty reasonable defaults for our WebApplication JSON-LD structured data, and we make it clear that this is the mainEntityOfPage as discussed previously.
But with a little custom code from the SEOmatic Twig API we can make it much more interesting:
{#
# Add WebApplication schema for the this plugin as per:
# https://schema.org/WebApplication
#
# @param appInfo the appInfo Entry for the plugin
#}
{# Offer for the plugin #}
{% set offer = seomatic.jsonLd.create({
'type': 'Offer',
'id': '{seomatic.site.identity.genericUrl}#plugin-offer',
'availability': 'http://schema.org/InStock',
'price': appInfo.pluginPrice ??? '0.00',
'priceCurrency': 'USD',
'priceValidUntil': now | date_modify('+1 year') | atom,
'url': appInfo.pluginStoreLink,
}) %}
{# Merge in the additional properties to the WebApplication mainEntityOfPage #}
{% set mainEntity = seomatic.jsonLd.get('mainEntityOfPage') %}
{% set screenshotAsset = appInfo.pluginSeoImage.one() %}
{% set logoAsset = appInfo.pluginIcon.one() %}
{% do mainEntity.setAttributes({
'name': appInfo.title,
'applicationCategory': 'Plugin',
'browserRequirements': 'Requires Craft CMS 3',
'downloadUrl': appInfo.pluginStoreLink,
'installUrl': appInfo.pluginStoreLink,
'image': {
'type': 'ImageObject',
'url': logoAsset.url,
'width': logoAsset.getWidth(),
'height': logoAsset.getHeight(),
},
'screenshot': {
'type': 'ImageObject',
'url': screenshotAsset.url,
'width': screenshotAsset.getWidth(),
'height': screenshotAsset.getHeight(),
},
'offers': offer,
'operatingSystem': 'Web Browser',
}) %}
{# Product for the plugin #}
{% set productConfig = mainEntity.getAttributes() | merge({
'key': 'Plugin-Product',
'type': 'Product',
'brand': {
'id': '{seomatic.site.identity.genericUrl}#identity',
},
'category': 'Plugin',
'isRelatedTo': {
'type': 'Product',
'name': 'Craft CMS',
'description': 'Craft is a flexible, user-friendly CMS for creating custom digital experiences on the web and beyond.',
'slogan': 'Craft is the CMS that makes the whole team happy.',
'url': 'https://craftcms.com/',
'offers': {
'type': 'Offer',
'availability': 'http://schema.org/InStock',
'price': '299.00',
'priceCurrency': 'USD',
'priceValidUntil': now | date_modify('+1 year') | atom,
'url': 'https://craftcms.com/pricing',
},
},
'logo': {
'type': 'ImageObject',
'url': logoAsset.url,
'width': logoAsset.getWidth(),
'height': logoAsset.getHeight(),
},
'productID': appInfo.slug,
'sku': appInfo.slug,
'slogan': appInfo.pluginSlogan,
}) %}
{% set product = seomatic.jsonLd.create(productConfig) %}
This results in a much more fleshed out WebApplication JSON-LD structured data, as well as creating new Offer and Product JSON-LD structured data schemas:
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "WebApplication",
"applicationCategory": "Plugin",
"author": {
"@id": "https://nystudio107.com/#identity"
},
"browserRequirements": "Requires Craft CMS 3",
"copyrightHolder": {
"@id": "https://nystudio107.com/#identity"
},
"copyrightYear": "2017",
"creator": {
"@id": "https://nystudio107.com/#creator"
},
"dateModified": "2019-05-21T11:35:49-04:00",
"datePublished": "2017-12-31T19:48:00-05:00",
"description": "SEOmatic facilitates modern SEO best practices & implementation for Craft CMS 3. It is a turnkey SEO system that is comprehensive, powerful, and flexible.",
"downloadUrl": "https://plugins.craftcms.com/seomatic",
"headline": "SEOmatic Craft CMS Plugin",
"image": {
"@type": "ImageObject",
"height": "250",
"url": "https://nystudio107.com/img/plugins/seomatic/seomatic-icon.svg",
"width": "250"
},
"inLanguage": "en",
"installUrl": "https://plugins.craftcms.com/seomatic",
"mainEntityOfPage": "https://nystudio107.com/plugins/seomatic",
"name": "SEOmatic",
"offers": {
"@id": "https://nystudio107.com/#plugin-offer"
},
"operatingSystem": "Web Browser",
"publisher": {
"@id": "https://nystudio107.com/#creator"
},
"screenshot": {
"@type": "ImageObject",
"height": "500",
"url": "https://nystudio107.com/img/plugins/seomatic/plugin-logo.png",
"width": "450"
},
"url": "https://nystudio107.com/plugins/seomatic"
}
</script>
Here we are once again filling in a number of properties that make sense for a WebApplication (which is the most correct fit for a plugin for a web-based CMS).
We’ve added in properties like downloadUrl, installUrl, screenshot, and more interestingly offers via the "@id
": "https://nystudio107.com/#plugin-offer" Node Identifier:
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@id": "https://nystudio107.com/#plugin-offer",
"@type": "Offer",
"availability": "http://schema.org/InStock",
"price": "99",
"priceCurrency": "USD",
"priceValidUntil": "2020-05-28T19:03:39-04:00",
"url": "https://plugins.craftcms.com/seomatic"
}
</script>
Since some of our plugins are available for sale, we can indicate that via the Offer schema that lists a price, priceCurrency, availability, etc.
Pro tip: if your product is free, you should indicate that by having all of the above properties, but setting the price to 0.
We implemented this with a Node Identifier so that we can reference it from both the WebApplication and Product JSON-LD:
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "Product",
"brand": {
"@id": "https://nystudio107.com/#identity"
},
"category": "Plugin",
"description": "SEOmatic facilitates modern SEO best practices & implementation for Craft CMS 3. It is a turnkey SEO system that is comprehensive, powerful, and flexible.",
"image": {
"@type": "ImageObject",
"height": "250",
"url": "https://nystudio107.com/img/plugins/seomatic/seomatic-icon.svg",
"width": "250"
},
"isRelatedTo": {
"@type": "Product",
"description": "Craft is a flexible, user-friendly CMS for creating custom digital experiences on the web and beyond.",
"name": "Craft CMS",
"offers": {
"@type": "Offer",
"availability": "http://schema.org/InStock",
"price": "299.00",
"priceCurrency": "USD",
"priceValidUntil": "2020-11-20T09:07:28-05:00",
"url": "https://craftcms.com/pricing"
},
"slogan": "Craft is the CMS that makes the whole team happy.",
"url": "https://craftcms.com/"
},
"logo": {
"@type": "ImageObject",
"height": "250",
"url": "https://nystudio107.com/img/plugins/seomatic/seomatic-icon.svg",
"width": "250"
},
"mainEntityOfPage": "https://nystudio107.com/plugins/seomatic",
"name": "SEOmatic",
"offers": {
"@id": "https://nystudio107.com/#plugin-offer"
},
"productID": "seomatic",
"sku": "seomatic",
"url": "https://nystudio107.com/plugins/seomatic"
}
</script>
We took all of the properties from our WebApplication, and used them to create a new Product JSON-LD schema (SEOmatic strips out any properties that don’t exist in the new schema type).
Then we added a few things specific to a Product such as the isRelatedTo property, relating it to Craft CMS (since our plugins require it).
So why create a new Product schema? Because once again, it’s a type that Google specifically supports.
This is also something that can result in your Product appearing on the Search Engine Results Page (SERP) with a thumbnail screenshot, reviews, ratings, and other features that encourage engagement.
Additional things that could be done:
Google SERP Preview → nystudio107 Plugin Page
Google Rich Results Test → nystudio107 Plugin Page
Google Structured Data Testing Tool link → nystudio107.com Plugin Page
devMode.fm About Page
The devMode.fm About page serves as a good example of how we can implement an FAQPage schema.org type with Questions & Answers that can show up on the Google Search Engine Results Page (SERP). Check out the Google Developers Search FAQ page for details.
{
"@context": "http://schema.org",
"@type": "FAQPage",
"author": {
"@id": "https://devmode.fm/#identity"
},
"copyrightHolder": {
"@id": "https://devmode.fm/#identity"
},
"copyrightYear": "2017",
"creator": {
"@id": "https://devmode.fm/#creator"
},
"dateModified": "2019-08-19T11:19:14-04:00",
"datePublished": "2017-12-11T13:44:33-05:00",
"description": "devMode.fm is a bi-weekly podcast dedicated to the tools, techniques, and technologies used in modern web development. Each episode, we have a cadre of hosts discussing the latest hotness, pet peeves, and technologies we use every day. We all come from a Craft CMS background, but we'll be focusing on other cool frontend development technologies as well.",
"headline": "About the podcast",
"image": {
"@type": "ImageObject",
"url": "https://devmode.fm/assets/site/_1200x630_crop_center-center_82_none/devmode_light-itunes.jpg?mtime=1515536278"
},
"inLanguage": "en-us",
"mainEntity": [
{
"@type": "Question",
"acceptedAnswer": {
"@type": "Answer",
"text": "<p>devMode.fm is a bi-weekly podcast dedicated to the tools, techniques, and technologies used in modern web development. Each episode, we have a cadre of hosts discussing the latest hotness, pet peeves, and technologies we use every day. We all come from a Craft CMS background, but we'll be focusing on other cool frontend development technologies as well.</p>"
},
"name": "What is the devMode.fm podcast all about?"
},
{
"@type": "Question",
"acceptedAnswer": {
"@type": "Answer",
"text": "<p>It sure is! The source code for this website is MIT licensed, and can be found on Github: <a href=\"https://github.com/nystudio107/devmode\" target=\"_blank\" rel=\"noreferrer noopener\">nystudio107/devmode</a>.</p>"
},
"name": "Is the source code to the devMode.fm website available?"
},
{
"@type": "Question",
"acceptedAnswer": {
"@type": "Answer",
"text": "<p>The devMode.fm website is built with:</p><a href=\"https://craftcms.com/\" target=\"_blank\">Craft 3 CMS</a>, <a href=\"https://tailwindcss.com/\" target=\"_blank\">Tailwind CSS</a>, <a href=\"https://vuejs.org/\" target=\"_blank\">Vue.js</a>, <a href=\"https://webpack.js.org/\" target=\"_blank\">webpack</a>, <a href=\"https://www.nginx.com/\" target=\"_blank\">Nginx</a>, <a href=\"https://www.postgresql.org\" target=\"_blank\">PostgreSQL</a>, <a href=\"https://forge.laravel.com\" target=\"_blank\">Forge</a>, <a href=\"https://github.com/nystudio107/craft\" target=\"_blank\">nystudio107/craft</a>"
},
"name": "What technologies were used building the devMode.fm website?"
}
],
"mainEntityOfPage": "https://devmode.fm/about",
"name": "About the podcast",
"publisher": {
"@id": "https://devmode.fm/#creator"
},
"url": "https://devmode.fm/about"
}
Here we have set the Main Entity of Page to FAQPage in the SEOmatic settings in the CP, and then we’re using a bit of Twig to read FAQs from a matrix block that has the fields question, answer, and links:
{#
# Add FAQPage schema for the this page as per:
# https://schema.org/FAQPage
#
# @param faqs the FAQs matrix blocks
# @param showInfo the showInfo Global for the show
#}
{% set faqsArray = [] %}
{% for faq in faqs.all() %}
{% set faqLinks = '' %}
{% for link in faq.links %}
{%- set linkHtml -%}
<a href="{{ link.linkUrl }}" target="_blank">{{ link.linkText }}</a>
{%- endset -%}
{% set faqLinks = faqLinks ~ linkHtml ~ (loop.last ? '' : ', ') %}
{% endfor %}
{% set faqsArray = faqsArray | merge([seomatic.jsonLd.create({
'type': 'Question',
'name': faq.question,
'acceptedAnswer': {
'type': 'Answer',
'text': (faq.answer ~ faqLinks),
},
}, false)]) %}
{% endfor %}
{# Merge in the additional properties to the FAQpage mainEntityOfPage #}
{% set mainEntity = seomatic.jsonLd.get('mainEntityOfPage') %}
{% do mainEntity.setAttributes({
'mainEntity': faqsArray,
}) %}
In this case we set the mainEntity property to an array of Question JSON-LD types, each of which has an Answer JSON-LD type embedded in the acceptedAnswer field.
This is something that can result in your FAQPage Questions & Answers appearing on the Search Engine Results Page (SERP).
Google SERP Preview → devMode.fm About Page
Google Rich Results Test → devMode.fm About Page
Google Structured Data Testing Tool link → devMode.fm About Page
devMode.fm Episode Page
Finally let’s have a look at the JSON-LD structured data for the episode pages on devMode.fm, in this case the webpack inside & out with Sean Larkin episode.
We’re going to skip the identity, creator, and breadcrumbs mentioned earlier. Even though the values will be different for this site, the same properties are filled in.
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "RadioEpisode",
"author": {
"@id": "https://devmode.fm/#identity"
},
"copyrightHolder": {
"@id": "https://devmode.fm/#identity"
},
"copyrightYear": "2019",
"creator": {
"@id": "https://devmode.fm/#creator"
},
"dateModified": "2019-05-27T09:10:22-04:00",
"datePublished": "2019-05-27T09:05:00-04:00",
"description": "In this episode, we talk to webpack core maintainer Sean Larkin about what webpack is, who it's intended for, and where it's going in the future!\n\nWe discuss the serendipitous history of how Sean came to be a webpack core maintainer, and how his job at Microsoft came about as a result of it.\n\nJoin by guest host Jake Dohm, we then go on to discuss a whole lot of gritty technical detail of how webpack works, go through the various terminology, talk about Web Assembly, and what the future holds for webpack 5 and beyond.\n\nSean also drops some truth-bombs about CSS being flawed, and browser makers conspiring to kill off webpack. Tune in for the good stuff!",
"headline": "webpack inside & out with Sean Larkin",
"image": {
"@type": "ImageObject",
"url": "https://devmode.fm/assets/site/_1200x630_crop_center-center_82_none/devmode_light-itunes.jpg"
},
"inLanguage": "en-us",
"mainEntityOfPage": "https://devmode.fm/episodes/webpack-inside-out-with-sean-larkin",
"name": "webpack inside & out with Sean Larkin",
"publisher": {
"@id": "https://devmode.fm/#creator"
},
"url": "https://devmode.fm/episodes/webpack-inside-out-with-sean-larkin"
}
</script>
Once again we have a pretty good starting point for our RadioEpisode schema (there is no Podcast schema yet) that is our mainEntityOfPage. But we can add a bunch more information in this case to describe the series that this podcast belongs to, and more.
Once again we’ll use the SEOmatic Twig API to do so:
{#
# Add audio schema for the this episode as per:
# https://schema.org/RadioEpisode
#
# @param episode the episode Entry for the current page
# @param fileInfo summary of the episode media file info as per:
# https://github.com/nystudio107/craft-transcoder#getting-information-about-a-videoaudio-file
# @param showInfo the showInfo Global for the show
# @param audioUrl the URL to the audio for this episode, wrapped in the
# PodTrac.com tracking redirect
#}
{% from "_partials/macros.twig" import addPersonArray %}
{% from "_partials/macros.twig" import addMentionsArray %}
{# RadioSeries for the episode to belong to #}
{% set showImage = showInfo.showImage.one() %}
{% set radioSeries = seomatic.jsonLd.create({
'type': 'RadioSeries',
'id': '{seomatic.site.identity.genericUrl}#radio-series',
'name': showInfo.showTitle,
'description': showInfo.showDescription,
'url': showInfo.showUrl,
'mainEntityOfPage': siteUrl,
'inLanguage': '{seomatic.meta.language}',
'copyrightHolder': {
'id': '{seomatic.site.identity.genericUrl}#identity',
},
'author': {
'id': '{seomatic.site.identity.genericUrl}#identity',
},
'creator': {
'id': '{seomatic.site.creator.genericUrl}#creator',
},
'image': {
'type': 'ImageObject',
'url': showImage.url,
'width': showImage.width,
'height': showImage.height,
},
}) %}
{# Person array of the episode hosts #}
{% do addPersonArray(radioSeries, 'actor', craft.users.group("hosts").all()) %}
{# Person array of the episode directors #}
{% do addPersonArray(radioSeries, 'director', craft.users.group("owners").all()) %}
{# AudioObject for the episode #}
{% set audio = seomatic.jsonLd.create({
'type': 'AudioObject',
'bitrate': '64k',
'contentSize': fileInfo.size,
'contentUrl': audioUrl,
'duration': fileInfo.duration,
'embedUrl': siteUrl("/player-card/#{episode.slug}"),
'encodingFormat': 'audio/mpeg',
'partOfSeries': {
'id': '{seomatic.site.identity.genericUrl}#radio-series',
},
'productionCompany': {
'id': '{seomatic.site.creator.genericUrl}#creator',
},
'uploadDate': episode.postDate | rss,
}, false) %}
{# Merge in the additional properties to the RadioEpisode mainEntityOfPage #}
{% set mainEntity = seomatic.jsonLd.get('mainEntityOfPage') %}
{% do mainEntity.setAttributes({
'audio': audio,
'episodeNumber': episode.episodeNumber,
'genre': showInfo.showGenre,
'isAccessibleForFree': true,
'productionCompany': {
'id': '{seomatic.site.creator.genericUrl}#creator',
},
}) %}
{# Person array of the episode hosts #}
{% do addPersonArray(mainEntity, 'actor', episode.episodeHosts.all()) %}
{# Person array of the episode directors #}
{% do addPersonArray(mainEntity, 'director', craft.users.group("owners").all()) %}
{# Thing array of the episode mentions #}
{% do addMentionsArray(mainEntity, 'mentions', episode.episodeReferenceLinks) %}
This is getting a whole lot more complex, and we’re even including two macros to help generate the additional JSON-LD structured data:
{# Add a Person array from `users` to the `property` of `jsonLd` #}
{% macro addPersonArray(jsonLd, property, users) %}
{% set usersArray = [] %}
{% for user in users %}
{% set usersArray = usersArray | merge([seomatic.jsonLd.create({
'type': 'Person',
'affiliation': user.profileCompany,
'description': user.profileBio,
'jobTitle': user.profileTitle,
'familyName': user.lastName,
'givenName': user.firstName,
'name': user.fullName,
'sameAs': [
user.profileTwitterUrl,
user.profileGithubUrl,
],
'url': user.profileUrl,
}, false)]) %}
{% endfor %}
{% do jsonLd.setAttributes({
(property): usersArray
}) %}
{% endmacro addPersonArray %}
{# Add a Thing array from `mentions` to the `property` of `jsonLd` #}
{% macro addMentionsArray(jsonLd, property, mentions) %}
{% set mentionsArray = [] %}
{% for mention in mentions %}
{% set mentionsArray = mentionsArray | merge([seomatic.jsonLd.create({
'type': 'Thing',
'name': mention.linkName,
'url': mention.linkUrl,
}, false)]) %}
{% endfor %}
{% do jsonLd.setAttributes({
(property): mentionsArray
}) %}
{% endmacro addMentionsArray %}
I think the easiest way to understand what the code above does is to look at the resulting output, so let’s do just that, and look at our RadioEpisode mainEntityOfPage JSON-LD now:
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "RadioEpisode",
"actor": [
{
"@type": "Person",
"affiliation": "nystudio107",
"description": "Andrew Welch has been in the tech business since he was 15 years old. After a stint running a software company for a couple of decades, he's now immersing himself in doing consulting to help businesses use technology effectively.",
"familyName": "Welch",
"givenName": "Andrew",
"jobTitle": "Demolitions Expert",
"name": "Andrew Welch",
"sameAs": [
"https://twitter.com/nystudio107",
"https://github.com/nystudio107"
],
"url": "https://nystudio107.com"
},
{
"@type": "Person",
"affiliation": "MDD",
"description": "Jonathan is principal/CEO of MDD, a small web agency based in Atlanta, Georgia. He has been developing for the web since 2005 and Craft CMS since 2013. He used to have free time and cool hobbies until his second child arrived on December 6, 2017. He loves his work and his family.",
"familyName": "Melville",
"givenName": "Jonathan",
"jobTitle": "Jonathan Melville",
"name": "Jonathan Melville",
"sameAs": [
"https://twitter.com/jonmelville",
"https://github.com/jonathanmelville"
],
"url": "https://codemdd.io"
}
],
"audio": {
"@context": "http://schema.org",
"@type": "AudioObject",
"bitrate": "64k",
"contentSize": "72969149",
"contentUrl": "https://devmode.fm/transcoder/devmode0041_64kbps_22050_1c.mp3",
"duration": "4560.535510",
"embedUrl": "https://devmode.fm/player-card/webpack-inside-out-with-sean-larkin",
"encodingFormat": "audio/mpeg",
"productionCompany": {
"@id": "https://nystudio107.com/#creator"
},
"uploadDate": "Mon, 27 May 2019 09:05:00 -0400"
},
"author": {
"@id": "https://devmode.fm/#identity"
},
"copyrightHolder": {
"@id": "https://devmode.fm/#identity"
},
"copyrightYear": "2019",
"creator": {
"@id": "https://devmode.fm/#creator"
},
"dateModified": "2019-05-27T09:10:22-04:00",
"datePublished": "2019-05-27T09:05:00-04:00",
"description": "In this episode, we talk to webpack core maintainer Sean Larkin about what webpack is, who it's intended for, and where it's going in the future!\n\nWe discuss the serendipitous history of how Sean came to be a webpack core maintainer, and how his job at Microsoft came about as a result of it.\n\nJoin by guest host Jake Dohm, we then go on to discuss a whole lot of gritty technical detail of how webpack works, go through the various terminology, talk about Web Assembly, and what the future holds for webpack 5 and beyond.\n\nSean also drops some truth-bombs about CSS being flawed, and browser makers conspiring to kill off webpack. Tune in for the good stuff!",
"director": [
{
"@type": "Person",
"affiliation": "nystudio107",
"description": "Andrew Welch has been in the tech business since he was 15 years old. After a stint running a software company for a couple of decades, he's now immersing himself in doing consulting to help businesses use technology effectively.",
"familyName": "Welch",
"givenName": "Andrew",
"jobTitle": "Demolitions Expert",
"name": "Andrew Welch",
"sameAs": [
"https://twitter.com/nystudio107",
"https://github.com/nystudio107"
],
"url": "https://nystudio107.com"
},
{
"@type": "Person",
"affiliation": "Mildly Geeky",
"description": "Patrick Harrington has been obsessed with the web ever since picking up a copy of HTML for Dummies at age 13. After working for larger Boston-area agencies, he started his own company in 2011 and specializes in Craft CMS.",
"familyName": "Harrington",
"givenName": "Patrick",
"jobTitle": "President, Founder",
"name": "Patrick Harrington",
"sameAs": [
"https://twitter.com/p_harrington83",
"https://github.com/mildlygeeky"
],
"url": "https://mildlygeeky.com"
}
],
"episodeNumber": 41,
"genre": "Technology",
"headline": "webpack inside & out with Sean Larkin",
"image": {
"@type": "ImageObject",
"url": "https://devmode.fm/assets/site/_1200x630_crop_center-center_82_none/devmode_light-itunes.jpg"
},
"inLanguage": "en-us",
"isAccessibleForFree": true,
"mainEntityOfPage": "https://devmode.fm/episodes/webpack-inside-out-with-sean-larkin",
"mentions": [
{
"@type": "Thing",
"name": "webpack concepts",
"url": "https://webpack.js.org/concepts/"
},
{
"@type": "Thing",
"name": "webpack in Wikipedie",
"url": "https://en.wikipedia.org/wiki/Webpack"
},
{
"@type": "Thing",
"name": "Webpack 4 Fundamentals videos",
"url": "https://frontendmasters.com/courses/webpack-fundamentals/"
},
{
"@type": "Thing",
"name": "Webpack — The Confusing Parts",
"url": "https://medium.com/@rajaraodv/webpack-the-confusing-parts-58712f8fcad9"
},
{
"@type": "Thing",
"name": "An Annotated webpack 4 Config for Frontend Web Development",
"url": "https://nystudio107.com/blog/an-annotated-webpack-4-config-for-frontend-web-development"
},
{
"@type": "Thing",
"name": "A tale of Webpack 4 and how to finally configure it in the right way.",
"url": "https://hackernoon.com/a-tale-of-webpack-4-and-how-to-finally-configure-it-in-the-right-way-4e94c8e7e5c1"
}
],
"name": "webpack inside & out with Sean Larkin",
"productionCompany": {
"@id": "https://nystudio107.com/#creator"
},
"publisher": {
"@id": "https://devmode.fm/#creator"
},
"url": "https://devmode.fm/episodes/webpack-inside-out-with-sean-larkin"
}
</script>
I’ll skip over some of the added properties, and focus on the more interesting bits.
The addPersonArray() macro is used to add in the directors of the show, and it is also re-used to add in the actors who participate in the episode as well. Adding this type of relationship information really helps Google figure out the connections between people and their works on the web.
Then we’ve also added an AudioObject JSON-LD object in the audio property that represents the episode audio itself, and other various associated meta information about the audio.
Finally, all of the “show links” we display on the webpage are added in the mentions property via the addMentionsArray() macro, so everything we mention on the show is linked to.
Finally, we add a new RadioSeries JSON-LD structured data schema:
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@id": "https://devmode.fm/#radio-series",
"@type": "RadioSeries",
"actor": [
{
"@type": "Person",
"affiliation": "nystudio107",
"description": "Andrew Welch has been in the tech business since he was 15 years old. After a stint running a software company for a couple of decades, he's now immersing himself in doing consulting to help businesses use technology effectively.",
"familyName": "Welch",
"givenName": "Andrew",
"jobTitle": "Demolitions Expert",
"name": "Andrew Welch",
"sameAs": [
"https://twitter.com/nystudio107",
"https://github.com/nystudio107"
],
"url": "https://nystudio107.com"
},
{
"@type": "Person",
"affiliation": "Hypatia Industries",
"description": "Earl Johnston is a freelance web developer operating under the Hypatia Industries banner. He specializes in high fives and bourbon.",
"familyName": "Johnston",
"givenName": "Earl",
"jobTitle": "Professor Doctor",
"name": "Earl Johnston",
"url": "http://www.hypatia-industries.com"
},
{
"@type": "Person",
"affiliation": "A Bright Color",
"description": "Lauren is a developer with a background in design, hailing from Toledo, Ohio. Her focus: make understanding code scalability easier, to help create performant and accessible experiences. Outside of work: playing with her MIDI controller, finding the best shows, and cycling around the streets of Neukölln.",
"familyName": "Dorman",
"givenName": "Lauren",
"jobTitle": "Development",
"name": "Lauren Dorman",
"sameAs": [
"https://twitter.com/laurendorman?lang=en",
"https://github.com/laurendorman"
],
"url": "https://laurendorman.io/about/"
},
{
"@type": "Person",
"affiliation": "MDD",
"description": "Jonathan is principal/CEO of MDD, a small web agency based in Atlanta, Georgia. He has been developing for the web since 2005 and Craft CMS since 2013. He used to have free time and cool hobbies until his second child arrived on December 6, 2017. He loves his work and his family.",
"familyName": "Melville",
"givenName": "Jonathan",
"jobTitle": "Jonathan Melville",
"name": "Jonathan Melville",
"sameAs": [
"https://twitter.com/jonmelville",
"https://github.com/jonathanmelville"
],
"url": "https://codemdd.io"
},
{
"@type": "Person",
"affiliation": "Newlevant",
"description": "Marion has been writing software for over 40 years. She may not have seen it all, but she's seen many things, most of them more than once. Marion is the queen of twig macros, and the second most famous Newlevant.",
"familyName": "Newlevant",
"givenName": "Marion",
"jobTitle": "Marion Newlevant",
"name": "Marion Newlevant",
"sameAs": [
"https://twitter.com/marionnewlevant",
"https://github.com/marionnewlevant/"
],
"url": "http://marion.newlevant.com"
},
{
"@type": "Person",
"affiliation": "Working Concept",
"description": "Matt's a web developer with a design background that's been building websites for the past decade. He loves learning, connecting things, and helping businesses solve multi-faceted, internet-shaped problems.",
"familyName": "Stein",
"givenName": "Matt",
"jobTitle": "President/Designer/Developer",
"name": "Matt Stein",
"sameAs": [
"https://twitter.com/mattrambles",
"https://github.com/mattstein"
],
"url": "https://workingconcept.com"
},
{
"@type": "Person",
"affiliation": "Build For Humans",
"description": "Michael runs a small dev team in Texas. Code is his happy place. He wants to be a teacher when he grows up. When not writing code, he cooks with friends, travels all over the place, sings in the shower, and works out with the circus.",
"familyName": "Rog",
"givenName": "Michael",
"jobTitle": "Michael Rog",
"name": "Michael Rog",
"sameAs": [
"https://twitter.com/michaelrog",
"https://github.com/TopShelfCraft"
],
"url": "https://michaelrog.com"
},
{
"@type": "Person",
"affiliation": "Mildly Geeky",
"description": "Patrick Harrington has been obsessed with the web ever since picking up a copy of HTML for Dummies at age 13. After working for larger Boston-area agencies, he started his own company in 2011 and specializes in Craft CMS.",
"familyName": "Harrington",
"givenName": "Patrick",
"jobTitle": "President, Founder",
"name": "Patrick Harrington",
"sameAs": [
"https://twitter.com/p_harrington83",
"https://github.com/mildlygeeky"
],
"url": "https://mildlygeeky.com"
}
],
"author": {
"@id": "https://devmode.fm/#identity"
},
"copyrightHolder": {
"@id": "https://devmode.fm/#identity"
},
"creator": {
"@id": "https://nystudio107.com/#creator"
},
"description": "devMode.fm is a bi-weekly podcast dedicated to the tools, techniques, and technologies used in modern web development. Each episode, we have a cadre of hosts discussing the latest hotness, pet peeves, and technologies we use every day. We all come from a Craft CMS background, but we'll be focusing on other cool frontend development technologies as well.",
"director": [
{
"@type": "Person",
"affiliation": "nystudio107",
"description": "Andrew Welch has been in the tech business since he was 15 years old. After a stint running a software company for a couple of decades, he's now immersing himself in doing consulting to help businesses use technology effectively.",
"familyName": "Welch",
"givenName": "Andrew",
"jobTitle": "Demolitions Expert",
"name": "Andrew Welch",
"sameAs": [
"https://twitter.com/nystudio107",
"https://github.com/nystudio107"
],
"url": "https://nystudio107.com"
},
{
"@type": "Person",
"affiliation": "Mildly Geeky",
"description": "Patrick Harrington has been obsessed with the web ever since picking up a copy of HTML for Dummies at age 13. After working for larger Boston-area agencies, he started his own company in 2011 and specializes in Craft CMS.",
"familyName": "Harrington",
"givenName": "Patrick",
"jobTitle": "President, Founder",
"name": "Patrick Harrington",
"sameAs": [
"https://twitter.com/p_harrington83",
"https://github.com/mildlygeeky"
],
"url": "https://mildlygeeky.com"
}
],
"image": {
"@type": "ImageObject",
"height": 1800,
"url": "http://devmode.test/assets/site/devmode_light-itunes.jpg",
"width": 1800
},
"inLanguage": "en-us",
"mainEntityOfPage": "http://devmode.test/",
"name": "devMode.fm",
"url": "https://devmode.fm/"
}
</script>
This RadioSeries schema gives the individual episode context in terms of what series it belongs to, and also re-uses the addPersonArray() macro to add all of the people who have ever been on the show as a host as part of the RadioSeries.
Additional things that could be done:
- Telephone numbers could be included for the Organization & Person schemas
- Add information on the guests to the actor property (this data just isn’t in the CMS currently)
- A Sitelinks Searchbox could be added
Google Structured Data Testing Tool link → devMode.fm Episode Page
Fin.
These examples show several real-world uses of JSON-LD structured data, but even here, more could be done!
I think it makes sense to get the low-hanging fruit first, and address the connections & context that might be difficult for Google to determine on its own.
Hopefully these annotated examples of how JSON-LD structured data is actually used in the wild will be helpful to you.
Create your own custom JSON-LD structured data by pragmatically picking the schemas and properties that “best fit” your website, paying a little extra special attention to the schemas that Google consumes.
Enjoy your structured data!
Further Reading
If you want to be notified about new articles, follow nystudio107 on Twitter.
Copyright ©2020 nystudio107. Designed by nystudio107
Top comments (0)