Guide · 13 min read

Structured data for developers: JSON-LD that earns rich results

For developers adding schema markup to SaaS, docs, blog or shop pages. Which types are worth it today, how to render JSON-LD safely, and how to check it.

Updated · By the Serpkin team

In short

  • Structured data makes pages eligible for rich results. It guarantees nothing and isn't a ranking shortcut.
  • For most sites the useful set is Organization and WebSite on the homepage, Article on posts, BreadcrumbList on deep pages and Product with Offer for shops.
  • Google no longer shows HowTo (since 2023) or FAQ (since May 2026) rich results. Don't add them expecting a Google benefit.
  • Use JSON-LD built from the same data you render, include every required property, mark up only visible content and escape < as \u003c.
  • Validate with the Rich Results Test and the Schema Markup Validator. Google says AI Overviews need no special schema.

Structured data is a machine-readable description of a page, usually a JSON-LD <script type="application/ld+json"> using schema.org types, that tells search engines what the page is: an article, a product with a price, an app, an organization. Its main practical payoff is eligibility for rich results in Google. It is not a ranking shortcut, and Google no longer shows several rich results developers still add markup for, including FAQ and HowTo. For most SaaS, docs and blog sites, the useful set is small: Organization and WebSite on the homepage, Article on posts, BreadcrumbList on deep pages, and Product with an Offer if you sell physical goods. Build it from the same data you render, include every required property, and validate it.

What structured data does (and doesn't)

What it does:

  • Makes pages eligible for rich results: product prices and availability, review stars, article images and dates, breadcrumbs, software app details.
  • Removes guesswork: your site name, logo, publish dates and prices are stated explicitly instead of inferred from the layout.
  • Feeds Search Console reports: each supported type gets an enhancement report with errors and warnings per URL.

What it doesn't do:

  • It doesn't guarantee anything. Google's policies say it does not guarantee that your structured data will show up in results, even when it passes the Rich Results Test.
  • It doesn't fix indexing. Markup on a page that isn't indexed, or isn't in the server HTML, does nothing. Fix crawling, canonicals and content first (the Next.js SEO checklist covers the order).
  • It isn't a stated ranking factor. Google's documentation describes it as a way to enable rich results, not a way to rank higher.

Which types are worth adding

Add types that describe what the page really is and that Google (or another consumer you care about) actually uses. Everything else is weight in your HTML.

Schema types by site type and what Google does with them (checked 2026-10-27)
TypeWhereGoogle feature todayRequired for that feature
OrganizationHomepageLogo and details in knowledge panelsNone; name, url, logo recommended
WebSiteHomepage onlySite name shown in resultsname, url
Article / BlogPostingPosts, guides, docs articlesArticle appearance (title, image, date)None; headline, image, datePublished, author recommended
BreadcrumbListAny page below the top levelBreadcrumb trail (desktop only since January 2025)itemListElement with position, name, item
Product + OfferProduct pages (e-commerce)Product snippets and merchant listingsMerchant listing: name, image, offers with price and priceCurrency
SoftwareApplicationApp or SaaS product pageSoftware app rich resultname, offers.price, and aggregateRating or review
FAQPagePages with visible Q&ANone: FAQ rich results stopped showing in May 2026n/a
HowToStep-by-step pagesNone: removed from results in 2023n/a

SaaS

Organization plus WebSite on the homepage is the baseline. SoftwareApplication is tempting, but Google's software app feature requires a rating or review. If you have no real, on-page reviews, skip it rather than inventing an aggregateRating.

Docs and blogs

Article (or BlogPosting, or TechArticle) on each post, with headline, image, datePublished, dateModified and an author with a name and url. Add BreadcrumbList on docs pages that sit several levels deep.

E-commerce

Product with a nested Offer is where structured data pays off most. Merchant listings need name, image and offers with a price and an ISO 4217 priceCurrency, and Google requires a price greater than zero for them.

What Google stopped showing

Google has been trimming the rich result gallery for years. If an older tutorial tells you to add one of these, check the current search gallery first.

Rich results removed or restricted, from Google's Search Central changelog
FeatureWhat changedWhen
HowToNo longer shown on desktop or mobile; docs removedSeptember 2023
FAQRestricted to well-known government and health sitesSeptember 2023
FAQNo longer shown in Google Search at all; docs removedMay to June 2026
Sitelinks search boxNo longer shown; docs removedNovember 2024
BreadcrumbsShown on desktop onlyJanuary 2025
Course info, estimated salary, learning video, special announcement, vehicle listingNo longer shown; docs removedSeptember 2025
Practice problemNo longer shown; docs removedJanuary 2026

Removing these is optional: Google's changelog only says the features are no longer shown. But it is code you maintain for no Google benefit, and if other consumers matter to you (Bing, AI crawlers), make that a deliberate choice rather than a leftover.

JSON-LD vs microdata

Google supports three formats: JSON-LD, microdata and RDFa. It says all three are equally fine when valid, and recommends JSON-LD. For developers JSON-LD is the obvious choice: it is one object built from your data, kept separate from your markup, so a design change can't break it.

Microdata (harder to maintain)
<div itemscope itemtype="https://schema.org/Product">
  <h1 itemprop="name">Trail Runner 2</h1>
  <div itemprop="offers" itemscope itemtype="https://schema.org/Offer">
    <span itemprop="priceCurrency" content="USD">$</span><span itemprop="price" content="129.00">129</span>
  </div>
</div>

Each Google feature lists required and recommended properties. Google's rule: you must include all the required properties for an object to be eligible. Recommended properties improve the result, and Google says fewer, complete and accurate recommended properties beat filling every field with weak data.

Two rules decide most validation results:

  • Requirements are per feature, not per schema.org type. schema.org lets Product have almost anything. Google's merchant listing needs image and offers; a product snippet accepts offers, review or aggregateRating.
  • Only mark up visible content. Google's policies say: don't mark up content that is not visible to readers of the page. The price in your JSON-LD must be the price on the page. Breaking this can lead to a manual action that removes the page's rich result eligibility.

Structured data in Next.js

Next.js recommends rendering JSON-LD as a <script> in layout.tsx or page.tsx. JSON.stringify doesn't escape HTML, so a product name containing </script> could break out of the tag. The Next.js docs suggest replacing < with \u003c. Wrap that in one component:

components/json-ld.tsx
// Server Component: the markup is in the HTML Google receives.
export function JsonLd({ data }: { data: Record<string, unknown> }) {
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{
        __html: JSON.stringify(data).replace(/</g, "\\u003c"),
      }}
    />
  );
}

