← All guides
Setup guide

Add JSON-LD structured data to Next.js

Get your product's price, rating and stock into Google and AI answers from the very first HTML response. In the App Router, that means rendering the JSON-LD from a Server Component.

  • Render it from a Server Component
  • Not next/script, not a "use client" component
  • FullSchema gives you a ready PageSchema component
  • Check the raw HTML with curl, not DevTools

Free page check first, then $10 free to start. No card needed.

halden.audio/products/arc-anc

Your page

Tap a detail to trace it

Halden

Arc ANC Headphones

4.7 (382 reviews)

$249.00In stock

Reviews

★★★★★ “Quiet on a full flight, and the battery lasted all week.”

Shipping & returns

Free shipping · 30-day returns

FullSchema reads it and writes the schema search engines and AI read.

In search results

halden.audio › products › arc-anc

Arc ANC Headphones

4.7 (382)·$249.00·In stock

Free delivery · 30-day returns

In AI answers

Noise-cancelling headphones under $300?

The Arc ANC Headphones from Halden is $249.00, rated 4.7 from 382 reviews, and in stock with free shipping.

halden.audio
app/product/[slug]/page.tsx
export default async function Page() {
const jsonLd =
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Product",
"@id": "https://halden.audio/products/arc-anc#product",
"name": "Arc ANC Headphones",
"description": "Wireless noise-cancelling headphones with 40-hour battery life.",
"sku": "HA-ARC-ANC-BLK",
"image": [
"https://halden.audio/images/arc-anc-black.jpg"
],
"color": "Black",
"brand": { "@type": "Brand", "name": "Halden" },
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.7",
"reviewCount": "382"
},
"review": [
{
"@type": "Review",
"author": { "@type": "Person", "name": "Chris T." },
"reviewRating": { "@type": "Rating", "ratingValue": "5" },
"reviewBody": "Quiet on a full flight, and the battery lasted all week."
}
],
"offers": {
"@type": "Offer",
"url": "https://halden.audio/products/arc-anc",
"price": "249.00",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock",
"itemCondition": "https://schema.org/NewCondition",
"seller": { "@id": "https://halden.audio/#org" },
"shippingDetails": {
"@type": "OfferShippingDetails",
"shippingRate": {
"@type": "MonetaryAmount",
"value": 0,
"currency": "USD"
},
"shippingDestination": {
"@type": "DefinedRegion",
"addressCountry": "US"
},
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"handlingTime": {
"@type": "QuantitativeValue",
"minValue": 0,
"maxValue": 1,
"unitCode": "DAY"
},
"transitTime": {
"@type": "QuantitativeValue",
"minValue": 2,
"maxValue": 4,
"unitCode": "DAY"
}
}
},
"hasMerchantReturnPolicy": {
"@type": "MerchantReturnPolicy",
"applicableCountry": "US",
"returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
"merchantReturnDays": 30,
"returnMethod": "https://schema.org/ReturnByMail",
"returnFees": "https://schema.org/FreeReturn"
}
}
},
{
"@type": "Organization",
"@id": "https://halden.audio/#org",
"name": "Halden",
"url": "https://halden.audio/",
"logo": "https://halden.audio/images/logo.png"
},
{
"@type": "WebPage",
"@id": "https://halden.audio/products/arc-anc#webpage",
"url": "https://halden.audio/products/arc-anc",
"name": "Arc ANC Headphones | Halden",
"breadcrumb": {
"@id": "https://halden.audio/products/arc-anc#breadcrumb"
},
"mainEntity": {
"@id": "https://halden.audio/products/arc-anc#product"
}
},
{
"@type": "BreadcrumbList",
"@id": "https://halden.audio/products/arc-anc#breadcrumb",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://halden.audio/"
},
{
"@type": "ListItem",
"position": 2,
"name": "Headphones",
"item": "https://halden.audio/headphones"
},
{
"@type": "ListItem",
"position": 3,
"name": "Arc ANC Headphones"
}
]
}
]
}
return (
<script type="application/ld+json"
dangerouslySetInnerHTML={{ __html: json }}
/>
);
}
✓ Valid JSON-LD124 lines88 fieldswritten from one page

Short answer

To add JSON-LD to a Next.js App Router site, render a <script type="application/ld+json"> tag from a Server Component, with the JSON string in dangerouslySetInnerHTML. Don't use next/script or a Client Component: their script arrives after hydration, so crawlers that don't run JavaScript miss it. Verify with curl.

Renders from
Server Component only
Avoid
next/script, Client Components
Setup time
~2 minutes

Updated

Step by step

