SEO
Structured Data (JSON-LD): Practical Patterns for Agencies
What application/ld+json actually is, which schema types still earn rich results, and how to emit Article, BreadcrumbList, Product and Organization from one source of truth without duplication.

Structured data tells search engines what a page is about in a form they don't have to guess at. Done consistently it can unlock rich results; done ad hoc it produces validation errors nobody notices for months.
What application/ld+json actually is
JSON-LD stands for JSON for Linked Data. application/ld+json is its MIME type, and it is the value you put in the type attribute of a script tag:
<script type="application/ld+json">
{ "@context": "https://schema.org", "@type": "Article", "headline": "..." }
</script>The browser does not execute it. Because the type is not text/javascript, the script is inert — it is a data block that happens to live in your HTML. Crawlers parse it; users never see it.
Two keys do the heavy lifting. @context says which vocabulary the terms come from, effectively always https://schema.org. @type says what the thing is. Everything else is properties of that type.
JSON-LD is Google's recommended format precisely because it sits apart from your markup. The older alternatives — microdata and RDFa — interleave attributes into your HTML, so any template change risks breaking your schema silently.
Where to put it
Emit it server-side so it is in the initial HTML. Client-only rendering works for Google in many cases but not reliably for other crawlers, and it makes debugging harder for no benefit.
Head or body both work; head is conventional. One script per entity, or one script containing an array — both are valid. In Next.js, render it from a server component:
export function JsonLd({ data }: { data: Record<string, unknown> }) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(data).replace(/</g, "\\u003c"),
}}
/>
);
}That replace is not decoration. Without it, a < inside any string value — a title containing <script>, a description with an HTML snippet — closes your script tag early and injects markup into the page. It is the one genuine security footgun in structured data.
Article, for posts
Include headline, datePublished, dateModified, author, image and publisher. Pull every value from your CMS or frontmatter so nothing is hand-written per post.
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Your Post Title",
"datePublished": "2026-02-01",
"dateModified": "2026-08-14",
"author": { "@type": "Person", "name": "Yasir Haleem" },
"publisher": {
"@type": "Organization",
"name": "Your Site",
"logo": { "@type": "ImageObject", "url": "https://yoursite.com/logo.png" }
},
"image": "https://yoursite.com/post-image.jpg"
}dateModified should reflect a real edit. Bumping it on every build to look fresh is a pattern Google learned to discount years ago, and it costs you the signal when you genuinely do update something.
BreadcrumbList, the one most sites skip
Breadcrumbs are the most reliably rewarded schema type left — Google uses them to replace the raw URL in the result with a readable path. They are also trivial to generate, because you already know the page's ancestry from its route.
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "https://yoursite.com/" },
{ "@type": "ListItem", "position": 2, "name": "Insights", "item": "https://yoursite.com/blog" },
{ "@type": "ListItem", "position": 3, "name": "Your Post Title", "item": "https://yoursite.com/blog/your-post" }
]
}Three rules that account for most breadcrumb errors: position starts at 1 and must not skip; every item must be an absolute URL; and the trail must match what a user would actually see on the page. Google checks the last one.
Product and LocalBusiness
Product wants name, image, description and offers with price, priceCurrency and availability. LocalBusiness wants address, telephone and openingHours.
The rule for both is the same and it is a discipline rather than a technique: only emit properties you can keep true. A price that drifts out of date or opening hours left over from last year are worse than omitting them — inaccurate structured data gets your rich results pulled, and manual actions for it do exist.
One source of truth, referenced by @id
The most common failure at agency scale is the same entity described twice with conflicting data — an Organization in the site layout with one logo, another in the article schema with a different one.
Fix it by declaring the entity once and referencing it:
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://yoursite.com/#organization",
"name": "Your Site",
"logo": "https://yoursite.com/logo.png"
},
{
"@type": "Article",
"headline": "Your Post Title",
"publisher": { "@id": "https://yoursite.com/#organization" }
}
]
}@graph holds multiple entities in one script, and @id lets any of them point at another rather than restating it. Change the logo once and every page follows.
Which types still earn rich results
Worth being blunt, because a lot of advice online is out of date:
- BreadcrumbList — still shown, broadly. Best return for the effort.
- Article — feeds Top Stories and article treatments, though it no longer guarantees a thumbnail.
- Product, Recipe, Review, Event, JobPosting — still produce rich results where the page genuinely is that thing.
- FAQPage — Google restricted these to authoritative government and health sites in 2023. On a commercial site the markup is valid and harmless, but expect no visual result from it. Keep the FAQ content for users and long-tail queries; don't count the schema as a win.
- HowTo — deprecated for most surfaces.
Search Console's Search appearance tab is the honest scoreboard. If a type earns you nothing, it will be absent there no matter how clean the markup validates.
Validating, and what to expect
Use the Rich Results Test for whether Google can parse it, and the Schema.org validator for whether it is well-formed schema. They answer different questions and disagree often — the first is what matters for search.
Then watch Search Console's Enhancements section. Errors there are the ones actually affecting you; a warning about a recommended-but-missing property usually is not worth chasing.
Structured data does not improve rankings directly. It improves how your result looks, which moves click-through rate — and that is worth having, but it will not rescue a page that ranks on page five.
Summary
Emit JSON-LD server-side, escape < when serialising, and generate every value from your CMS rather than by hand. Ship BreadcrumbList everywhere — it is the best-rewarded type left. Declare shared entities once under @graph with an @id and reference them, so an Organization can never contradict itself across pages. Only emit properties you can keep accurate, and check Search appearance to see which types are actually earning their keep.
Related: SEO in Next.js — metadata and canonicals and technical SEO for developers.
Related reading
- SEOTechnical SEO for Developers: What Actually Moves RankingsThe technical SEO decisions that live in code, not in a marketing checklist — rendering strategy, canonical implementation, redirect architecture, and the Core Web Vitals work that's actually engineering, not configuration.
- SEOOn-Page SEO That Doesn't Ruin UX: Headings, Links, and ContentHeadings, links, and content structure that serve search engines and users at the same time — and the specific patterns (keyword-stuffed H1s, hidden link blocks, generic "SEO paragraphs") that hurt both.
- SEOCanonical URLs, Pagination, and Duplicate Content: A Clear GuideHow to set canonicals and handle pagination so search engines index the right URLs and you don’t dilute rankings.
More in SEO