Homepage: Organization and WebSite

app/page.tsx
import { JsonLd } from "@/components/json-ld";

const site = "https://your-site.com";

export default function Home() {
  return (
    <main>
      <JsonLd
        data={{
          "@context": "https://schema.org",
          "@graph": [
            {
              "@type": "Organization",
              "@id": `${site}/#org`,
              name: "Acme",
              url: site,
              logo: `${site}/logo.png`,
              sameAs: ["https://github.com/acme", "https://x.com/acme"],
            },
            {
              "@type": "WebSite",
              "@id": `${site}/#website`,
              name: "Acme",
              url: site,
              publisher: { "@id": `${site}/#org` },
            },
          ],
        }}
      />
      {/* ... */}
    </main>
  );
}

Posts: Article and BreadcrumbList from the same data

app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
import { JsonLd } from "@/components/json-ld";

const site = "https://your-site.com";

export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await getPost(slug);
  if (!post) notFound();
  const url = `${site}/blog/${post.slug}`;

  return (
    <article>
      <JsonLd
        data={{
          "@context": "https://schema.org",
          "@graph": [
            {
              "@type": "BlogPosting",
              headline: post.title,
              image: [`${site}${post.cover}`],
              datePublished: post.publishedAt, // ISO 8601
              dateModified: post.updatedAt,
              author: { "@type": "Person", name: post.author.name, url: post.author.url },
              mainEntityOfPage: url,
            },
            {
              "@type": "BreadcrumbList",
              itemListElement: [
                { "@type": "ListItem", position: 1, name: "Blog", item: `${site}/blog` },
                { "@type": "ListItem", position: 2, name: post.title, item: url },
              ],
            },
          ],
        }}
      />
      <h1>{post.title}</h1>
      {/* the same title, date and author rendered visibly */}
    </article>
  );
}

