Menu

React & Next.js

Render JSON-LD with dangerouslySetInnerHTML — safe here by construction.

The pattern

React treats a script tag’s serialized JSON like any other content, so the string goes in via dangerouslySetInnerHTML. That prop name is a deliberate warning label — but toJsonLd output cannot contain a raw <, >, or &, so nothing in it can close the tag or start a new one. This is the one place the prop’s danger doesn’t apply:

import { createArticleSchema, toJsonLd } from "structured-data-generators";

export function ArticleJsonLd({ post }: { post: Post }) {
  const article = createArticleSchema({
    inLanguage: "en",
    headline: post.title,
    description: post.description,
    datePublished: post.publishedAt,
    url: `https://example.com/blog/${post.slug}`,
  });

  return (
    <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: toJsonLd(article) }} />
  );
}

Google reads JSON-LD from the body as well as the head, so the script can render right where your page component lives — no head-management library required.

Next.js App Router

Build the schema in a server component next to the data it describes, so it ships in the initial HTML that crawlers receive:

// app/blog/[slug]/page.tsx
import {
  createArticleSchema,
  createBreadcrumbListSchema,
  toJsonLd,
} from "structured-data-generators";

export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await getPost(slug);

  const structuredData = [
    createArticleSchema({
      inLanguage: "en",
      headline: post.title,
      description: post.description,
      datePublished: post.publishedAt,
      url: `https://example.com/blog/${slug}`,
    }),
    createBreadcrumbListSchema([
      { name: "Home", url: "https://example.com" },
      { name: "Blog", url: "https://example.com/blog" },
      { name: post.title },
    ]),
  ];

  return (
    <article>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: toJsonLd(structuredData) }}
      />
      <h1>{post.title}</h1>
    </article>
  );
}

This is the same shape Next’s own JSON-LD guidance recommends; the Metadata API covers titles and Open Graph tags but not structured data. In the Pages Router, the identical script element goes inside next/head’s <Head>.

An aside on React’s escaping

Rendering the JSON as a plain child ({toJsonLd(article)} between the tags) happens to survive React’s HTML escaping — the escaper rewrites raw <, >, and &, and the serializer’s output contains none. Don’t rely on that coincidence of implementations: dangerouslySetInnerHTML states the intent (“this string is final markup”) and is the documented pattern.