Blog
Wild & Free Tools

Schema Markup in Next.js (JSON-LD Patterns for App Router and Pages Router)

Last updated: April 2026 9 min read

Table of Contents

  1. App router pattern
  2. Pages router pattern
  3. Dynamic schema from cms
  4. Multiple schema types per page
  5. Site-wide schema in layout
  6. Validating nextjs schema
  7. Frequently Asked Questions

Next.js doesn't include schema markup helpers out of the box, but adding JSON-LD is straightforward in both the App Router and the older Pages Router. The pattern is the same: build a JavaScript object with your schema, render it inside a script tag with type="application/ld+json". This guide gives copy-paste patterns for both routers and the most common schema types.

App Router: Schema in Layout or Page Component

In the App Router (Next.js 13+), the cleanest pattern is rendering the JSON-LD as a script tag directly in your layout or page component. React renders it into the page output, where Google can crawl it.

Basic pattern in app/page.tsx (or app/blog/[slug]/page.tsx for dynamic routes):

export default function Page() {
  const schema = {
    "@context": "https://schema.org",
    "@type": "Article",
    "headline": "Your Article Title",
    "datePublished": "2026-04-08",
    "author": { "@type": "Person", "name": "Author Name" }
  };

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
      />
      <article>
        {/* your page content */}
      </article>
    </>
  );
}

The dangerouslySetInnerHTML pattern is required because React's normal text rendering escapes characters that would break JSON. Using innerHTML preserves the raw JSON string.

Pages Router: Schema in Head Component

In the older Pages Router, use Next.js's Head component to inject the script tag into the document head:

import Head from 'next/head';

export default function Page({ post }) {
  const schema = {
    "@context": "https://schema.org",
    "@type": "Article",
    "headline": post.title,
    "datePublished": post.publishedAt,
    "author": { "@type": "Person", "name": post.authorName }
  };

  return (
    <>
      <Head>
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
        />
      </Head>
      <article>
        {/* your content */}
      </article>
    </>
  );
}

The schema is now in the page head when Next.js renders it. This works for static generation (getStaticProps), server-side rendering (getServerSideProps), and incremental static regeneration.

Building Schema From CMS Data Dynamically

For sites with content from a headless CMS (Contentful, Sanity, Strapi, Notion), you typically fetch the data at build time or request time and pass it as props. The schema generator becomes a function that takes that data and returns the JSON object.

Example helper function:

function buildArticleSchema(post) {
  return {
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    "headline": post.title,
    "description": post.excerpt,
    "image": post.featuredImage,
    "datePublished": post.publishedAt,
    "dateModified": post.updatedAt,
    "author": {
      "@type": "Person",
      "name": post.author.name,
      "url": post.author.url
    },
    "publisher": {
      "@type": "Organization",
      "name": "Your Site Name",
      "logo": { "@type": "ImageObject", "url": "https://yoursite.com/logo.png" }
    }
  };
}

Call this function in your page component, pass the result to the script tag. Use the same pattern for Product schema (passing product data), FAQ schema (passing question/answer arrays), and any other type.

Sell Custom Apparel — We Handle Printing & Free Shipping

Multiple Schema Types on One Page

A typical content page might have Article schema, FAQ schema, and Breadcrumb schema all together. You can render multiple script tags, one per schema type:

<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }} />
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(faqSchema) }} />
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }} />

Or combine them into a single block using @graph:

const combinedSchema = {
  "@context": "https://schema.org",
  "@graph": [
    articleSchema,
    faqSchema,
    breadcrumbSchema
  ]
};

Both work. The @graph approach is slightly cleaner for pages with many schema types. Multiple separate script tags are easier to debug. Pick whichever you prefer.

Site-Wide Schema in Root Layout

Organization and WebSite schema should be on every page. Put them in the root layout (app/layout.tsx in App Router, _app.tsx or _document.tsx in Pages Router):

// app/layout.tsx
export default function RootLayout({ children }) {
  const orgSchema = {
    "@context": "https://schema.org",
    "@type": "Organization",
    "name": "Your Brand",
    "url": "https://yoursite.com",
    "logo": "https://yoursite.com/logo.png",
    "sameAs": [
      "https://twitter.com/yourbrand",
      "https://linkedin.com/company/yourbrand"
    ]
  };

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

This schema appears on every page automatically. Then individual pages can add their own page-specific schemas in their components.

Validating Schema in Next.js

Next.js renders schema correctly when statically generated or server-side rendered. For pages that are client-rendered, Google may not see the schema (depending on whether Googlebot executes JavaScript for that page).

Best practice: render schema during the initial page generation (getStaticProps, getServerSideProps, or App Router server components) so it's in the initial HTML response. Schema added via client-side useEffect or useState is unreliable for crawling.

Validate by deploying a page and running it through Google's Rich Results Test. If the schema appears in the test results, you're good. If it's missing, the most common cause is rendering it client-side instead of server-side.

For sites with many page types, build a small helper library: schemaHelpers.ts with functions like buildArticleSchema(), buildProductSchema(), buildFAQSchema(). Each takes data and returns a JSON object. Import and call from each page component.

Try It Free — No Signup Required

Runs 100% in your browser. No data is collected, stored, or sent anywhere.

Open Free Schema Markup Generator

Frequently Asked Questions

Should schema go in layout or page component in Next.js App Router?

Both. Site-wide schema (Organization, WebSite) goes in the root layout. Page-specific schema (Article, Product, FAQ) goes in the individual page component. They render together in the final HTML.

Why use dangerouslySetInnerHTML instead of just rendering the JSON as text?

Because React's normal text rendering escapes characters like quotes and ampersands, which breaks JSON syntax. dangerouslySetInnerHTML preserves the raw JSON string. The "dangerously" name is misleading here — for JSON-LD it's the correct, safe approach.

Do I need a Next.js schema package like next-seo?

No. next-seo has helpful wrappers but adds a dependency. For most projects, writing the JSON-LD directly in 5-10 lines of code is simpler and avoids the dependency. Use next-seo if you want a unified API for both meta tags and schema.

Will Google read schema rendered with React?

Yes if the schema is in the initial server-rendered HTML (which it is for App Router server components, getStaticProps, and getServerSideProps). Schema added client-side via useEffect may not be seen by Googlebot reliably.

Can I use the same schema generator for SSG and ISR pages?

Yes. The schema is just a JavaScript object built from page data. Whether the page is statically generated, ISR-rebuilt, or server-side rendered, the schema construction logic is identical.

How do I handle schema for very large sites with thousands of pages?

Build helper functions for each schema type, call them in the page component, pass real data. Static generation handles the bulk — all schema is computed at build time, no runtime overhead. Use ISR for pages that change frequently.

Launch Your Own Clothing Brand — No Inventory, No Risk