Paul LovellBook a call
Back to Blog
11 min readPaul Lovell

Schema Markup for JS-Rendered Sites (React/Next.js)

A schema markup validator can show valid output while the JSON-LD never reaches what a crawler actually processes. Here's why, and how to fix it.

Technical SEOStructured DataJavaScript SEO

Structured data on a JavaScript-rendered site has a failure mode that doesn't exist on a static HTML page: schema that's genuinely present in your source code and never reaches the version of the page a search engine or AI system actually processes. The schema markup validator shows valid output, the developer sees correct JSON-LD in the component tree, and it still doesn't work in production — because rendering and crawling aren't the same event on a JS-heavy site the way they are on static HTML.

Why JS-rendered schema fails silently

Google's crawler executes JavaScript before extracting structured data, but that execution happens in a separate rendering step after the initial crawl, on a delay, and with real resource constraints. If your JSON-LD is injected by a client-side script that runs late, depends on data that hasn't loaded yet, or fails silently under a rendering timeout, the rendered HTML Google actually evaluates may simply not contain the schema block that exists in your component source. This is the JS-rendered equivalent of the "valid in isolation, missing in practice" problem covered across this series — the schema markup itself can be perfectly correct and still never reach the system it's meant to inform.

Non-Google AI crawlers make this worse, not better: many don't execute JavaScript at all, or do so far less reliably than Googlebot. Structured data that depends entirely on client-side rendering may be effectively invisible to a meaningful share of the AI systems increasingly relevant to the retrieval-and-citation mechanics covered in the AI Overviews article in this series.

SSR vs CSR vs static generation for schema

Server-side rendering (SSR) and static site generation (SSG) both produce a rendered HTML response that already contains the JSON-LD script tag — no client-side JavaScript execution required for the schema to be present in what a crawler first receives. This is the most reliable pattern for structured data specifically, independent of any broader rendering-strategy decision for the rest of the page. Client-side rendering (CSR), where the schema is injected by JavaScript running in the browser after initial page load, is the pattern most likely to produce the silent-failure problem — it works fine for a crawler that fully executes JavaScript and waits long enough, and may not work at all for one that doesn't.

For a Next.js site specifically, generating JSON-LD server-side — inside a Server Component, in generateMetadata, or via getServerSideProps/getStaticProps in the Pages Router — and rendering it as a plain script tag in the initial HTML response sidesteps the client-side timing problem entirely. This is the same principle covered in the JavaScript hydration and React Server Components articles elsewhere in this hub: server-rendered output is what a crawler reliably sees; client-rendered output is conditional on JavaScript execution actually completing successfully.

The specific Next.js pattern

In the App Router, JSON-LD generated inside a Server Component and output via a script tag with dangerouslySetInnerHTML is present in the server-rendered HTML from the first response — this is the pattern used throughout this site's own blog implementation. Avoid generating JSON-LD inside a Client Component ('use client') purely for structured data purposes; if the component needs to be a Client Component for other reasons (interactivity elsewhere on the page), keep the schema generation in a separate Server Component or in the page's server-rendered metadata function rather than bundling it with client-side logic.

Google's own guidance on JavaScript rendering

Google Developers' documentation on JavaScript SEO is explicit that Google Search does render JavaScript for indexing purposes, but recommends server-side rendering or pre-rendering where possible specifically because rendering is a resource-constrained, deferred process, not an instant guarantee. That guidance applies to structured data with extra weight, since structured data has no visible fallback the way rendered content does — a paragraph that fails to render at least still might leave some indexable text elsewhere on the page; a JSON-LD block that fails to render leaves nothing behind at all, no partial credit, no degraded-but-present version. Treat Google's own stated preference for server-side rendering as directly applicable to schema markup decisions, not just general content-rendering strategy.

