Next.js Schema Markup: Server-Side JSON-LD (App Router Guide)
Last Updated: February 25, 2026 · 13 min read
Next.js is the leading React framework for SEO-conscious sites — and it has an excellent JSON-LD story. The App Router renders schema server-side by default, meaning Google sees your structured data in the initial HTML without any JavaScript execution. This guide covers every implementation pattern from sitewide schema in layout.tsx to dynamic route product schemas.
✅ Why Next.js is ideal for schema markup
- • Server Components render JSON-LD in the initial HTML response
- • No "JavaScript SEO" rendering delay — Googlebot sees schema immediately
- •
dangerouslySetInnerHTMLis safe in Server Components (no XSS risk from server-rendered content)
Pattern 1: Sitewide Schema in layout.tsx
For schema that applies to every page (Organization, WebSite, SearchAction), add it to the root layout. This renders once in the <head> on every page:
// app/layout.tsx
import type { Metadata } from 'next';
const organizationSchema = {
'@context': 'https://schema.org',
'@type': 'Organization',
'@id': 'https://yoursite.com/#organization',
'name': 'Your Company Name',
'url': 'https://yoursite.com',
'logo': {
'@type': 'ImageObject',
'url': 'https://yoursite.com/logo.png',
'width': 512,
'height': 512,
},
'sameAs': [
'https://twitter.com/yourhandle',
'https://linkedin.com/company/yourcompany',
],
};
const websiteSchema = {
'@context': 'https://schema.org',
'@type': 'WebSite',
'@id': 'https://yoursite.com/#website',
'name': 'Your Site Name',
'url': 'https://yoursite.com',
'publisher': { '@id': 'https://yoursite.com/#organization' },
'potentialAction': {
'@type': 'SearchAction',
'target': {
'@type': 'EntryPoint',
'urlTemplate': 'https://yoursite.com/search?q={search_term_string}',
},
'query-input': 'required name=search_term_string',
},
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema) }}
/>
</head>
<body>{children}</body>
</html>
);
}Pattern 2: Page-Level Schema in page.tsx
For page-specific schema (Article, Product, FAQPage), add the JSON-LD block directly in the page component. It renders into the page HTML, not the head — which is fine for structured data:
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
// Schema is defined outside or inside the component
const articleSchema = {
'@context': 'https://schema.org',
'@type': 'Article',
'headline': 'Static Page Title',
'datePublished': '2026-02-25T09:00:00Z',
'author': {
'@type': 'Person',
'name': 'Jane Doe',
'url': 'https://yoursite.com/authors/jane',
},
};
export default function BlogPost() {
return (
<>
{/* Place before or after your markup — Google finds it anywhere */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }}
/>
<article>
<h1>My Blog Post</h1>
...
</article>
</>
);
}Pattern 3: Dynamic Schema from Route Params
For dynamic routes (e.g. product pages, blog posts from a CMS), generate schema from the fetched data:
// app/products/[slug]/page.tsx
import { getProduct } from '@/lib/api';
export default async function ProductPage({
params,
}: {
params: { slug: string };
}) {
const product = await getProduct(params.slug);
const productSchema = {
'@context': 'https://schema.org',
'@type': 'Product',
'name': product.name,
'image': product.imageUrl,
'description': product.description,
'sku': product.sku,
'brand': {
'@type': 'Brand',
'name': product.brand,
},
'offers': {
'@type': 'Offer',
'price': product.price.toString(),
'priceCurrency': 'USD',
'availability': product.inStock
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
'url': `https://yoursite.com/products/${params.slug}`,
},
...(product.rating && {
'aggregateRating': {
'@type': 'AggregateRating',
'ratingValue': product.rating.average.toFixed(1),
'reviewCount': product.rating.count,
'bestRating': '5',
},
}),
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(productSchema) }}
/>
<main>
<h1>{product.name}</h1>
...
</main>
</>
);
}Pattern 4: Schema Utility Function
For teams managing many schema types, extract generation into typed utility functions:
// lib/schema.ts
export interface ArticleSchemaInput {
headline: string;
datePublished: string;
dateModified: string;
authorName: string;
authorUrl: string;
description: string;
imageUrl: string;
url: string;
}
export function articleSchema(input: ArticleSchemaInput) {
return {
'@context': 'https://schema.org',
'@type': 'Article',
'headline': input.headline.slice(0, 110), // max 110 chars
'datePublished': input.datePublished,
'dateModified': input.dateModified,
'author': {
'@type': 'Person',
'name': input.authorName,
'url': input.authorUrl,
},
'publisher': {
'@id': 'https://yoursite.com/#organization',
},
'description': input.description,
'image': input.imageUrl,
'mainEntityOfPage': {
'@type': 'WebPage',
'@id': input.url,
},
};
}
// Usage in page.tsx:
// import { articleSchema } from '@/lib/schema';
// const schema = articleSchema({ headline: post.title, ... });Important: dangerouslySetInnerHTML vs next/script
| Approach | Renders SSR? | Recommended for schema? |
|---|---|---|
| dangerouslySetInnerHTML in Server Component | ✅ Yes (inline in HTML) | ✅ Best choice |
| next/script strategy="beforeInteractive" | ✅ Yes | ✅ Also works |
| next/script strategy="afterInteractive" | ⚠️ Client-side only | ❌ Avoid for schema |
| next/script strategy="lazyOnload" | ⚠️ Client-side only | ❌ Avoid for schema |
| useEffect to inject script | ❌ Client-side only | ❌ Never use for schema |
Frequently Asked Questions
Does Next.js Server Components render JSON-LD on the server?▼
Yes. In the App Router, all Server Components (the default) render on the server. Any <script> tag with dangerouslySetInnerHTML inside a Server Component is included in the initial HTML response, meaning Google sees your JSON-LD without executing JavaScript.
Is dangerouslySetInnerHTML safe for schema markup in Next.js?▼
Yes, in Server Components. The XSS risk with dangerouslySetInnerHTML only applies to client-rendered content where untrusted user input could be injected. For schema, the JSON is generated server-side from your own data, making it safe.
Should I put schema markup in the <head> or the <body>?▼
Either location is fine for Google. JSON-LD inside <head> is traditional; placing it in the page <body> (as a sibling to <article> or <main>) is equally valid by the spec and standard Next.js practice. Google processes both.
Can I use next/script for schema markup?▼
Use next/script with strategy="beforeInteractive" or avoid it for schema. The default strategy ("afterInteractive") is client-side only, which means Google may miss it. The safest and simplest approach is dangerouslySetInnerHTML in a Server Component, not next/script.
How do I add schema markup to dynamic Next.js routes?▼
In dynamic route pages (app/products/[slug]/page.tsx), fetch your data server-side then build the schema object from the fetched data before returning JSX. The schema is specific to each page's content and rendered server-side for each request (or at build time for static routes).
What is the best way to reuse schema code across pages?▼
Create typed utility functions in lib/schema.ts that accept page data and return a schema object. Import these functions in each page component. This avoids repetition, ensures consistency, and makes it easy to update schema properties in one place.
Will React hydration affect my schema markup?▼
No. Schema markup rendered by Server Components is static HTML — React hydration only applies to interactive client-side elements. Your JSON-LD blocks are inert <script> tags that React does not need to hydrate.
How do I verify that schema is rendered server-side in Next.js?▼
Right-click your page → View Page Source (not DevTools Elements). Search for "application/ld+json" in the raw HTML. If you can find your schema in the raw source, it is server-rendered. If it only appears in DevTools Elements, it is client-side only.
Test Your Next.js Schema
Paste your JSON-LD or enter your live URL to validate server-side rendering and rich result eligibility.
Validate Schema →Related Guides
What is JSON-LD?
The format used to write structured data
Schema Markup Tutorial
Step-by-step guide to adding schema to any site
Google Rich Results Guide
Which schemas unlock rich result features
Schema Markup Checker
Validate and debug your structured data
JavaScript SEO & Structured Data
How JavaScript affects schema rendering
Schema Markup Audit
Audit all schema across your Next.js site