JSON-LD Complete Guide

Last updated:

JSON-LD (JavaScript Object Notation for Linked Data) is the recommended format for adding structured data to web pages. Google, Bing, and other search engines prefer it. This guide covers everything you need to implement it correctly.

Getting Started

JSON-LD lives inside a <script> tag in your HTML. You can place it in the <head> or <body>. Google processes both equally.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "JSON-LD Complete Guide"
}
</script>

Three properties appear in nearly every JSON-LD block:

  • @context — Always "https://schema.org". This tells parsers which vocabulary you are using.
  • @type — The schema.org type (e.g., Article, Product, Person).
  • The remaining properties describe the entity.

You do not need to install anything. No libraries. No build tools. Just a script tag with valid JSON inside it.

Basic Properties

Properties accept different value types depending on the schema.org specification.

Strings, Numbers, and Booleans

{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Wireless Headphones",
  "description": "Over-ear wireless headphones with noise cancellation.",
  "sku": "WH-1000XM5",
  "gtin13": "4548736132597",
  "isAccessoryOrSparePartFor": false
}

Dates

Use ISO 8601 format. You can specify a date, a datetime, or a datetime with timezone.

{
  "@context": "https://schema.org",
  "@type": "Event",
  "name": "Web Standards Conference",
  "startDate": "2026-06-15T09:00:00-05:00",
  "endDate": "2026-06-17"
}

URLs

URLs are plain strings. Use fully qualified URLs, not relative paths.

{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": "Acme Corp",
  "url": "https://www.acme.com",
  "logo": "https://www.acme.com/images/logo.png"
}

Nesting Objects

Most real-world structured data involves nested objects. An Article has an author. A Product has an offers. You nest these as regular JSON objects with their own @type.

{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "Understanding Structured Data",
  "datePublished": "2026-03-01",
  "author": {
    "@type": "Person",
    "name": "Jane Smith",
    "url": "https://example.com/authors/jane-smith"
  },
  "publisher": {
    "@type": "Organization",
    "name": "Tech Publications",
    "logo": {
      "@type": "ImageObject",
      "url": "https://example.com/logo.png",
      "width": 200,
      "height": 60
    }
  }
}

You can nest as deeply as needed. Each nested object gets its own @type but does not need a separate @context.

Using @id and @graph for Multiple Entities

The @id Property

@id assigns a unique identifier to an entity. This is typically a URL, but it does not need to resolve to a real page. It serves as an internal reference.

{
  "@context": "https://schema.org",
  "@type": "Person",
  "@id": "https://example.com/#jane-smith",
  "name": "Jane Smith"
}

The @graph Property

@graph lets you define multiple top-level entities in a single JSON-LD block. Each entity is an element in the array.

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "WebPage",
      "@id": "https://example.com/blog/my-post",
      "name": "My Blog Post",
      "author": { "@id": "https://example.com/#jane-smith" }
    },
    {
      "@type": "Person",
      "@id": "https://example.com/#jane-smith",
      "name": "Jane Smith",
      "jobTitle": "Senior Engineer"
    },
    {
      "@type": "Organization",
      "@id": "https://example.com/#org",
      "name": "Example Inc.",
      "url": "https://example.com"
    }
  ]
}

With @graph, you define each entity once and reference it by @id from other entities. This avoids duplication and keeps your data clean.

Arrays

When a property can have multiple values, use a JSON array.

Multiple Authors

{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "Co-authored Research Paper",
  "author": [
    {
      "@type": "Person",
      "name": "Alice Johnson"
    },
    {
      "@type": "Person",
      "name": "Bob Williams"
    }
  ]
}

Multiple Images

{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Running Shoes",
  "image": [
    "https://example.com/photos/shoes-front.jpg",
    "https://example.com/photos/shoes-side.jpg",
    "https://example.com/photos/shoes-back.jpg"
  ]
}

Google recommends providing multiple images in different aspect ratios (16:9, 4:3, 1:1) for articles. Use an array for all of them.

Linking Between Entities with @id