A framework-agnostic snippet

Astro, SvelteKit, Remix, a static generator: the pattern is the same. Build an object, serialize it with < escaped, and print it into the server HTML. Here is a product page with an Offer:

lib/jsonld.ts
export function jsonLdScript(data: object): string {
  const json = JSON.stringify(data).replace(/</g, "\\u003c");
  return `<script type="application/ld+json">${json}</script>`;
}

export function productJsonLd(p: {
  name: string; image: string; description: string; sku: string;
  price: number; currency: string; inStock: boolean; url: string;
}) {
  return {
    "@context": "https://schema.org",
    "@type": "Product",
    name: p.name,
    image: [p.image],
    description: p.description,
    sku: p.sku,
    offers: {
      "@type": "Offer",
      url: p.url,
      price: p.price.toFixed(2),
      priceCurrency: p.currency, // ISO 4217, e.g. "USD"
      availability: p.inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
    },
  };
}

In Astro, render it with <script type="application/ld+json" set:html={json} />; in SvelteKit, inside <svelte:head> with {@html jsonLdScript(data)}. In plain HTML, paste the script anywhere in the <head> or <body>.

Validate and monitor it

  1. 1BuildFrom the same data the page renders
  2. 2Schema Markup ValidatorIs it valid schema.org?
  3. 3Rich Results TestIs it eligible for a Google feature?
  4. 4Search ConsoleEnhancement reports after Google recrawls
The structured data loop: build from rendered data, check syntax and Google eligibility before shipping, then watch Search Console reports.
  1. Check the server HTML

    Markup that only appears after client JavaScript runs is easy to miss, and one syntax error hides a whole block. This prints the types in each JSON-LD block the server sends, or the parse error:

    Terminal
    curl -s https://your-site.com/blog/launch | node -e '
    let h = ""; process.stdin.on("data", (d) => (h += d)).on("end", () => {
      for (const [, j] of h.matchAll(/<script type="application\/ld\+json"[^>]*>(.*?)<\/script>/gs)) {
        try {
          const d = JSON.parse(j);
          console.log("ok  ", [].concat(d["@graph"] ?? d).map((x) => x["@type"]).join(", "));
        } catch (e) { console.log("FAIL", e.message); }
      }
    });'
    # ok   BlogPosting, BreadcrumbList
  2. Run the Rich Results Test

    The Rich Results Test shows which Google features the page is eligible for, and which required or recommended properties are missing. It only knows types Google supports.

  3. Run the Schema Markup Validator

    The Schema Markup Validator checks any schema.org type, including ones Google ignores. Use it for syntax and nesting problems.

  4. Watch Search Console

    After deployment, the rich result reports (Products, Breadcrumbs and so on) list errors and warnings per URL. Syntax errors don't show there, because Google couldn't tell which feature the block was for: they go to the Unparsable structured data report, which only appears if Google found some. Use URL Inspection to confirm Google found the markup on a specific page, and Validate fix once every instance is fixed.

Troubleshooting: symptom, cause, fix

What goes wrong after you ship structured data
SymptomLikely causeFix
Rich Results Test passes, no rich result in searchEligibility isn't a promise, or the page isn't indexed yetCheck indexing in URL Inspection. Allow several days for a recrawl, and check the Manual Actions report
Entry in Unparsable structured dataInvalid JSON: trailing comma, stray backslash, a duplicate @contextBuild the object in code and serialize it with JSON.stringify. Never hand-assemble JSON strings
Error: missing field (a required property)Template has no data for it on some pagesFill it from real page data, or don't emit that type on pages that lack it
Warning: missing recommended fieldOptional property absentAdd it if you have accurate data. Warnings don't block eligibility
Markup present locally, absent in URL InspectionRendered only on the client, or on a code path production doesn't takeRender it in the server HTML and check with the script above
Article image not usedImage blocked by robots.txt, relative URL or too smallAbsolute, crawlable image URLs. Google recommends at least 50K pixels (width × height) in 16x9, 4x3 and 1x1
Dates off by hoursdatePublished without a timezoneUse full ISO 8601 with an offset. Without one, Google assumes Googlebot's timezone
Two Organization objects with different namesLayout and page both emit oneOne source per type per page. Link related items on a page with @id
Rich results dropped after a deployA template change removed a required propertyCheck the rich result reports after each release, or fail the build on missing fields

