Skip to content
APIs

JSON-LD for SEO: How to Add Structured Data to Your Website

·9 min read
JSON-LD for SEO: How to Add Structured Data to Your Website - APIs guide from JSON Prettifier

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 always https://schema.org) and a @type such as Article or Product, 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:

  • @context names the vocabulary. For search engines this is https://schema.org, the shared dictionary of types and properties that Google, Bing, and others agreed on.
  • @type says 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-LDMicrodataRDFa
Where it livesOne <script> block, anywhere in the pageAttributes on each visible HTML elementAttributes on each visible HTML element
Touches page markupNoYes, every tagged elementYes, every tagged element
Easy to generate from a CMS or APIYes, it is plain JSONHard, mixed with templatesHard, mixed with templates
Can describe things not shown on the pageYes (within Google's rules)NoNo
Google's recommendationRecommendedSupportedSupported

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.

{
  "@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:

  1. 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 @context and @type to float to the top for review. This step takes ten seconds and avoids the silent-failure case entirely.
  2. Is it valid schema.org? The Schema Markup Validator checks property names and types against the vocabulary and flags unknown keys.
  3. 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 not Product, and DatePublished is not datePublished. schema.org names are case-sensitive.
  • Comments in the block. JSON has no comment syntax. A // TODO line 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 \u003c there.
  • Relative URLs. item, url, and image should 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.

Frequently Asked Questions

What is JSON-LD in simple terms?
JSON-LD is ordinary JSON with two special keys, @context and @type, that tell machines what the data describes. On a web page it sits in a script tag with type application/ld+json and lets search engines read facts such as the author of an article or the price of a product without parsing the visible HTML.
Where should the JSON-LD script tag go?
Either the head or the body works. Google reads JSON-LD from both locations and also from scripts injected by JavaScript after the page loads. The head is the common convention because it keeps structured data separate from content.
Does JSON-LD directly improve rankings?
Not as a ranking factor on its own. Structured data makes a page eligible for rich results such as star ratings, breadcrumbs, and product prices in search listings, which can raise click-through rate. Google has said structured data is not a general ranking signal.
Can I put multiple JSON-LD blocks on one page?
Yes. You can use several separate script tags or combine everything in one block under an @graph array. Both are valid; @graph is tidier when the items reference each other, for example an Article whose publisher is the Organization on the same page.
How do I test JSON-LD structured data?
Use Google's Rich Results Test to check eligibility for Google features and the Schema Markup Validator at validator.schema.org to check against the schema.org vocabulary. Before either, validate the raw JSON in a formatter, because a single trailing comma makes the whole block invisible to crawlers.
Do FAQ schema rich results still show in Google?
Google reduced the visibility of FAQ rich results in 2023, limiting them mostly to authoritative government and health sites. The markup is still valid and still helps machines understand the page, but do not expect an expanded FAQ snippet for most sites.

Validate Your Structured Data Block

Paste your JSON-LD, click Validate, and fix the syntax before Google silently ignores it. Runs entirely in your browser.

Validate My JSON-LD