schemapilot.
Blog
Implementation

Next.js JSON-LD: How to Add Structured Data (2026 Guide)

·7 min read

Next.js gives you server-side rendering, static generation, and excellent core web vitals out of the box. What it does not give you is structured data. There is no built-in JSON-LD solution in Next.js, which means if you want rich results in Google -- star ratings, FAQ dropdowns, breadcrumb trails, product prices -- you need to add schema markup to your Next.js application yourself.

This guide covers three approaches to adding Next.js JSON-LD to your site: automated generation with Schema Pilot, native implementation using Server Components, and third-party packages like next-seo. Pick the one that fits your team and project.

Option 1: Automated schema markup with Schema Pilot

If you do not want to write or maintain JSON-LD code, Schema Pilot handles schema markup for Next.js sites automatically. It works with both App Router and Pages Router without any npm packages or per-page configuration.

Here is how it works:

  1. Sign up and add your Next.js site URL
  2. Schema Pilot's AI scans your pages, detects the content type, and generates the correct JSON-LD for each page
  3. Add a single embed script to your root layout.tsx:
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <script
          src="https://www.schemapilot.app/embed/YOUR_SITE_ID.js"
          defer
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

That is it. No schema code to write, no types to maintain, no JSON-LD objects scattered across your codebase. When your content changes, Schema Pilot re-scans and updates the structured data automatically.

The free plan covers 1 site and 30 page scans. For larger Next.js applications, paid plans add unlimited scans and weekly auto-rescanning.

Stop writing schema markup by hand

Schema Pilot scans your pages, generates valid JSON-LD, and serves it automatically. No code changes required.

Option 2: Native JSON-LD in Next.js App Router

The App Router in Next.js 13+ uses React Server Components by default. Server Components are ideal for JSON-LD because the structured data renders on the server, which is exactly what search engines want.

Adding JSON-LD to a page

The standard pattern for Next.js JSON-LD is a <script> tag with dangerouslySetInnerHTML. Despite the alarming name, this is safe when you control the data being serialized:

// app/blog/[slug]/page.tsx
interface BlogPostProps {
  params: Promise<{ slug: string }>;
}

export default async function BlogPost({ params }: BlogPostProps) {
  const { slug } = await params;
  const post = await getPost(slug);

  const jsonLd = {
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    headline: post.title,
    description: post.excerpt,
    datePublished: post.publishedAt,
    dateModified: post.updatedAt,
    author: {
      "@type": "Person",
      name: post.author.name,
      url: post.author.url,
    },
    image: post.coverImage,
  };

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <article>
        <h1>{post.title}</h1>
        {/* post content */}
      </article>
    </>
  );
}

This renders the Next.js JSON-LD directly in the server response. Googlebot sees it immediately without executing client-side JavaScript.

Site-wide schema in layout.tsx

Some schema types belong on every page: Organization and WebSite markup. Add these to your root layout so they appear site-wide:

// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  const orgJsonLd = {
    "@context": "https://schema.org",
    "@type": "Organization",
    name: "Your Company",
    url: "https://www.yoursite.com",
    logo: "https://www.yoursite.com/logo.png",
    sameAs: [
      "https://twitter.com/yourcompany",
      "https://github.com/yourcompany",
    ],
  };

  const websiteJsonLd = {
    "@context": "https://schema.org",
    "@type": "WebSite",
    name: "Your Company",
    url: "https://www.yoursite.com",
    potentialAction: {
      "@type": "SearchAction",
      target: "https://www.yoursite.com/search?q={search_term_string}",
      "query-input": "required name=search_term_string",
    },
  };

  return (
    <html lang="en">
      <head>
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{ __html: JSON.stringify(orgJsonLd) }}
        />
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteJsonLd) }}
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

Free Organization Schema Generator

Schools, NGOs, corporations, and similar entities. Generate valid JSON-LD in seconds.

Creating a reusable JsonLd component

