<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: saikodev</title>
    <description>The latest articles on DEV Community by saikodev (@saikodev).</description>
    <link>https://dev.to/saikodev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4072372%2Fd6ef620d-de8d-4c94-a7ab-2a855b7c4a08.JPG</url>
      <title>DEV Community: saikodev</title>
      <link>https://dev.to/saikodev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saikodev"/>
    <language>en</language>
    <item>
      <title>Your Next.js JSON-LD might be invisible to crawlers. Here's a one-line check.</title>
      <dc:creator>saikodev</dc:creator>
      <pubDate>Tue, 11 Aug 2026 05:55:35 +0000</pubDate>
      <link>https://dev.to/saikodev/your-nextjs-json-ld-might-be-invisible-to-crawlers-heres-a-one-line-check-2nbb</link>
      <guid>https://dev.to/saikodev/your-nextjs-json-ld-might-be-invisible-to-crawlers-heres-a-one-line-check-2nbb</guid>
      <description>&lt;p&gt;I spent a while wondering why a site of mine had almost nothing indexed in Bing. The structured data looked perfect. It validated. It showed up in DevTools.&lt;br&gt;
It was never in the HTML.&lt;br&gt;
Here's the one-liner that told me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'application/ld+json'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I got &lt;code&gt;0&lt;/code&gt;. On every page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cause
&lt;/h2&gt;

&lt;p&gt;The JSON-LD was rendered with &lt;code&gt;next/script&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Script&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Layout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Script&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"website-structured-data"&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;
        &lt;span class="na"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;websiteSchema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks completely reasonable. It's also wrong for this use case.&lt;br&gt;
&lt;code&gt;next/script&lt;/code&gt; defaults to &lt;code&gt;strategy="afterInteractive"&lt;/code&gt;, which means the tag is injected &lt;strong&gt;after hydration&lt;/strong&gt;, on the client. That's the right behavior for analytics snippets and third-party widgets — it's what the component is for. But structured data isn't a script to &lt;em&gt;execute&lt;/em&gt;. It's markup that needs to be &lt;em&gt;in the document&lt;/em&gt; when a crawler reads it.&lt;br&gt;
So the server-rendered HTML went out with zero JSON-LD, and the tag only appeared once React had booted in a real browser.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why it's so easy to miss
&lt;/h2&gt;

&lt;p&gt;Every way you'd naturally check it says everything is fine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DevTools → Elements&lt;/strong&gt; shows the tag. Of course it does — that's the live DOM, after hydration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rich Results Test / Schema validators&lt;/strong&gt; often run a headless browser, so they execute JS and see the tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your eyes&lt;/strong&gt;, because the code reads correctly.
The one place it doesn't show up is &lt;code&gt;view-source:&lt;/code&gt; — the actual bytes the server sent. Which is exactly what most crawlers parse.
Google &lt;em&gt;can&lt;/em&gt; render JavaScript, so this isn't guaranteed to be fatal there. But rendering is a separate, deferred, budgeted pass — not the same as being in the initial HTML. And plenty of other consumers don't render JS at all, or do it inconsistently: Bing, social card scrapers, and a growing pile of LLM crawlers. If your structured data only exists post-hydration, you're betting your rich results on the most expensive code path.
## The fix
Use a plain &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag. That's it.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;JsonLd&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;
      &lt;span class="na"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No import, no strategy, no &lt;code&gt;id&lt;/code&gt; needed. React renders it straight into the server HTML.&lt;br&gt;
The part that surprised me: &lt;strong&gt;this works in client components too.&lt;/strong&gt; I assumed &lt;code&gt;'use client'&lt;/code&gt; meant "not in the SSR output," but client components are still server-rendered on the first pass — they just also hydrate. A plain &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag inside one lands in the HTML exactly like it would in a server component.&lt;/p&gt;
&lt;h2&gt;
  
  
  Verifying the fix
&lt;/h2&gt;

&lt;p&gt;Same one-liner, run against a production build (&lt;code&gt;next build &amp;amp;&amp;amp; next start&lt;/code&gt;) rather than the dev server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:3000/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'application/ld+json'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a whole site at once, against your build output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find .next/server/app &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.html'&lt;/span&gt; &lt;span class="nt"&gt;-exec&lt;/span&gt; sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'echo "$(grep -c "application/ld+json" "$1")  $1"'&lt;/span&gt; _ &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mine went from &lt;code&gt;0&lt;/code&gt; on every page to 2–5 per page. Then I parsed each block to make sure I hadn't shipped malformed JSON along the way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;script type="application&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;ld&lt;/span&gt;&lt;span class="se"&gt;\+&lt;/span&gt;&lt;span class="sr"&gt;json"&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;?)&lt;/span&gt;&lt;span class="sr"&gt;&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;script&amp;gt;/g&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;// throws loudly if you broke something&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;72 blocks, all valid. That check took two minutes and I'd have shipped a busted escape without it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general lesson
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;next/script&lt;/code&gt; is for scripts that &lt;strong&gt;do&lt;/strong&gt; something. JSON-LD, and any other metadata-shaped &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;, isn't code — it's content. Content belongs in the HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  More broadly: for anything a crawler consumes, verify against &lt;code&gt;curl&lt;/code&gt;, not DevTools. The two disagree in exactly the cases that matter, and DevTools is the one that lies to you.
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Found this while debugging indexing on &lt;a href="https://how-we-invest.com/learn/13f-filing-deadlines/" rel="noopener noreferrer"&gt;how-we-invest.com&lt;/a&gt;, a 13F filing tracker I build in Next.js. The root layout was affected, so the bug was quietly live on every page on the site.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>nextjs</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