It's worth remembering Google is not the only search engine or AI system your structured data needs to reach. Other search engines and a large share of AI crawlers have historically had less reliable or nonexistent JavaScript rendering support compared to Googlebot specifically — a JS-rendered-only schema implementation that Google itself renders correctly may still be invisible to a meaningful share of the broader search and AI ecosystem now relevant to how content gets discovered and cited.

Server-side rendering, static generation, and indexing outcomes

The practical indexing outcome differs meaningfully by rendering strategy. Server side rendering and static generation both deliver the JSON-LD script tag as part of the initial HTML response — the same response a crawler receives on first fetch, before any rendering step is even required. Client-side-only rendering makes the schema's presence conditional on a second, resource-constrained rendering pass completing successfully, which introduces both a timing risk (the render happens too late or times out) and, for less capable crawlers, a rendering-support risk (the crawler doesn't execute JavaScript at all). For any javascript heavy websites where structured data matters — which, per this series, is effectively every site with entities worth disambiguating — server-side or static generation of the schema block specifically is the safer default, even on projects where some other parts of the page reasonably use client-side rendering for interactivity.

Monitoring JS-rendered schema in Google Search Console

Google Search Console's structured data reports, covered in more depth in the testing-tools article in this series, reflect what Google's rendering pipeline actually captured — which makes Search Console a genuinely useful ongoing check specifically for JS-rendered sites, since it's checking the rendered outcome rather than your source code. A discrepancy between what you see in your component source and what Search Console reports as detected is a direct signal of a rendering-timing or client-side-execution problem worth investigating immediately, not a report to dismiss as a fluke. Check this report specifically after any framework upgrade, hydration-strategy change, or data-fetching refactor — these are exactly the changes most likely to silently shift schema from server-rendered to client-rendered without anyone noticing during a routine visual QA pass, since the page still looks identical to a human reviewer.

Testing schema on a JS-rendered site

Standard schema markup validators aren't all equally reliable here — some render JavaScript before checking structured data, some check only the initial HTML response. Google's Rich Results Test does execute JavaScript and reflects what Googlebot's rendering pipeline would see, making it a meaningfully better check for JS-rendered sites than a validator that only parses raw HTML. But the most reliable check remains manual: view the page's rendered source (not view-source, which shows pre-JavaScript HTML) via browser developer tools, or fetch the URL with a tool that executes JavaScript, and confirm the JSON-LD script tag is actually present in what gets delivered — not just in your component source or in a browser where JavaScript has already fully executed locally.

Hydration timing and schema

A subtler failure mode: schema that depends on client-side data fetching (an API call that populates the values used to build the JSON-LD object) can render correctly for a real user, whose browser waits for the fetch to complete, while still being absent or incomplete in what a crawler captures if the crawler doesn't wait as long or times out before the fetch resolves. Structured data should be built from data already available at render time — server-side data fetching that completes before the HTML response is sent — rather than from a client-side fetch that happens after initial paint. This is the same hydration-timing category of issue covered in the JavaScript hydration and SEO article in this hub, applied specifically to structured data rather than visible content.

Common JS-rendered schema mistakes

Injecting JSON-LD via useEffect or another client-side-only lifecycle hook is the most common version of this problem — it guarantees the schema is absent from the initial server response and dependent entirely on client-side JavaScript execution completing. Conditionally rendering schema based on client-side state (a loading flag, for instance) that a crawler might capture the page in before that state resolves is a related variant. And relying on a single validator or testing tool that doesn't execute JavaScript to declare a JS-rendered implementation "working" gives a false negative or false positive depending on which tool and which rendering path it's checking — cross-check with at least one JavaScript-executing tool (Google's Rich Results Test) and a direct rendered-source inspection before treating JS-rendered schema as verified.

Paul Lovell

Written by

Paul Lovell

International SEO Consultant & Founder of Always Evolving SEO. 15+ years running technical audits, log-file analysis, and root-cause fixes for multimillion-dollar businesses — speaker at SMX London, Search & Content Summit, and SEMrush events.