Schema markup is a vocabulary (schema.org) combined with a format (JSON-LD, usually) that tells search engines what your content represents. Not just “this is text”, but “this is a recipe for banana bread” or “this is an organization called DevBottle.”
This guide is about the format itself: how the pieces fit together, how to connect entities, and how to generate markup from your data without breaking your pages. If you’re looking for the step-by-step version (which type to use, where to paste it in WordPress or Next.js, how to validate), read How to Add Schema Markup to Your Website.
Generate correct JSON-LD for common schema types.
Why JSON-LD over other formats
There are three ways to add schema markup to HTML: Microdata (inline attributes on existing HTML elements), RDFa (similar), and JSON-LD (a separate <script> block).
Google recommends JSON-LD. The reason is practical: the markup doesn’t touch your HTML structure. It lives in a script tag that can be generated, updated, and validated independently of the page template. There’s no risk of breaking your layout when updating schema, and no coupling between your semantic HTML and your schema definitions.
The basic structure
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "DevBottle",
"url": "https://devbottle.com/"
}
</script>
Every schema block needs @context (almost always https://schema.org) and @type (the schema type you’re using). Everything else depends on the type. Property names are case-sensitive and must match schema.org exactly: datePublished works, datepublished is silently ignored.
Nesting vs. referencing with @id
Entities often relate to each other: an article has an author and a publisher. The simplest way is to nest the related object:
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "JSON-LD Schema Markup: A Practical Guide",
"publisher": {
"@type": "Organization",
"name": "DevBottle",
"logo": "https://devbottle.com/images/devbottle-logo.png"
}
}
Nesting works, but on a site with hundreds of articles you repeat the same organization hundreds of times, and a typo in one copy creates what looks like a different entity. The alternative is to give an entity a stable @id (a URL, often with a fragment) and reference it:
"publisher": { "@id": "https://devbottle.com/#organization" }
The @id doesn’t have to resolve to a page. It’s an identifier that tells parsers “this is the same thing as the node with this ID.”
Combining entities with @graph
@graph lets one script block hold several top-level entities that reference each other by @id. This is how most well-structured sites (and SEO plugins like Yoast) organize their markup:
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://devbottle.com/#organization",
"name": "DevBottle",
"url": "https://devbottle.com/"
},
{
"@type": "WebSite",
"@id": "https://devbottle.com/#website",
"url": "https://devbottle.com/",
"publisher": { "@id": "https://devbottle.com/#organization" }
},
{
"@type": "BlogPosting",
"@id": "https://devbottle.com/blog/json-ld-schema-markup-guide/#article",
"headline": "JSON-LD Schema Markup: A Practical Guide for Developers",
"datePublished": "2025-08-12",
"publisher": { "@id": "https://devbottle.com/#organization" },
"isPartOf": { "@id": "https://devbottle.com/#website" }
}
]
}
Define the site-wide nodes (Organization, WebSite) once in your layout, and add page-specific nodes per page. Keep each @id identical everywhere it’s used, including the trailing slash.
Schema types worth implementing
WebSite. Identifies your site as an entity and gives search engines its canonical name and URL. It can also include a SearchAction describing your site search URL.
Organization. Your company or brand. Include name, url, logo, and sameAs (an array of links to your official profiles elsewhere). sameAs is how you connect your entity to the same organization on other sites.
Article / BlogPosting. For editorial content. The important fields are headline, author, datePublished, dateModified, and image. Update dateModified when you meaningfully revise the content, not on every build.
BreadcrumbList. Describes the page’s position in the site hierarchy, which Google can show in place of the raw URL in search results.
Product, Event, Recipe, LocalBusiness. These are the types with the most visible rich results, and also the most required fields. Generate them from your real data rather than by hand.
FAQPage. Still valid schema.org markup, but since 2023 Google only shows FAQ rich results for well-known government and health websites. On most sites it won’t produce the expandable questions in search results. Only add it when the questions and answers are visible on the page anyway.
Generating JSON-LD from data
Most real markup comes from a CMS or database, not a text editor. Build a plain object and serialize it, rather than concatenating strings:
const schema = {
"@context": "https://schema.org",
"@type": "Product",
name: product.name,
image: product.images.map((image) => image.url),
offers: {
"@type": "Offer",
price: product.price.toFixed(2),
priceCurrency: "USD",
availability: product.inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
},
};
const html = `<script type="application/ld+json">${JSON.stringify(schema).replace(/</g, "\\u003c")}</script>`;
The .replace(/</g, "\\u003c") is important. JSON.stringify doesn’t escape <, so a product name containing </script> would close the script tag and inject whatever follows into your page. Escaping < as \u003c keeps the JSON valid and the script block intact. Framework helpers such as Astro’s set:html or React’s dangerouslySetInnerHTML don’t do this for you.
Omit properties that have no value instead of outputting empty strings or null. An empty "image": "" is a validation error, while a missing optional property is fine.
Validating before deploying
Google’s Rich Results Test shows which Google rich result types were detected and whether any required fields are missing. It only covers types Google uses for rich results.
Schema Markup Validator checks against the full schema.org vocabulary, which catches misspelled properties and wrong value types on any type.
Google Search Console shows structured data reports under Enhancements once your pages are indexed, with errors aggregated across the whole site.
Common issues: missing required fields (each type has its own), dates that aren’t ISO 8601 (2026-09-15, not “September 15, 2026”), relative URLs where absolute ones are expected, and URLs or @id values that don’t match the page’s canonical URL.
Keeping it accurate
Schema markup that misrepresents the page is worse than none. An Article with a stale dateModified misleads search engines about freshness, and markup describing content that isn’t visible on the page goes against Google’s structured data guidelines.
The markup should describe the page as it currently is. Generating it from the same data that renders the page is the most reliable way to keep the two in sync.