Brief your coding agent

Agents add schema eagerly and from memory, which is how sites end up with HowTo blocks and invented ratings. Give the rules up front:

Prompt
Add JSON-LD structured data to this site.
- Use only these types: Organization + WebSite (homepage), BlogPosting (posts),
  BreadcrumbList (pages two or more levels deep). Nothing else without asking.
- Build each object from the same data the page renders. Every value must be
  visible on the page. Never invent ratings, reviews or prices.
- One JsonLd server component; JSON.stringify with < escaped as \u003c.
- Include every property Google lists as required for that feature; skip a
  recommended one rather than filling it with weak data.
- Dates: full ISO 8601 with timezone. Images: absolute URLs.
- Show me the curl output of the parse check for one page of each type.

Structured data and AI answers

It is easy to find claims that schema markup gets you cited in AI answers. Be careful with them. Google's guidance on AI Overviews and AI Mode says there is no special schema.org structured data you need to add, and no additional requirements beyond normal Search eligibility. Its one structured data note is that markup should match the visible text on the page.

A reasonable position: structured data states facts (prices, dates, authors, your organization's name) unambiguously, which can only help any system that reads it. It does not replace clear text in the HTML, which is what AI systems quote. Our AI Overviews guide covers what does help.

Checklist

  • JSON-LD is rendered on the server and has < escaped as \u003c.
  • Organization and WebSite on the homepage only; Article on posts; BreadcrumbList on deep pages.
  • Every required property for the targeted Google feature is present, from real page data.
  • Every marked-up value is visible on the page (price, rating, author, date).
  • No HowTo markup kept for Google, and no FAQ markup added expecting FAQ rich results.
  • The Rich Results Test and the Schema Markup Validator both pass.

Serpkin checks structured data on every scan against a built-in rule table for Google's common types. It flags markup missing required properties (schema_invalid), valid markup missing recommended properties (schema_incomplete), pages that look like articles or products but carry no matching markup (schema_missing_eligible), and leftover HowTo markup (schema_deprecated). Missions come with a JSON-LD snippet listing the required properties for your agent to fill in, and npx serpkin check fails a preview whose JSON-LD is invalid JSON or newly misses required properties. To test one URL now, use the free SEO checker.

Check a page nowFree: see what a crawler finds on any URL, with a prompt for your agent.

Questions

Is structured data a ranking factor?

Google doesn't describe it as one. Its documentation presents structured data as a way to become eligible for rich results, which can make a listing more useful and clickable. It doesn't guarantee those results appear.

Does Google still show FAQ rich results?

No. Google restricted FAQ rich results to well-known government and health sites in 2023, and its changelog says the feature stopped appearing in Google Search from May 7, 2026. HowTo rich results were removed in 2023.

Is JSON-LD better than microdata?

Google supports JSON-LD, microdata and RDFa equally when they are valid, and recommends JSON-LD. For developers it is easier to maintain because it is one object built from your data, separate from the HTML.

Can I add JSON-LD with JavaScript?

Google says it can read JSON-LD injected by JavaScript, but rendering it in the server HTML is safer: it is seen on the first crawl and by tools and crawlers that don't run JavaScript.

Do I need schema markup to appear in AI Overviews?

No. Google says there is no special schema.org structured data needed for AI Overviews or AI Mode. Clear, crawlable text on an indexable page matters more. Structured data should simply match what the page shows.

Sources

Let your agent do the SEO work.

Serpkin finds the next fix from real search data, hands it to Claude Code, Codex or Cursor, and checks it after you ship.

npx serpkin
Start free