all posts
2026-06-17

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:

  1. 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.
  2. 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.)

post object (renders page)
<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">&lt;head&gt; 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>
Generate JSON-LD from the object that renders the page, at build time, into the initial HTML. One source of truth, read on the first crawl wave.

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 typeWhat it does in 2026Worth it for
BlogPosting / ArticleArticle cards; author & publisher entity linksAny content site
BreadcrumbListReplaces the raw URL with a readable trail in the resultSites with real hierarchy
OrganizationBrand entity, logo, sameAs social profiles → knowledge panel & AI citationsEvery site, once, sitewide
Product / OfferPrice, availability, review stars in the listingE-commerce, SaaS pricing
SoftwareApplicationApp name, category, rating for software listingsDev tools, apps
FAQPage / HowToNo rich result anymore — understanding onlySkip unless you have another reason

Four rules that keep you out of trouble

  1. 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.
  2. 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.
  3. Get it into the initial HTML. Build-time or server-side, not a client-side useEffect. Otherwise it rides the slow second wave.
  4. 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.

[ From the team building this ]

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 free