Repeating dangerouslySetInnerHTML gets tedious. Extract it into a typed component:

// components/json-ld.tsx
import type { Thing, WithContext } from "schema-dts";

interface JsonLdProps<T extends Thing> {
  data: WithContext<T>;
}

export function JsonLd<T extends Thing>({ data }: JsonLdProps<T>) {
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
    />
  );
}

Now your page components stay clean:

import { JsonLd } from "@/components/json-ld";
import type { Article } from "schema-dts";

export default function BlogPost() {
  return (
    <>
      <JsonLd<Article>
        data={{
          "@context": "https://schema.org",
          "@type": "Article",
          headline: "Schema Markup for Next.js",
          datePublished: "2026-03-18",
        }}
      />
      <article>{/* content */}</article>
    </>
  );
}

This component uses schema-dts for TypeScript types (covered below), but you can also use Record<string, unknown> if you want to skip the type dependency.

Place JSON-LD script tags inside the component's return JSX, not in the Next.js metadata export. The metadata API handles <title> and <meta> tags, not arbitrary script elements. For Next.js structured data, the script tag approach is the correct pattern.

Using the Next.js metadata API

The App Router metadata export does not directly support JSON-LD. However, you can use generateMetadata alongside your JSON-LD script tags to keep all SEO concerns in one file:

// app/products/[id]/page.tsx
import type { Metadata } from "next";

interface Props {
  params: Promise<{ id: string }>;
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { id } = await params;
  const product = await getProduct(id);

  return {
    title: product.name,
    description: product.description,
    openGraph: {
      title: product.name,
      images: [product.image],
    },
  };
}

export default async function ProductPage({ params }: Props) {
  const { id } = await params;
  const product = await getProduct(id);

  const jsonLd = {
    "@context": "https://schema.org",
    "@type": "Product",
    name: product.name,
    description: product.description,
    image: product.image,
    offers: {
      "@type": "Offer",
      price: product.price,
      priceCurrency: "USD",
      availability: "https://schema.org/InStock",
    },
  };

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <div>{/* product UI */}</div>
    </>
  );
}

The metadata export handles Open Graph and <title>. The script tag handles Next.js JSON-LD. They complement each other.

Option 3: next-seo and schema-dts

next-seo (Pages Router)

The next-seo package was the go-to solution for structured data in Next.js Pages Router projects. It provides React components for common schema types:

// pages/blog/[slug].tsx (Pages Router)
import { ArticleJsonLd, NextSeo } from "next-seo";

export default function BlogPost({ post }) {
  return (
    <>
      <NextSeo title={post.title} description={post.excerpt} />
      <ArticleJsonLd
        type="BlogPosting"
        url={`https://yoursite.com/blog/${post.slug}`}
        title={post.title}
        images={[post.coverImage]}
        datePublished={post.publishedAt}
        authorName={post.author.name}
        description={post.excerpt}
      />
      <article>{/* content */}</article>
    </>
  );
}

next-seo supports ArticleJsonLd, ProductJsonLd, FAQPageJsonLd, LocalBusinessJsonLd, and several other types. Each component handles the JSON-LD structure internally.

With the App Router, next-seo is less relevant. The built-in metadata API replaces its meta tag features, and the native script tag approach for Next.js structured data is simpler than adding a dependency. If you are starting a new project on App Router, skip next-seo and use the native approach.

schema-dts for TypeScript type safety

The schema-dts package provides TypeScript type definitions for every Schema.org type. It does not generate any runtime code -- it is purely a type library.

npm install schema-dts

With schema-dts, your IDE catches errors before they reach production:

import type { Product, WithContext } from "schema-dts";

// TypeScript will flag invalid properties
const productSchema: WithContext<Product> = {
  "@context": "https://schema.org",
  "@type": "Product",
  name: "Wireless Headphones",
  offers: {
    "@type": "Offer",
    price: 99.99,
    priceCurrency: "USD",
    // @ts-expect-error - 'available' is not a valid property
    available: true,
  },
};

