The ticket says "Add schema to the product pages." Simple enough. You search for "JSON-LD schema," and half the results are about JSON Schema, which turns out to be something else entirely. The other half are SEO blogs showing FAQ markup that Google stopped rewarding years ago.

Structured data is one of the most useful parts of technical SEO and one of the most misunderstood. Done well, it tells search engines exactly what a page is about: this is a product, here's its price, this is the company that sells it. Done badly, it's a block of stale, hand-written JSON that quietly contradicts the page it sits on.

In this guide, I'll untangle the terminology, walk through a JSON-LD block line by line, cover the schema types still worth adding in 2026 (and the ones Google has dropped), show how to link entities with @graph, generate the markup safely from your data, and test it automatically.

Disclosure: I work with the team behind SEO Site Checker, whose tools I mention below. Everything else here is tool-agnostic.

The Short Version

  • JSON-LD is a format. Schema.org is the vocabulary you use inside it. Together, they're what people mean by "JSON-LD schema."
  • Google recommends JSON-LD, and it can go in the <head> or the <body>.
  • Only mark up content that's visible on the page. No hidden or invented data.
  • Structured data helps Google understand a page and can enable rich results, but it doesn't guarantee them.
  • Generate it from your data, inject it safely and validate it in CI.

First, Clear Up the Naming Confusion

Three different things get mixed up here, so let's separate them:

  • JSON-LD stands for JSON for Linked Data. It's a W3C standard for expressing linked data in ordinary JSON, using special keywords like @context, @type and @id.
  • Schema.org is a shared vocabulary of types (Product, Organization, Article, Event and hundreds more) and their properties. It's maintained collaboratively and backed by the major search engines.
  • JSON Schema is an unrelated specification for describing and validating the shape of JSON documents, usually API requests and responses. It has nothing to do with SEO.

Then there's structured data markup, the umbrella term for adding machine-readable data to a page. It can be written as JSON-LD, Microdata or RDFa. Microdata and RDFa weave attributes into your HTML, while JSON-LD lives in its own script tag, which is why it's much easier to generate and maintain.

So when an SEO ticket says "JSON-LD schema," it means Schema.org vocabulary written in JSON-LD. You can close the JSON Schema tabs.

Anatomy of a JSON-LD Block

Here's a minimal, valid Organization block:

<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "Organization", "name": "Acme Tools", "url": "https://example.com", "logo": "https://example.com/images/logo.png", "sameAs": [ "https://www.linkedin.com/company/acme-tools", "https://www.instagram.com/acmetools" ], "contactPoint": { "@type": "ContactPoint", "telephone": "+1-555-555-0123", "contactType": "customer service" } } </script> 

Line by line:

  • <script type="application/ld+json"> tells the browser this is data, not code to execute. Nothing renders on the page.
  • @context says which vocabulary the keys come from, which here is Schema.org.
  • @type is the kind of thing you're describing. Types use PascalCase and are case-sensitive, so Organization works and organization doesn't.
  • Properties like name, url and logo describe it. Properties use camelCase.
  • Nested objects have their own @type. Here, contactPoint is a ContactPoint, not just a loose object.
  • Arrays hold multiple values. sameAs lists your official profiles elsewhere, which helps search engines connect them to the same entity.

Use full absolute URLs everywhere, and make sure every value is valid JSON: double quotes only, and no trailing commas.

Where to Put It and How Google Reads It

Google's introduction to structured data recommends JSON-LD and says it can go in either the <head> or the <body>. A page can have several JSON-LD blocks, so a product page might have one for the product and another for breadcrumbs.

Google can also read JSON-LD injected with JavaScript after the page loads. That works, but server-rendering it is more reliable, because it's in the HTML from the first byte and other crawlers that don't run JavaScript can see it too.

The rule that matters most is this one: structured data must describe content that's visible on that page. If the JSON-LD says a product costs $49 and is in stock, the page should say the same. Marking up hidden content, or data that isn't on the page at all, goes against Google's guidelines and can lead to the markup being ignored.

The Schema Types Worth Adding in 2026

You don't need dozens of types. For most sites, these cover the important ground. The full vocabulary lives on Schema.org if you need something more specific.

Organization or LocalBusiness

Every site should describe who's behind it. Use Organization for most businesses, or LocalBusiness (or a more specific subtype, like Dentist or Restaurant) if customers visit a physical location:

