Quick answer: JSON-LD is structured data written as plain JSON inside a
<script type="application/ld+json">tag. Each object declares an@context(almost alwayshttps://schema.org) and a@typesuch asArticleorProduct, followed by that type's properties. Google recommends it over microdata because it is separate from your HTML, easy to generate, and easy to validate.
A competitor's product page shows a star rating and a price right in the search results. Yours shows a blue link and two lines of text. The difference is usually a dozen lines of JSON that tell Google what the page is about. This guide gives you those lines for the five types most sites need, plus the checks that keep them from silently failing.
Explore a valid sample in the JSON Viewer to check its fields and nesting. The viewer does not perform the protocol-specific validation discussed in this guide.
What JSON-LD Is (and What the @ Keys Mean)
JSON-LD stands for JSON for Linked Data. It is a W3C standard that adds a handful of reserved keys to ordinary JSON so that the data carries its own meaning. For SEO you only need two of them:
@contextnames the vocabulary. For search engines this ishttps://schema.org, the shared dictionary of types and properties that Google, Bing, and others agreed on.@typesays which entry in that dictionary the object describes:Organization,Article,Product, and so on.
Everything else is a normal key/value pair whose names come from schema.org. The smallest useful example:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Harbour Coffee Roasters",
"url": "https://www.harbourcoffee.example",
"logo": "https://www.harbourcoffee.example/logo.png",
"sameAs": [
"https://www.instagram.com/harbourcoffee",
"https://www.linkedin.com/company/harbourcoffee"
]
}
</script>
Because it is JSON, every rule from the JSON spec applies: double quotes only, no trailing commas, no comments. That matters more than usual here, because a crawler that hits a syntax error does not warn you; it just ignores the block.
Why Google Recommends JSON-LD Over Microdata and RDFa
Google supports three formats for structured data. Microdata and RDFa attach attributes to the HTML elements that display the content (itemprop="price" on the price span, and so on). JSON-LD puts the whole description in one block that does not touch your markup.
| JSON-LD | Microdata | RDFa | |
|---|---|---|---|
| Where it lives | One <script> block, anywhere in the page | Attributes on each visible HTML element | Attributes on each visible HTML element |
| Touches page markup | No | Yes, every tagged element | Yes, every tagged element |
| Easy to generate from a CMS or API | Yes, it is plain JSON | Hard, mixed with templates | Hard, mixed with templates |
| Can describe things not shown on the page | Yes (within Google's rules) | No | No |
| Google's recommendation | Recommended | Supported | Supported |
The practical win is maintainability. A CMS can build the JSON-LD object from the same database row that renders the page, serialize it with one call, and drop it in a template. Redesigning the page does not break the markup, and a developer can review it in a formatter rather than hunting through attributes.
JSON-LD Examples for the Five Most Common Types
Organization (site-wide identity)
Put this on the home page, or on every page if your template makes that easier. It feeds the knowledge panel and gives other blocks something to reference as the publisher. The example above is a complete, valid Organization block; add contactPoint or address if those details are public.
Article (blog posts and news)
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "How We Cut Checkout Latency by Half",
"description": "A write-up of the caching changes behind our faster checkout.",
"image": "https://www.example.com/images/checkout-latency.png",
"datePublished": "2026-07-01T09:00:00+00:00",
"dateModified": "2026-07-05T14:30:00+00:00",
"author": {
"@type": "Person",
"name": "Priya Nair",
"url": "https://www.example.com/authors/priya-nair"
},
"publisher": {
"@type": "Organization",
"name": "Example Engineering",
"logo": {
"@type": "ImageObject",
"url": "https://www.example.com/logo.png"
}
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://www.example.com/blog/checkout-latency"
}
}
Dates must be ISO 8601 with a timezone offset; "07/01/2026" is rejected. If you are unsure which string to produce from your language's date type, the JSON date format guide has the exact calls. Use NewsArticle, BlogPosting, or TechArticle instead of plain Article when they fit; they inherit every Article property.
FAQPage (question-and-answer sections)
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Do you ship internationally?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. We ship to most countries; delivery takes 5 to 12 business days."
}
},
{
"@type": "Question",
"name": "Can I return opened coffee?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Unopened bags can be returned within 30 days. Opened bags cannot."
}
}
]
}
Every question and answer in the block must appear on the page in visible text. Google reduced FAQ rich results for most sites in 2023, so treat this as a machine-readability aid rather than a snippet guarantee.
BreadcrumbList (navigation path)
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "https://www.example.com/" },
{ "@type": "ListItem", "position": 2, "name": "Coffee", "item": "https://www.example.com/coffee/" },
{ "@type": "ListItem", "position": 3, "name": "Ethiopia Yirgacheffe" }
]
}
Positions start at 1 and must be numbers, not strings. The last item can omit item because it is the current page.
Product (ecommerce listings)
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Ethiopia Yirgacheffe, 250g",
"image": ["https://www.example.com/images/yirgacheffe-250.jpg"],
"description": "Washed single-origin coffee with notes of bergamot and peach.",
"sku": "ETH-YRG-250",
"brand": { "@type": "Brand", "name": "Harbour Coffee Roasters" },
"offers": {
"@type": "Offer",
"url": "https://www.example.com/coffee/ethiopia-yirgacheffe",
"priceCurrency": "AUD",
"price": "18.50",
"availability": "https://schema.org/InStock",
"itemCondition": "https://schema.org/NewCondition"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.7",
"reviewCount": "128"
}
}
availability takes a schema.org URL, not the word InStock. The price in the markup must match the price shown on the page; mismatches are one of the most common reasons for a manual action on structured data.
Where to Put the Script Tag
Inside <head> or <body>; Google reads both. The type="application/ld+json" attribute is what matters: browsers see an unknown script type and skip execution, so the JSON is never run as code. This is the same mechanism this very page uses; view the source and you will find a TechArticle block and a FAQPage block near the top. If you are curious what the media type means, see the JSON Content-Type guide.
Injecting the tag with JavaScript also works, since Google renders pages. It is less reliable with other crawlers and adds a rendering delay, so emit the block in server HTML when you can.
Multiple Blocks vs @graph
A page often needs more than one type: an Article plus a BreadcrumbList plus the Organization. Two valid layouts:
Separate script tags. One block per type. Simple, and each block can be generated by a different component.
One block with @graph. An array of nodes that can reference each other by @id:
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://www.example.com/#org",
"name": "Example Engineering",
"logo": { "@type": "ImageObject", "url": "https://www.example.com/logo.png" }
},
{
"@type": "Article",
"headline": "How We Cut Checkout Latency by Half",
"datePublished": "2026-07-01T09:00:00+00:00",
"author": { "@type": "Person", "name": "Priya Nair" },
"publisher": { "@id": "https://www.example.com/#org" }
},
{
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Blog", "item": "https://www.example.com/blog/" },
{ "@type": "ListItem", "position": 2, "name": "Checkout Latency" }
]
}
]
}
The publisher line references the Organization instead of repeating it. Use @graph when nodes share data; use separate tags when they do not. Google treats both the same.
Validating JSON-LD Before You Publish
Three checks, in this order, catch nearly every problem:
- Is it valid JSON? Paste the block (without the surrounding script tag) into JSON Prettifier. A trailing comma or a stray quote shows up with a line and column. Click Sort Keys if you want
@contextand@typeto float to the top for review. This step takes ten seconds and avoids the silent-failure case entirely. - Is it valid schema.org? The Schema Markup Validator checks property names and types against the vocabulary and flags unknown keys.
- Is it eligible for Google features? The Rich Results Test applies Google's stricter rules, such as required fields for Product and image size requirements, and shows which rich result the page qualifies for.
After publishing, Google Search Console's Enhancements reports show errors across the whole site, which is where you catch the template bug that only affects one category of pages.
Common JSON-LD Mistakes
- Trailing commas from CMS fields. Editors paste JSON into a plugin textbox, delete the last property, and leave a comma. The block is now invalid and Google ignores it. Validate on save; see fixing trailing commas in JSON.
- Markup that does not match the page. A 4.9 rating in the JSON and no reviews on the page violates Google's guidelines and can earn a manual action. Describe what is visible.
- Wrong case on types and properties.
"@type": "product"is notProduct, andDatePublishedis notdatePublished. schema.org names are case-sensitive. - Comments in the block. JSON has no comment syntax. A
// TODOline breaks the whole thing; comments in JSON explains the alternatives. - Unescaped characters in strings. A product description with a literal double quote or a line break will not parse. Let your serializer escape it, and if you hand-write, check the escape sequence table. One extra rule inside an HTML script tag: the sequence
</script>inside a string ends the tag early, so serializers escape<as\u003cthere. - Relative URLs.
item,url, andimageshould be absolute, including the scheme. - One block for the whole site. A Product block on the home page describes nothing the home page shows. Each page gets the markup for its own content.
Generating JSON-LD From Code
Never build the block with string concatenation. Build an object and serialize it so quotes, newlines, and non-ASCII characters are handled for you. In a Next.js page or any React component:
const productLd = {
"@context": "https://schema.org",
"@type": "Product",
name: product.name,
description: product.description,
offers: {
"@type": "Offer",
priceCurrency: "AUD",
price: product.price.toFixed(2),
availability: product.inStock
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock",
},
};
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(productLd) }}
/>
The same pattern works in any templating language: construct the data, call the JSON serializer, and output the result raw (not HTML-escaped) inside the script tag. If a block ever fails to appear in the Rich Results Test, copy the rendered page source into the formatter and read the error; it is almost always a serialization step that was skipped or run twice.