This catches typos and invalid properties at build time. For any Next.js structured data implementation of meaningful size, schema-dts is worth the install.

Free Product Schema Generator

Goods and services with offers and reviews. Generate valid JSON-LD in seconds.

Dynamic schema generation

Most Next.js sites pull content from a CMS, database, or API. Your schema markup for Next.js should be generated dynamically from the same data source.

At build time (Static Generation)

For generateStaticParams pages, build the JSON-LD during static generation:

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

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

  const jsonLd = {
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    headline: post.title,
    datePublished: post.publishedAt,
    dateModified: post.updatedAt,
    author: { "@type": "Person", name: post.author },
    image: post.coverImage,
    wordCount: post.wordCount,
  };

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <article>{/* render post */}</article>
    </>
  );
}

The Next.js JSON-LD is baked into the static HTML at build time. No runtime cost, no client-side rendering delay.

At request time (Dynamic Rendering)

For pages with dynamic = "force-dynamic" or uncached fetches, the schema is generated per-request. The code pattern is identical -- Server Components handle both cases transparently. This is useful for product pages where price or availability changes frequently.

Keep your Next.js structured data in sync with visible page content. If your product page shows a price of $49 but your JSON-LD says $39, Google may issue a manual action. Always derive schema values from the same data source that renders the page.

Essential schema types for Next.js sites

The right schema types depend on what your Next.js site does:

Site typePrimary schemaSupporting schema
Blog / content siteArticle / BlogPostingWebSite, Organization, BreadcrumbList
E-commerceProduct with OfferOrganization, BreadcrumbList, FAQPage
SaaS / softwareSoftwareApplicationOrganization, WebSite, FAQPage
Local businessLocalBusinessOrganization, BreadcrumbList
Documentation siteWebPage, HowToWebSite, BreadcrumbList, Organization

Every Next.js site should have Organization and WebSite schema in the root layout. Add page-specific types from the table above based on your content.

BreadcrumbList is worth adding to nearly every site. It replaces the raw URL in search results with a clean navigation path like "Home > Blog > Schema Markup for Next.js," and it is straightforward to generate from your route structure.

Testing and validation

Before deploying your schema markup to production, validate it:

Google Rich Results Test (search.google.com/test/rich-results) -- paste a URL or code snippet. This tells you which rich result types your Next.js structured data qualifies for and highlights any errors.

Schema Markup Validator (validator.schema.org) -- validates against the full Schema.org spec. Catches structural issues that Google's tool might not flag.

Chrome DevTools -- inspect the rendered HTML to confirm your JSON-LD script tags are present in the server response. Check both the initial HTML (View Source) and the rendered DOM.

For Next.js applications specifically, test both the server-rendered output and the hydrated page. Use curl or "View Page Source" to confirm the schema markup for Next.js pages is present in the initial HTML, not injected client-side:

curl -s https://yoursite.com/blog/my-post | grep "application/ld+json"

If the script tag appears in the raw HTML, search engines will reliably read it.

Conclusion

Adding JSON-LD to Next.js is not complicated, but it does require a deliberate approach. For teams that want zero maintenance, Schema Pilot automates the entire process with a single embed script. For developers who prefer manual control, the native <script> tag pattern in Server Components is clean, type-safe with schema-dts, and renders on the server where search engines expect it.

Whichever approach you choose, the key is to actually implement it. Next.js gives you great technical SEO foundations, but structured data is the piece you have to add yourself.

Stop writing schema markup by hand

Schema Pilot scans your pages, generates valid JSON-LD, and serves it automatically. No code changes required.

Related posts

Stop leaving rich results on the table

Every page without schema markup is a missed opportunity for clicks. Schema Pilot handles the entire process — scanning, generating, validating, and serving structured data — so you don't have to.