Shipping JSON-LD structured data from your codebase
Structured data is the rare SEO task that belongs to engineers, not marketers — it's literally code that ships with your build. But most of what's been written about it is either out of date or quietly wrong about what it does. Here's what JSON-LD actually buys you in 2026, and how to generate it from your app so it never drifts out of sync.
If you've ever installed an SEO plugin that "adds schema," you've seen structured data done the brittle way: a layer bolted onto the side of your site that guesses at your content and goes stale the moment anything changes. For a team that writes code, there's a better model — generate the structured data from the same source of truth that renders the page, at build time, and bake it into the HTML. It's a handful of lines, it never drifts, and it's the version Google actually wants.
But before any code: be honest about what structured data does, because the internet is full of claims it doesn't deserve.
It's an eligibility signal, not a ranking signal
The single most important fact about structured data is the one most often gotten wrong: it does not boost your rankings. Google has been unambiguous about this. Schema markup is not a ranking signal — it's an eligibility signal. It makes a page eligible to appear as a rich result (the enhanced listings with stars, breadcrumbs, article cards, and so on), and it helps Google understand the page and the entities on it. It does not move you up the ten blue links.
You can see the logic in how Google enforces it. A structured-data manual action — the penalty for spammy or mismatched markup — removes a page's eligibility for rich results. It explicitly does not lower how the page ranks in web search. Reward and punishment both live on the rich-results layer, never on the ranking layer. If a post promises that adding schema will lift your position, close the tab.
So why bother? Two reasons that are real:
- Rich results. An article card with an image, or a breadcrumb trail in place of a raw URL, is a bigger, more clickable target. Same position, more clicks.
- Entity understanding. Structured data is how you tell Google, in a machine-readable way, "this is an Organization named X, here are its social profiles, here's the author of this article." That's how a brand resolves into a knowledge-panel entity — and increasingly, how it gets cited in AI-generated answers, which lean on the same structured understanding of who-published-what.
Neither of those is a ranking boost. Both are worth having.
Why it belongs in your codebase, not a plugin
Google's structured-data policy has one rule that quietly disqualifies most plugin setups: the markup must match the content visible on the page. Structured data that describes content the user can't see — a different headline, a date that's not shown, a rating nobody can find — is a policy violation, and it's exactly the kind of drift a side-channel plugin produces. The marketing tool says the article was published in January; the CMS says March; the two were never wired together.
The fix is structural. Generate the JSON-LD from the same data object that renders the page, so there is only one source of truth and nothing to keep in sync. In a typed codebase this is a pure function:
// One source of truth: the same post object that renders the page.
export function blogPostingJsonLd(post: Post) {
return {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
datePublished: post.date,
image: post.ogImage,
mainEntityOfPage: `https://example.com/blog/${post.slug}`,
author: { '@type': 'Organization', name: 'Code Results' },
publisher: {
'@type': 'Organization',
name: 'Code Results',
logo: { '@type': 'ImageObject', url: 'https://example.com/logo.png' },
},
};
}
Then serialize it into the document head:
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BlogPosting", ...}
</script>
Because the headline, description, and date come straight off the post object the renderer already uses, they cannot disagree with the rendered page. That's the whole win.
Put it where wave one can see it
There's a delivery catch that bites JavaScript apps specifically. If your site is a client-rendered SPA and you inject the <script type="application/ld+json"> only after React mounts, you've put your structured data in the second of Google's two indexing waves — the rendered one, which is queued and can lag hours to weeks. The first wave parses raw HTML and may never wait for the second.
So the JSON-LD has to be in the initial HTML response, not added by client JavaScript. For a prerendered or server-rendered site that's automatic: render the <script> tag into the static HTML at build time, where the crawler reads it on first byte. (This is the same wave-one logic that decides whether your content gets indexed reliably — covered in our React & Vite SPA SEO post.)
<rect x="196" y="46" width="140" height="48" fill="#111" stroke="#2a2a2a"/>
<text x="266" y="68" fill="#ddd" text-anchor="middle">jsonLd(post)</text>
<text x="266" y="84" fill="#666" text-anchor="middle">pure builder</text>
<rect x="378" y="46" width="150" height="48" fill="#111" stroke="#2a2a2a"/>
<text x="453" y="68" fill="#ddd" text-anchor="middle">prerendered</text>
<text x="453" y="84" fill="#666" text-anchor="middle"><head> HTML</text>
<rect x="570" y="46" width="126" height="48" fill="#b4ff57" fill-opacity="0.9"/>
<text x="633" y="68" fill="#050505" text-anchor="middle">crawler reads</text>
<text x="633" y="84" fill="#0a3b00" text-anchor="middle">wave one</text>
<g fill="#b4ff57" font-size="14">
<text x="170" y="74">→</text>
<text x="352" y="74">→</text>
<text x="544" y="74">→</text>
</g>
<text x="360" y="150" fill="#888" text-anchor="middle">one source of truth → markup can't drift from the page</text>
<text x="360" y="176" fill="#b4ff57" text-anchor="middle">result: eligible for rich results, not a ranking boost</text>
What still earns rich results in 2026 (and what doesn't)
This is where most older guides will lead you astray, because Google has been retiring rich-result types, not adding them. Two big ones are gone: HowTo rich results were fully removed across desktop and mobile in 2024, and FAQ rich results were deprecated in May 2026 — Search Console dropped its FAQ reports the following month. The FAQPage and HowTo schema types are still valid and Google may still use them to understand a page, but if you were adding them to win the enhanced listing, that listing no longer exists. Don't spend engineering time chasing a feature Google already buried.
Here's what's worth shipping, and why:
| Schema type | What it does in 2026 | Worth it for |
|---|---|---|
| BlogPosting / Article | Article cards; author & publisher entity links | Any content site |
| BreadcrumbList | Replaces the raw URL with a readable trail in the result | Sites with real hierarchy |
| Organization | Brand entity, logo, sameAs social profiles → knowledge panel & AI citations | Every site, once, sitewide |
| Product / Offer | Price, availability, review stars in the listing | E-commerce, SaaS pricing |
| SoftwareApplication | App name, category, rating for software listings | Dev tools, apps |
| FAQPage / HowTo | No rich result anymore — understanding only | Skip unless you have another reason |
Four rules that keep you out of trouble
- Match the visible page. Every value in your JSON-LD must correspond to something a user can actually see. This is policy, and it's the most common reason markup gets ignored or penalized.
- Prefer JSON-LD. Google recommends it over Microdata and RDFa precisely because it lives in one block in the
<head>instead of being tangled through your DOM — which is also why it's the format that survives refactors. - Get it into the initial HTML. Build-time or server-side, not a client-side
useEffect. Otherwise it rides the slow second wave. - Validate, then trust the pipeline. Run a representative page through Google's Rich Results Test and the Schema Markup Validator once. After that, because the markup is generated from a typed function, every new page is correct by construction — you validate the builder, not each post.
That last point is the real reason to own structured data in code rather than in a plugin. Done as a function over your content model, it's write-once: every page that ships is correct, in sync, and in the right wave, without anyone remembering to do anything. That's the kind of SEO work engineers are uniquely good at — and the kind that keeps paying out long after you've forgotten you wrote it.
See which of your PRs actually moved rankings.
Code Results connects your GitHub deploys to Google Search Console with causal attribution — so you stop guessing which code change moved organic search, and start measuring it.
Start for freeTechnical SEO for React and Vite SPAs Googlebot actually rewards
Googlebot runs JavaScript — but "can render" and "reliably indexes" are different promises, and the gap is where a React SPA leaks traffic. The two-wave indexing pipeline, why dynamic rendering is dead, and the SPA-specific mistakes (hash routing, soft 404s, onClick nav) that silently tank rankings.
How to tell if your last deploy hurt your SEO
Traffic dipped a few days after you shipped. Was it your deploy, a Google update, or the weekend? Read impressions, position, and clicks together — only one pattern points at your code — then rule out the short list of deploy-level regressions that actually tank rankings.
Do Core Web Vitals actually move your ranking?
Yes — but as a conditional signal that decides positions among pages already close on relevance, measured on your slowest quarter of real users over a rolling month. Here is what Google actually said, and why a performance fix takes six to eight weeks to show up.