How to implement structured data with JSON-LD
Chances are, you’ve already heard of JSON-LD — you might even be using it already. Kudos to you if that’s the case. If you haven’t heard of it, I’d urgently recommend you to read up on what is becoming an ever-more important part of SEO. Its increasing importance is down to its selection as Google’s structured data approach of choice, as it’s more flexible than microdata and more easily maintainable in the long run.
What do I mean by more flexible? Put simply, JSON-LD lets you place all your structured data between a special pair of <script> tags in the <head> of the HTML, much as you would with Google Analytics tracking snippets or canonical tags. No more bugging your developers to make simple changes. Compared to the old ways of adding structured data, that’s a big improvement.
<head>
...
<script type="application/ld+json">
// your JSON-LD goes in here...
</script>
...
</head>
The flip side of that flexibility, though, is that JSON-LD is still a relatively new concept to most users, and it’s easy to get the structure wrong. It’s easy to believe that you can read JSON-LD, but not truly understand the structure.
The good news is it isn’t that hard to get right. By the end of this post, you’ll understand enough to understand any JSON-LD you come across.
Plain old JSON
The “JSON” in JSON-LD stands for “JavaScript Object Notation”. Although it owes its name to Javascript, it came into its own as a flexible format for passing data around between all kinds of applications. When you call an API — the Google Analytics API, for example — the results often come back in the JSON format. And you might also have heard of mongoDB — a so-called NoSQL database used by some of the biggest companies on the web to organise huge amounts of data. JSON is all over the web, but it’s usually behind the scenes. To learn it by example, let’s make some JSON to describe a person:
{
"@type" : "Person",
"first name" : "Mark",
"last name" : "Zuckerberg",
"age" : 32
}
Remember, this is just JSON — not JSON-LD — so don’t add it to any web pages! We haven’t gone very far with our model of a person, but that doesn’t matter. We used curly braces to surround the object, and within it, we’ve defined three properties. In each case, we put the name of the property and its value, separated by a colon. Wherever we used non-numeric characters, we enclosed the name or value with quotation marks. So “age”: 32, for example, shows that this person is 32.
But 32 what? Days, months, or years? Of course, in this case, we know it to be years, but a computer wouldn’t necessarily know that. So let’s expand our model slightly:
{
"@type" : "Person",
"first name" : "Mark",
"last name" : "Zuckerberg",
"age" :{
"value" : 32,
"units" : "years"
}
}
Instead of just using a value and hoping a computer can guess what we mean, we’ve gone further and used another JSON object to represent this person’s age. We can now say his age is 32 years. Probably the most common schema mistake I see on e-commerce sites is defining prices incorrectly, because money, like time, can be measured in different units. We’ll get to a practical example in just a moment.
Before we move on, though, let’s create another person to sit alongside the one we already have:
{
"@type" : "Person",
"first name" : "Mark",
"last name" : "Zuckerberg",
"age" : {
"value" : 32,
"units" : "years"
},
"children" : []
}
{
"@type" : "Person",
"first name" : "Maxima",
"last name" : "Zuckerberg",
"age" : {
"value" : 0,
"units" : "years"
}
}
This is perfectly valid JSON. But what we don’t have is any sense of how the two people relate to one another. As far as a computer is concerned, they are independent. We could think of many ways to show the relationship, but I’ve created a ‘children’ property for our first person and put an empty list as its value. In JSON we can use square brackets to store a list. We can put nearly anything in a list, including JSON objects. The list can be empty — as it is right now — or it could have only one item in it, or it could have very many.
{
"@type" : "Person",
"first name" : "Mark",
"last name" : "Zuckerberg",
"age" : {
"value" : 32,
"units" : "years"
},
"children" : [{
"@type" : "Person",
"first name" : "Maxima",
"last name" : "Zuckerberg",
"age" : {
"value" : 0,
"units" : "years"
}
}]
}
Now our list of children has one person object in it. We could add more the usual way: by putting a comma between each of them.
Making the jump from JSON to JSON-LD
LD stands for Linked Data. For SEO we care about using JSON objects to represent data linked to the page. The kinds of objects we want to create are all those laid out in Schema. Think of Schema as a vocabulary that major search engines have agreed upon. And JSON-LD is the grammar — the way we structure the object and add it to the page. When we use Schema, search engines know how to interpret our JSON objects.
Schema.org
In the example above, I just made up the rules as I went along. First, the people we created had names; then they had separate properties for first and last names. Then we changed the age property from a single number to a JSON object with a value and a unit. Then we added an optional list of children. I was defining my own schema as I went along. Of course, left to their own devices, everybody would do this in a slightly different way. Enter Schema.org, which is more or less a rulebook on how we should structure certain objects on our own websites. By sticking with the Schema.org rulebook, we can be consistent with all the other sites out there; Google will then be able to understand what we mean.
Building a Product
One of the most common uses for JSON-LD is defining a product. It’s also where people tend to make the most mistakes. Getting price data right is especially important if you’re looking for products to show up in product feeds.
We’ll now make some JSON-LD to represent a product. Start with empty braces:
{
}
Add a property called @context. Its value will be the schema.org website. This property says: “this object follows the rules laid out at http://schema.org”:
{
@context: "http://schema.org"
}
Schema.org defines various types. A type tells us how to structure an object: which properties we should use and what values we’re allowed to put in them. There are loads of examples on schema.org, representing all kinds of things, like a Person, an Event, and even a Taxi Reservation.
The type we’re going to use is called “Product“. Add that as the value of our @type property:
{
@context : "http://schema.org",
@type : "Product"
}
Pricing
This is where it often seems to go wrong. Once you’ve created a product, it’s natural to want to add a price to it straightaway — after all, the price is one of the most important facts about a product. But schema.org doesn’t allow you to add prices directly to products. There’s an important middle step: creating offers.
To see why this is necessary, take a look at the chart below (taken from Amazon price tracker camelcamelcamel). It shows the price of a notebook on Amazon over time. But at any given point, the notebook — the same product — is on offer from more than one seller. Amazon sells it directly at one price, and third-party sellers at a range of other prices.
So we have to add offers to our product. Each offer can have one or more prices. Prices have a currency and a value. It’s easiest to begin with the price and work your way up. Here’s how we’d represent the “Sold by Amazon” price:
{
"@type" : "PriceSpecification",
"currency" : "GBP",
"value" : 9.88
}
That’s simple enough. We then need to wrap it up in a slightly larger object that will represent the offer:
{
"@type" : "Offer",
"description" : "Sold By Amazon",
"price Specification" :
{
"@type" : "PriceSpecification",
"currency" : "GBP",
"value" : 9.88
}
}
Here we put the price into a property called PriceSpecification and put that into an Offer, which is the entire object you see above. The offer has a description, where we identify it as “Sold by Amazon”. Having packaged all that up, we need to put it in a property called “Offers”, on the product:
Your JSON-LD should now look like this:
{
"@context" : "http://schema.org",
"@type": "Product",
"name": "Moleskine Notebook",
"offers" : [
{
"@type" : "Offer",
"description": "Sold By Amazon",
"priceSpecification" :
{
"@type" : "PriceSpecification",
"currency": "GBP",
"value": 9.88
}
}]
}
That might seem like a lot of hassle just to add a price, but the advantage of it is that it’s very flexible. If your company starts selling different variants of the same product at different price points, adding refurbished or open-box products, or even starts allowing marketplace sellers as Amazon does, it’ll be easy to add new price information. Just make a new Offer object and add it to the list. In our example — the Moleskine Notebook on Amazon — we can easily add a marketplace offer at £5.07:
{
"@context" : "http://schema.org",
"@type": "Product",
"name": "Moleskine Notebook",
"offers" : [
{
"@type" : "Offer",
"description": "Sold By Amazon",
"priceSpecification" :
{
"@type" : "PriceSpecification",
"currency": "GBP",
"value": 9.88
}
},
{
"@type" : "Offer",
"description": "3rd Party Marketplace Seller",
"priceSpecification" :
{
"@type" : "PriceSpecification",
"currency": "GBP",
"value": 5.07
}
}
]
}
Multiple Objects
Another common mistake is more subtle, because there are no mistakes in the syntax, so it won’t trigger any errors or warnings in the Structured Data Testing Tool. Let’s say we have two videos on a page, and we’ve marked them both up with JSON-LD like this:
{
"@context": "http://schema.org/",
"@id": "https://mydomain.com/boring-5-second-video",
"@type": "VideoObject",
"duration": "PT5S",
"name": "Boring video",
"thumbnailUrl": "https://mydomain.com/boring-video-thumbnail.jpg",
"description": "A short and boring video",
"uploadDate": "20160727"
}
{
"@context": "http://schema.org/",
"@id": "https://mydomain.com/interesting-30-minute-video",
"@type": "VideoObject",
"duration": "PT30M",
"name": "Interesting video",
"thumbnailUrl": "https://mydomain.com/long-video-thumbnail.jpg",
"description": "An interesting video with a cool thumbnail image",
"uploadDate": "20160727"
}
This is more common than you might think, especially when we’re relying on some kind of automation to generate the JSON-LD (and we usually are). Put the above snippet into the Structured Data Testing Tool and you’ll find it just silently ignores the second video altogether. It’s a bit like the “Person” object we invented earlier — a computer just doesn’t have any way of understanding the relationships unless you tell it what they are. Given a bunch of objects that look the same, Google doesn’t know which one is the most important. So they just take the first one. In this example, that would mean the wrong thumbnail showing up in video SERPs.
Next steps
There’s no substitute for actually understanding the structure of your JSON-LD. Once you’ve grasped these basic concepts, you can go a lot further than we have here. Thinking back to the Moleskine Notebook, for example, we could have added lots of relevant information:
- Sales Taxes on each offer
- Delivery methods for each offer
- Delivery costs for each offer
- Descriptions of the product condition (new, used, etc.)
- Reviews
Even though it’s been available for quite some time now, very few sites take advantage of all JSON has to offer. And when they do, they often tack new features onto their existing objects without really understanding the syntax. Surf the net and you’ll see reviews that aren’t attached to any products; prices incorrectly configured, and objects of all kinds sitting next to one another instead of in a hierarchy (like the video thumbnails we looked at a moment ago).
This stuff isn’t that hard — it’s almost unfair that there’s still a competitive advantage in understanding it and getting it right. Check out the docs on schema.org, and test your pages regularly. But don’t stop there. It isn’t enough to see “0 Errors” in the testing tool — make sure that your JSON-LD fully reflects the whole structure: objects, reviews, prices, and how they relate to one another.