The real power of @id is cross-referencing. Define an entity once, then point to it from anywhere else in the graph.

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Article",
      "@id": "https://example.com/blog/post-1#article",
      "headline": "First Post",
      "author": { "@id": "https://example.com/#jane" },
      "publisher": { "@id": "https://example.com/#org" }
    },
    {
      "@type": "Article",
      "@id": "https://example.com/blog/post-2#article",
      "headline": "Second Post",
      "author": { "@id": "https://example.com/#jane" },
      "publisher": { "@id": "https://example.com/#org" }
    },
    {
      "@type": "Person",
      "@id": "https://example.com/#jane",
      "name": "Jane Smith"
    },
    {
      "@type": "Organization",
      "@id": "https://example.com/#org",
      "name": "Example Inc."
    }
  ]
}

Both articles reference the same author and publisher. Parsers resolve the @id references and build the full entity graph.

Dynamic JSON-LD

On most real sites, you generate JSON-LD from your CMS or database. The key is to output valid JSON with properly escaped strings.

Template Engine Example

In a server-side template (Jinja, EJS, Handlebars, etc.), you might do something like this:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "{{ article.title | escape_json }}",
  "datePublished": "{{ article.published_date | iso8601 }}",
  "author": {
    "@type": "Person",
    "name": "{{ article.author.name | escape_json }}"
  }
}
</script>

Watch for these pitfalls when generating JSON-LD dynamically:

  • Unescaped quotes in titles or descriptions will break the JSON.
  • Null values — omit the property entirely rather than setting it to null.
  • HTML in strings — strip HTML tags from any CMS fields before inserting them.

JavaScript-Based Injection

For single-page apps, you can inject JSON-LD with JavaScript:

// Build the structured data object
const structuredData = {
  "@context": "https://schema.org",
  "@type": "Product",
  "name": productName,
  "offers": {
    "@type": "Offer",
    "price": productPrice,
    "priceCurrency": "USD"
  }
};

// Inject into the page
const script = document.createElement("script");
script.type = "application/ld+json";
script.textContent = JSON.stringify(structuredData);
document.head.appendChild(script);

Google renders JavaScript, so dynamically injected JSON-LD works. However, static JSON-LD in the HTML source is faster to parse and more reliable.

Multiple JSON-LD Blocks vs @graph

You have two options for placing multiple entities on a page.

Option A: Multiple script tags

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "My Article"
}
</script>

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [...]
}
</script>

Option B: Single @graph

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@graph": [
    { "@type": "Article", "headline": "My Article" },
    { "@type": "BreadcrumbList", "itemListElement": [...] }
  ]
}
</script>

Both are valid. Use @graph when entities reference each other via @id. Use separate blocks when entities are independent. Google processes both the same way.

Common Patterns

Article + BreadcrumbList

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Article",
      "headline": "Understanding CSS Grid",
      "datePublished": "2026-02-15",
      "author": { "@type": "Person", "name": "Alex Chen" },
      "publisher": {
        "@type": "Organization",
        "name": "Web Dev Weekly"
      }
    },
    {
      "@type": "BreadcrumbList",
      "itemListElement": [
        { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://example.com" },
        { "@type": "ListItem", "position": 2, "name": "CSS", "item": "https://example.com/css" },
        { "@type": "ListItem", "position": 3, "name": "CSS Grid" }
      ]
    }
  ]
}

Product + Offer + AggregateRating

{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Ergonomic Desk Chair",
  "description": "Adjustable office chair with lumbar support.",
  "brand": { "@type": "Brand", "name": "ErgoSit" },
  "offers": {
    "@type": "Offer",
    "price": 499.99,
    "priceCurrency": "USD",
    "availability": "https://schema.org/InStock",
    "url": "https://example.com/chairs/ergo-desk"
  },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": 4.6,
    "reviewCount": 238
  }
}

Validation and Debugging

Always test your JSON-LD before deploying. Use these tools:

  1. Google Rich Results Testsearch.google.com/test/rich-results. Paste a URL or code snippet. It shows which rich result types are detected and reports errors.

  2. Schema Markup Validatorvalidator.schema.org. Validates against the full schema.org vocabulary, not just Google’s subset.

  3. Browser DevTools — Open the Elements panel, search for application/ld+json, and inspect the raw JSON. Paste it into a JSON formatter to check for syntax errors.

Common mistakes to watch for:

  • Missing commas between properties.
  • Trailing commas after the last property (invalid in JSON).
  • Using single quotes instead of double quotes.
  • Forgetting @context at the top level.
  • Using relative URLs where absolute URLs are required.
  • Omitting @type on nested objects.

Once your structured data is live, monitor it in Google Search Console under the Enhancements section. Google reports errors, warnings, and valid items for each structured data type it detects on your site.