Why is my JSON-LD missing from the HTML in Next.js?

In short

Next.js does not emit a script element for a component using next/script with strategy beforeInteractive. It serialises the payload into a self.__next_s array that the client bundle reads after hydration, so JSON-LD declared that way is absent from the served HTML and invisible to any crawler that does not execute JavaScript.

A printed page held up to the light, with one central block entirely blank where content should be.
Symptom
JSON-LD present in DevTools, absent from view-source and curl
Console output
None — nothing errors
Cause
next/script strategy="beforeInteractive" emits no element
What is emitted instead
A self.__next_s array entry, read after hydration
Blast radius
Every route rendered by the layout that declares it
Fix
A plain <script type="application/ld+json">

An external audit told me the identity block on my homepage had no name and no description. That was true, and it was the least interesting thing wrong. The reason a page carrying four structured-data nodes was being judged on one of them is that the other three were not in the HTML at all.

I work in digital marketing. Structured data is not an exotic corner of my job, it is close to the centre of it — which is why finding this on my own site was an unpleasant fifteen minutes. Everything looked correct in every tool I habitually check. It had been wrong for months.

#The code that looks completely fine

My root layout declared the site's Person and WebSite nodes like this. If you have ever added JSON-LD to a Next.js app, there is a reasonable chance you have written something close to it.

import Script from "next/script";

<Script
  id="ld-person"
  type="application/ld+json"
  strategy="beforeInteractive"
  dangerouslySetInnerHTML={{ __html: JSON.stringify(personJsonLd) }}
/>
app/layout.tsx — wrong, and gives no indication of being wrong.

The reasoning behind beforeInteractive seems sound when you write it: this is metadata, crawlers should see it as early as possible, so load it as early as the framework allows. Every part of that sentence is wrong, but not obviously.

#What beforeInteractive actually does

next/script with strategy="beforeInteractive" does not put a script element in the document. It serialises the props into an array on self.__next_s, and the client-side bundle walks that array after it loads and creates the real element then.

So the served HTML contains something like this, which is a string of escaped JSON inside a bootstrap call rather than a block any parser is looking for:

<script>(self.__next_s=self.__next_s||[]).push([0,{
  "type":"application/ld+json",
  "children":"{\"@context\":\"https://schema.org\",\"@type\":\"Person\"…"
}])</script>

#The check that tells the truth

Ask the server, not the browser. If the count comes back lower than the number of nodes you believe you have, the missing ones are being injected by JavaScript.

curl -s https://example.com/ | grep -c 'type="application/ld+json"'
Counts the literal ld+json blocks in the served HTML.

On my site that returned one. The DOM had four. The one it found was the only node I had written as an ordinary script tag inside a page component — which is why the audit judged my entire identity on it, and why the fields it wanted were missing: they were all sitting in the Person node it could not see.

#Who actually loses

It is worth being precise about the damage rather than overstating it. Google renders JavaScript in a second pass, so it can eventually pick up JSON-LD injected this way — deferred, and dependent on a render budget you do not control.

Almost nothing else does. Link unfurlers, most answer-engine fetchers, and any straightforward HTTP client see the response and nothing more. If your structured data exists only after hydration, then for that entire population it does not exist. For a personal site whose whole purpose is telling machines who the subject is, that is the failure that matters.

#The fix is to stop using next/script

JSON-LD is inert data. Nothing reads it at runtime, nothing depends on when it arrives, and there is therefore no reason for it to be loaded, ordered or prioritised at all. It only needs to be present.

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{ __html: JSON.stringify(personJsonLd) }}
/>
A plain script element. It renders into the HTML on the server.

It can sit in the body — JSON-LD is valid anywhere in the document, and React 19 hoists bare <link> elements into the head but not inline scripts, so do not expect it to move. After the change the same curl returned four blocks, and the audit score moved from 79 to 83 on the next run.

The score is the least important part of that. What actually changed is that the site's identity — the job title, the corroborating profile links, the disambiguation from somebody else with my name — is now readable by something that does not run JavaScript.

#The habit worth taking from this

The general form of this bug is verifying what was rendered instead of what was served, and it is not specific to structured data or to Next.js. Anything a framework describes as an optimisation is worth checking in the response body at least once, because optimisations are exactly the features that change where output ends up.

  1. Check structured data with curl against the deployed URL, never with DevTools.
  2. Count what you expect. A number that is lower than your node count is the whole diagnosis.
  3. Treat inert metadata as content to be printed, not as a script to be scheduled.

For what it is worth, the audit that surfaced this is Is Agentic, built by Vercel on Ora's checks. I would not have found this on my own — I had already looked at that layout many times and seen nothing wrong, because I was looking in the panel that shows you what you meant rather than the one that shows you what you shipped.

Questions this answers

Does next/script with beforeInteractive work for JSON-LD?

No. That strategy does not emit a script element into the server-rendered HTML; it serialises the payload into a self.__next_s array which the client bundle turns into a real element after hydration. Structured data declared this way is missing from the response and visible only to clients that execute JavaScript.

How do I check whether my structured data is in the served HTML?

Run curl against the deployed URL and count the literal occurrences of type="application/ld+json" in the output. Compare that number to the nodes you expect. Browser DevTools shows the live DOM after JavaScript has run, so it will display JSON-LD that was never present in the response.

Can Google read JSON-LD that is added by JavaScript?

Usually, because Google renders pages in a second pass and can pick up structured data injected during it. That pass is deferred and subject to a render budget. Most other consumers — link unfurlers, answer-engine fetchers and plain HTTP clients — do not execute JavaScript and will never see it.

See also

  • Notebook The notebook — what broke and the actual fix, plus first-person accounts of building as a marketer
  • The Story Operational experience, academic foundations and the full biography

Let's Build
What Comes Next.

Open to meaningful collaborations, AI-native systems, product strategy, and future-focused conversations.

“Human instinct. AI amplification.
Systemic execution.”

Suman Debnath

·

Brand Marketing Leader & AI Product Builder

© 2026

This site records visit data — pages viewed, time and scroll depth, device, your IP address and the approximate location and network provider derived from it — and sends it to me privately. It also runs Google Analytics and Vercel Analytics. Full detail and how to opt out.

This site, its code and its content are © 2026 Suman Debnath. All rights reserved — none of it is open source, and copying it needs permission first. Terms of use.