TYPESCRIPTseoadvanced
Next.js 15 Structured Data and Rich Results
Implement dynamic JSON-LD structured data in Next.js 15 App Router with Server Components and Metadata API
Faisal Yaqoob
January 10, 2026
#nextjs#react#schema#seo#structured-data#json-ld#typescript#app-router#server-components#rich-snippets
Code
typescript
1 // components/JsonLd.tsx 2 // Reusable, type-safe JSON-LD component for any schema type 3
4 import type { Thing, WithContext } from 'schema-dts'; 5
6 interface JsonLdProps<T extends Thing> { 7 data: WithContext<T>; 8 } 9
10 export default function JsonLd<T extends Thing>({ data }: JsonLdProps<T>) { 11 return ( 12 <script 13 type="application/ld+json" 14 dangerouslySetInnerHTML={{ 15 __html: JSON.stringify(data), 16 }} 17 /> 18 ); 19 }
Next.js 15 Structured Data and Rich Results
Implement production-ready JSON-LD structured data in Next.js 15 using the App Router, Server Components, and the Metadata API. This modern approach leverages React Server Components for zero-JavaScript schema injection and type-safe schema generation with schema-dts.
Type-Safe Schema Component
// components/JsonLd.tsx
// Reusable, type-safe JSON-LD component for any schema type
import type { Thing, WithContext } from 'schema-dts';
interface JsonLdProps<T extends Thing> {
data: WithContext<T>;
}
export default function JsonLd<T extends Thing>({ data }: JsonLdProps<T>) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(data),
}}
/>
);
}
Dynamic Article Schema with Metadata
// app/blog/[slug]/page.tsx
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import JsonLd from '@/components/JsonLd';
import type { Article, WithContext } from 'schema-dts';
interface BlogPost {
title: string;
description: string;
content: string;
author: string;
publishedAt: string;
updatedAt: string;
image: string;
slug: string;
category: string;
tags: string[];
readTime: number;
}
// Fetch your blog post data
async function getPost(slug: string): Promise<BlogPost | null> {
// Replace with your data source (CMS, database, MDX, etc.)
const res = await fetch(`${process.env.API_URL}/posts/${slug}`, {
next: { revalidate: 3600 }, // ISR: revalidate every hour
});
if (!res.ok) return null;
return res.json();
}
// Generate dynamic metadata
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
if (!post) return {};
const url = `${process.env.NEXT_PUBLIC_SITE_URL}/blog/${slug}`;
return {
title: post.title,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
url,
type: 'article',
publishedTime: post.publishedAt,
modifiedTime: post.updatedAt,
authors: [post.author],
images: [{ url: post.image, width: 1200, height: 630, alt: post.title }],
tags: post.tags,
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.description,
images: [post.image],
},
alternates: {
canonical: url,
},
};
}
// Page component with schema
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL!;
const postUrl = `${siteUrl}/blog/${slug}`;
const articleSchema: WithContext<Article> = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
description: post.description,
image: post.image,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
url: postUrl,
author: {
'@type': 'Person',
name: post.author,
url: `${siteUrl}/about`,
},
publisher: {
'@type': 'Organization',
name: 'Your Site Name',
logo: {
'@type': 'ImageObject',
url: `${siteUrl}/logo.png`,
},
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': postUrl,
},
articleSection: post.category,
keywords: post.tags.join(', '),
wordCount: post.content.split(/\s+/).length,
};
return (
<>
<JsonLd data={articleSchema} />
<article>
<h1>{post.title}</h1>
{/* Your article content */}
</article>
</>
);
}
Multi-Schema Layout (Root Layout)
// app/layout.tsx
import JsonLd from '@/components/JsonLd';
import type { Organization, WebSite, WithContext } from 'schema-dts';
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://yoursite.com';
const organizationSchema: WithContext<Organization> = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'Your Company Name',
url: siteUrl,
logo: `${siteUrl}/logo.png`,
description: 'Your company description for SEO.',
contactPoint: {
'@type': 'ContactPoint',
telephone: '+1-555-123-4567',
contactType: 'customer service',
email: 'hello@yoursite.com',
},
sameAs: [
'https://twitter.com/yourhandle',
'https://github.com/yourusername',
'https://linkedin.com/company/yourcompany',
],
};
const websiteSchema: WithContext<WebSite> = {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'Your Site Name',
url: siteUrl,
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: `${siteUrl}/search?q={search_term_string}`,
},
'query-input': 'required name=search_term_string',
} as any,
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<JsonLd data={organizationSchema} />
<JsonLd data={websiteSchema} />
</head>
<body>{children}</body>
</html>
);
}
Breadcrumb Schema Generator
// lib/schema/breadcrumb.ts
import type { BreadcrumbList, WithContext } from 'schema-dts';
interface BreadcrumbItem {
name: string;
url: string;
}
export function generateBreadcrumbSchema(
items: BreadcrumbItem[]
): WithContext<BreadcrumbList> {
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: items.map((item, index) => ({
'@type': 'ListItem' as const,
position: index + 1,
name: item.name,
item: item.url,
})),
};
}
// Usage in any page:
// const breadcrumbs = generateBreadcrumbSchema([
// { name: 'Home', url: 'https://yoursite.com' },
// { name: 'Blog', url: 'https://yoursite.com/blog' },
// { name: 'Post Title', url: 'https://yoursite.com/blog/post-slug' },
// ]);
FAQ Schema Generator
// lib/schema/faq.ts
import type { FAQPage, WithContext } from 'schema-dts';
interface FAQItem {
question: string;
answer: string;
}
export function generateFAQSchema(
faqs: FAQItem[]
): WithContext<FAQPage> {
return {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqs.map((faq) => ({
'@type': 'Question' as const,
name: faq.question,
acceptedAnswer: {
'@type': 'Answer' as const,
text: faq.answer,
},
})),
};
}
// Usage:
// const faqSchema = generateFAQSchema([
// { question: 'What is Next.js?', answer: 'Next.js is a React framework...' },
// { question: 'Is Next.js free?', answer: 'Yes, Next.js is open source...' },
// ]);
Product Schema for E-Commerce
// lib/schema/product.ts
import type { Product, WithContext } from 'schema-dts';
interface ProductData {
name: string;
description: string;
image: string | string[];
sku: string;
brand: string;
price: number;
currency: string;
availability: 'InStock' | 'OutOfStock' | 'PreOrder';
rating?: number;
reviewCount?: number;
url: string;
}
export function generateProductSchema(
product: ProductData
): WithContext<Product> {
const schema: WithContext<Product> = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
image: product.image,
sku: product.sku,
brand: {
'@type': 'Brand',
name: product.brand,
},
offers: {
'@type': 'Offer',
url: product.url,
priceCurrency: product.currency,
price: product.price,
availability: `https://schema.org/${product.availability}`,
itemCondition: 'https://schema.org/NewCondition',
},
};
if (product.rating && product.reviewCount) {
schema.aggregateRating = {
'@type': 'AggregateRating',
ratingValue: product.rating,
bestRating: 5,
reviewCount: product.reviewCount,
};
}
return schema;
}
Schema Validation Utility
// lib/schema/validate.ts
// Runtime schema validation helper
interface SchemaValidation {
isValid: boolean;
errors: string[];
warnings: string[];
}
export function validateSchema(schema: Record<string, any>): SchemaValidation {
const errors: string[] = [];
const warnings: string[] = [];
// Check required fields
if (!schema['@context']) errors.push('Missing @context');
if (!schema['@type']) errors.push('Missing @type');
// Type-specific validation
if (schema['@type'] === 'Article') {
if (!schema.headline) errors.push('Article missing headline');
if (!schema.author) errors.push('Article missing author');
if (!schema.datePublished) errors.push('Article missing datePublished');
if (!schema.image) warnings.push('Article missing image (recommended)');
if (!schema.dateModified) warnings.push('Article missing dateModified');
}
if (schema['@type'] === 'Product') {
if (!schema.name) errors.push('Product missing name');
if (!schema.offers) errors.push('Product missing offers');
if (!schema.image) warnings.push('Product missing image');
if (!schema.aggregateRating) warnings.push('Product has no ratings');
}
return {
isValid: errors.length === 0,
errors,
warnings,
};
}
Install schema-dts
# Install schema-dts for type-safe schema definitions
npm install schema-dts
# schema-dts provides TypeScript types for all Schema.org types
# No runtime code — types only, zero bundle impact
Best Practices
- Use Server Components — schema is injected at build/server time with zero client-side JavaScript
- Install
schema-dts— provides TypeScript types for all 800+ Schema.org types - Reuse
JsonLdcomponent — single component handles all schema types - Generate metadata and schema together — use the same data source for consistency
- Use ISR (
revalidate) — keep schema data fresh without rebuilding the entire site - Validate at build time — add schema validation to your CI/CD pipeline
- Place schema in
<head>— Next.js Server Components in layout render in the head
Features
- Zero Client JS: Server Components inject schema without JavaScript overhead
- Type-Safe: Full TypeScript support with
schema-dtstypes - Reusable Generators: Modular schema generators for any content type
- Metadata API Integration: Combine schema with Next.js Metadata API
- ISR Compatible: Works with Incremental Static Regeneration
- Multi-Schema Support: Multiple schema types per page via layout composition
Dependencies
- next
- react
- schema-dts
Related Snippets
Schema Markup for Headless CMS (Next.js, Contentful, Sanity)
Implement dynamic Schema.org structured data with headless CMS and React/Next.js
TYPESCRIPTseointermediate
typescriptPreview
// app/components/OrganizationSchema.tsx
export default function OrganizationSchema() {
const schema = {
"@context": "https://schema.org",
...#nextjs#react#headless-cms+6
11/28/2025
View
SoftwareApplication Schema for Apps and Tools
Add SoftwareApplication structured data for apps, plugins, and SaaS tools to display ratings and pricing in Google
JAVASCRIPTseobeginner
javascriptPreview
const webAppSchema = {
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "Schema Markup Generator",
...#software#app#schema+7
1/8/2026
View
HowTo Schema for Step-by-Step Guides
Add HowTo structured data for Google how-to rich results with steps, tools, materials, and estimated time
JAVASCRIPTseobeginner
javascriptPreview
const howToSchema = {
"@context": "https://schema.org",
"@type": "HowTo",
"name": "How to Set Up WordPress on a VPS Server",
...#howto#schema#seo+6
1/5/2026
View