{ "@context": "https://schema.org", "@type": "LocalBusiness", "name": "Happy Paws Grooming", "url": "https://happypaws.example", "telephone": "+1-555-555-0188", "address": { "@type": "PostalAddress", "streetAddress": "12 Main Street", "addressLocality": "Westfield", "addressRegion": "NJ", "postalCode": "07090", "addressCountry": "US" }, "openingHoursSpecification": [{ "@type": "OpeningHoursSpecification", "dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], "opens": "09:00", "closes": "17:00" }] } 

WebSite

A small block on your homepage that states your site's name, which helps Google show the right site name in results:

{ "@context": "https://schema.org", "@type": "WebSite", "name": "Acme Tools", "url": "https://example.com" } 

BreadcrumbList

This should mirror the breadcrumbs users actually see on the page:

{ "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "Power Tools", "item": "https://example.com/power-tools" }, { "@type": "ListItem", "position": 2, "name": "Drills", "item": "https://example.com/power-tools/drills" } ] } 

Article or BlogPosting

For editorial content, include the headline, author, dates and a representative image. Keep dateModified honest. Bumping it without real changes doesn't fool anyone.

Product with Offer and AggregateRating

For ecommerce, this is the big one. Price, currency, availability and, if you have them, reviews. Only include ratings from genuine reviews that are visible on the page. Self-written or invented ratings break Google's guidelines.

{ "@context": "https://schema.org", "@type": "Product", "name": "Cordless Drill 18V", "image": ["https://example.com/images/drill-1600.jpg"], "description": "Lightweight 18V cordless drill with two batteries.", "sku": "ACME-DRL-18", "brand": { "@type": "Brand", "name": "Acme Tools" }, "offers": { "@type": "Offer", "url": "https://example.com/products/cordless-drill-18v", "priceCurrency": "USD", "price": "129.00", "availability": "https://schema.org/InStock" }, "aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.6", "reviewCount": "87" } } 

Others, when they genuinely fit

Event, Recipe, JobPosting and VideoObject are all well supported when your content actually is one of those things. Don't force a type onto a page that isn't that type.

What Changed: Rich Results Google No Longer Shows

This is the part most JSON-LD guides haven't caught up with. Google has been steadily trimming which structured data features produce special search results:

  • FAQ rich results have been limited to well-known, authoritative government and health websites since 2023. For almost everyone else, FAQ markup no longer produces the expandable questions in search.
  • HowTo rich results were dropped.
  • In June 2025, as part of simplifying the search results page, Google phased out seven more features: Book Actions, Course Info, Claim Review, Estimated Salary, Learning Video, Special Announcement and Vehicle Listing.
  • In November 2025, Google said Practice Problems markup would be deprecated, and that Dataset markup would only be used in Dataset Search rather than in main search results.

What should you do with markup for deprecated features? Nothing dramatic. Leaving it in place won't trigger errors or hurt your site, and some markup is still useful to other consumers of your data. Just don't add new markup expecting a rich result that no longer exists, and don't promise a client those FAQ dropdowns.

One more expectation to set: Google describes structured data as a way to help it understand your content and enable richer displays. It doesn't describe it as a ranking boost. Good structured data makes a good page easier to understand. It won't rescue a weak one.

Linking Entities With @graph and @id

A single page often describes several connected things: the organization, the website, the web page and the article with its author. Instead of repeating the organization in every block, you can put everything in one @graph and connect the pieces with @id references:

{ "@context": "https://schema.org", "@graph": [ { "@type": "Organization", "@id": "https://example.com/#organization", "name": "Acme Tools", "url": "https://example.com", "logo": "https://example.com/images/logo.png" }, { "@type": "WebSite", "@id": "https://example.com/#website", "name": "Acme Tools", "url": "https://example.com", "publisher": { "@id": "https://example.com/#organization" } }, { "@type": "Person", "@id": "https://example.com/team/jay#person", "name": "Jay Barker", "url": "https://example.com/team/jay" }, { "@type": "BlogPosting", "@id": "https://example.com/blog/choosing-a-drill#article", "headline": "How to Choose a Cordless Drill", "datePublished": "2026-12-01", "dateModified": "2026-12-01", "author": { "@id": "https://example.com/team/jay#person" }, "publisher": { "@id": "https://example.com/#organization" }, "isPartOf": { "@id": "https://example.com/#website" }, "mainEntityOfPage": "https://example.com/blog/choosing-a-drill" } ] } 

An @id is just a unique identifier, conventionally a URL with a hash fragment. It doesn't need to resolve to a page, but it must be consistent. Use the same @id for your organization on every page, and search engines can understand that it's one entity rather than many.

Generate It From Data, Don't Hand-Write It

Hand-written JSON-LD has a fatal flaw: it drifts. Someone changes a price in the CMS, and the structured data still says the old one. The fix is to generate JSON-LD from the same data that renders the visible page.

Here's a small, reusable component for Next.js:

// components/JsonLd.tsx type JsonLdProps = { data: Record<string, unknown> }; export function JsonLd({ data }: JsonLdProps) { // Escape "<" so a value like "</script>" can't break out of the tag const json = JSON.stringify(data).replace(/</g, '\\u003c'); return ( <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: json }} /> ); } 

And using it on a product page in the App Router:

// app/products/[slug]/page.tsx import { JsonLd } from '@/components/JsonLd'; export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const product = await getProduct(slug); const productJsonLd = { '@context': 'https://schema.org', '@type': 'Product', name: product.name, image: product.images.map((img) => img.url), description: product.summary, sku: product.sku, brand: { '@type': 'Brand', name: product.brand }, offers: { '@type': 'Offer', url: `https://example.com/products/${slug}`, priceCurrency: 'USD', price: product.price.toFixed(2), availability: product.inStock ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock', }, }; return ( <> <JsonLd data={productJsonLd} /> {/* the visible product page, rendered from the same `product` data */} </> ); } 

Don't skip the escaping. JSON.stringify produces valid JSON, but it doesn't make that JSON safe to drop inside a <script> tag. If any value comes from users or editors, such as a product name or a review, a string containing </script> can close the tag early and inject arbitrary HTML. That's a cross-site scripting (XSS) hole. Replacing every < with < keeps the JSON identical once it's parsed, while making it impossible to break out of the script tag.

The same pattern works anywhere. In Astro, Nuxt, Rails or a PHP template, build a plain object from your data, serialize it, escape < and render it in a script tag from one shared component. If you're on a CMS, plugins can output schema for you. Just check what they produce, because two plugins each emitting their own Organization block is a surprisingly common mess.

Testing and Validation

There are several layers of checking, and each catches different problems:

  • Google's Rich Results Test shows whether a page is eligible for Google's rich results and flags errors in the properties Google cares about.
  • The Schema Markup Validator at validator.schema.org checks general Schema.org validity, including types Google doesn't use for rich results.
  • Google Search Console shows rich result reports across your site, and the URL Inspection tool confirms what Google actually found on a specific page.

For quick manual checks, it helps to format and inspect the raw JSON before anything else. SEO Site Checker has free developer tools, including a JSON viewer and formatter, that make a minified block readable in seconds. For a broader view, a site-wide audit that checks structured data flags pages where it's missing or broken.

But the real win is automation. Here's a small Node script (Node 18 or newer) that fetches pages, extracts every JSON-LD block, confirms it parses, and checks that each node has a @type and the properties you consider required:

// scripts/check-jsonld.mjs // Usage: node scripts/check-jsonld.mjs https://staging.example.com/ https://staging.example.com/products/sample // Adjust to the properties you require for the features you care about const REQUIRED = { Organization: ['name', 'url'], LocalBusiness: ['name', 'address'], Product: ['name', 'offers'], BlogPosting: ['headline', 'datePublished'], Article: ['headline', 'datePublished'], BreadcrumbList: ['itemListElement'], }; let failed = false; const fail = (url, msg) => { console.error(`FAIL ${url}: ${msg}`); failed = true; }; const SCRIPT_RE = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi; for (const url of process.argv.slice(2)) { const html = await (await fetch(url)).text(); const blocks = [...html.matchAll(SCRIPT_RE)].map((m) => m[1]); if (blocks.length === 0) { fail(url, 'no JSON-LD found'); continue; } for (const raw of blocks) { let data; try { data = JSON.parse(raw); } catch (err) { fail(url, `invalid JSON: ${err.message}`); continue; } const nodes = (Array.isArray(data) ? data : [data]).flatMap((d) => d['@graph'] ?? [d]); for (const node of nodes) { const types = [].concat(node['@type'] ?? []); if (types.length === 0) fail(url, 'a node has no @type'); for (const type of types) { for (const prop of REQUIRED[type] ?? []) { if (node[prop] === undefined) fail(url, `${type} is missing "${prop}"`); } } } } } process.exit(failed ? 1 : 0); 

It's deliberately simple. It won't replace Google's tools, but it will fail your build the moment a template change breaks the JSON or drops a property you rely on, which is exactly when you want to know.

Does Structured Data Help With AI Search?

It's the question I get most often now, so here's the honest answer. Google says there's no special schema you need to add to appear in AI Overviews or AI Mode. Its AI features rely on the same indexing and quality systems as regular search, and the same structured data guidelines apply.

That doesn't make structured data useless for AI. Clear, accurate structured data markup removes ambiguity. It states plainly that this page is about a product called "Cordless Drill 18V," made by this brand, costing this much and in stock. Any system reading your page, whether it's a search engine, an AI assistant, a shopping feed or a voice assistant, benefits from facts that are explicit rather than inferred. Consistent @ids and sameAs links also help connect your business to the same entity across the web.

What it won't do is get your content cited by an AI tool on its own. Be wary of anyone selling a special "AI schema" that promises AI visibility. The fundamentals still decide that: useful content, a trusted reputation and pages that can be crawled.

A Practical Rollout Plan

If you're adding structured data to an existing site, don't try to mark up everything at once. Roll it out in this order:

  1. Sitewide entities first. Add Organization (or LocalBusiness) and WebSite once, ideally in a shared layout, with stable @ids.
  2. Your money templates next. Product pages for ecommerce, service or location pages for local businesses, articles for publishers. One template change covers hundreds of pages.
  3. Breadcrumbs everywhere they're visible.
  4. Add the CI check from the testing section before you move on, so nothing regresses.
  5. Watch Search Console for a few weeks. Fix errors and warnings in the rich result reports, and confirm with URL Inspection that Google sees what you expect.

Then stop, and only add more types when a page genuinely is one of them. More schema isn't better schema.

Common Mistakes

  • Markup that doesn't match the page, like a price, rating or availability that differs from what users see.
  • Fake or self-written reviews in AggregateRating.
  • Re...