How to add it

  1. 1

    Generate the schema

    Paste your page's live (or deployed preview) URL into FullSchema. On the result, switch the code view to Next.js and click Copy: you get a ready PageSchema Server Component to save next to the page and render there.

  2. 2

    Render it from a Server Component

    Render <PageSchema /> in the page, or drop the object into your own JSX. Either way it has to come from a component that is not marked "use client". A script tag from a Client Component ships after hydration, and crawlers that don't run JavaScript never see it:

    export default async function Page() {
      const jsonLd = { /* generated schema */ };
      return (
        <>
          <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, "\\u003c") }} />
          {/* rest of the page */}
        </>
      );
    }
  3. 3

    Skip next/script for this specific case

    next/script is built for third-party behavioral scripts (analytics, chat widgets, things that can safely load late). JSON-LD isn't that: it needs to be present in the response your server sends on the very first request, which a plain server-rendered script tag guarantees and next/script, depending on its strategy prop, doesn't.

  4. 4

    Verify against the actual HTML response

    Don't trust the browser DevTools Elements panel. It shows the DOM after React has run, not the raw response. Run curl against the URL, or use View Page Source, and confirm the script tag is present in that raw HTML.

    curl output for a Next.js product page: JSON-LD from a Server Component is in the raw HTML, from a Client Component it is missing
    The raw response tells the truth: a Server Component's script tag is there, a Client Component's isn't.

Good to know

The same rule applies to Remix, Astro in SSR mode, and any other React meta-framework: render the schema server-side, don't inject it client-side after the page loads.

For dynamic pages (a specific product or article), generate the JSON-LD inside the page component using the same data you already fetch for the page itself, not a static, hardcoded object.

On your own

How to add JSON-LD to Next.js manually

Without FullSchema: what the same result takes by hand.

In Next.js you're writing code either way, so doing it by hand is mostly about getting the data model right.

1. Build it from the data you already fetch
Create the JSON-LD object in the same Server Component that fetches the page's data, so the markup and the page can't drift apart. The schema-dts package gives you TypeScript types for schema.org and catches misspelled properties at build time.
2. Choose types and fields deliberately
Pick the most specific schema.org type per route and check Google's documentation for the fields it needs. Leave out values the page doesn't show, even if your database has them.
3. Render it safely and check the HTML
Render a plain <script type="application/ld+json"> from the Server Component and escape < in the serialized JSON. Then check the built page's source, not DevTools, and run a few routes through the Rich Results Test.
4. Test it like the rest of your code
Add a test or build step that renders a few representative pages and validates their JSON-LD, so a refactor that renames a field doesn't silently remove your rich results.
From the forums

What people ask about Next.js schema, answered

Collected from Reddit, the Next.js community and SEO forums. Straight answers, including the cases where you don't need us.

I get a hydration mismatch as soon as I add a JSON-LD script.

You're rendering the script through next/script or inside a Client Component. Render a plain <script type="application/ld+json"> with dangerouslySetInnerHTML in a Server Component, the way the Next.js docs show. Nothing about it needs to hydrate.

next/script or a plain script tag?

Plain script tag. next/script is for JavaScript that runs; it adds the content to the hydration payload and can defer or move the tag. JSON-LD is inert data and belongs in the server HTML as is.

Should the schema live in layout.tsx or page.tsx?

Site-wide entities (Organization, WebSite) in the root layout, once. Page-specific types in the page component that already has the data. Don't put Product in a layout that wraps every route.

How do I build the JSON-LD for dynamic routes like /products/[slug]?

Use the same object you fetch for the page: map its fields into the schema object inside the Server Component and serialize with JSON.stringify. Generate one example with FullSchema for a live product URL to see which properties Google actually wants for that type, then template it.

Do I need to escape anything in the JSON?

Replace < with \u003c before injecting, so a value containing </script> can't break out of the tag. The docs' snippet does exactly that.

With ISR or static export, is the schema in the HTML?

Yes, as long as it's rendered by a Server Component; the generated HTML contains it. Verify on the deployed URL with View Page Source, not DevTools. If it's missing there but visible in DevTools, a Client Component is injecting it.

Not sure what your Next.js pages have today? The site report is free.

Check every page free
About the setup

Next.js structured data questions

Can I put this in generateMetadata instead?

generateMetadata is for the head metadata Next.js manages for you directly (title, meta tags, OpenGraph); it doesn't have a first-class slot for an arbitrary script tag. Rendering the script directly in your Server Component's JSX is the more direct route and behaves the same way for crawlers.

Why does my schema show up in DevTools but not in view-source?

That's the exact symptom of a Client Component: React is injecting the tag after the page loads, so it's real in the rendered DOM but absent from the HTML your server actually sent. Move the script tag into a Server Component to fix it.

Does this work the same way with the Pages Router?

Yes, same idea: render the script tag server-side, for example inside a getServerSideProps-backed page or _document.tsx for something site-wide, rather than injecting it from client-side JavaScript.

Ready to generate Next.js schema?

Paste your page's URL and FullSchema handles the rest. No dropdowns, nothing to fill in by hand.

Free page check first, then $10 free to start. No card needed.