| name | structured-data |
| description | Schema.org structured data and JSON-LD implementation for Next.js. This skill should be used when the user asks about "structured data", "JSON-LD", "Schema.org", "rich snippets", "rich results", "schema markup", "Organization schema", "Article schema", "Product schema", "BreadcrumbList", "FAQ schema", or needs to add structured data to improve search appearance. |
Structured Data (Schema.org / JSON-LD)
Structured data tells search engines what your content means, not just what it says. Google uses it to generate rich results (star ratings, product prices, breadcrumb trails, FAQ dropdowns). Use JSON-LD format โ Google's recommended approach.
Setup
Install TypeScript types for Schema.org (zero bundle impact โ types only):
pnpm add -D schema-dts
Reusable JSON-LD Component
Create a type-safe component for rendering structured data:
import type { Thing, WithContext } from 'schema-dts'
export function JsonLd<T extends Thing>({ data }: { data: WithContext<T> }) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(data).replace(/</g, '\\u003c'),
}}
/>
)
}
The .replace(/</g, '\\u003c') sanitizes the output to prevent XSS via </script> injection.
Schema Factory Functions
Create reusable factory functions in a utility file:
import type {
Article,
BreadcrumbList,
Organization,
Product,
WebSite,
WithContext,
} from 'schema-dts'
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://yourdomain.com'
export function createOrganization(org: {
name: string
logo: string
url: string
description?: string
sameAs?: string[]
}): WithContext<Organization> {
return {
'@context': 'https://schema.org',
'@type': 'Organization',
'@id': `${org.url}/#organization`,
name: org.name,
url: org.url,
logo: { '@type': 'ImageObject', url: org.logo },
description: org.description,
sameAs: org.sameAs,
}
}
export function createWebSite(): <> {
{
: ,
: ,
url,
name,
: {
: ,
: ,
} <>[],
}
}
(): <> {
{
: ,
: ,
: items.( ({
: ,
: index + ,
: item.,
: item.,
})),
}
}
(): <> {
{
: ,
: ,
: article.,
: article.,
: article.,
: article.,
: article. || article.,
: { : , : article.., : article.. },
: { : , : article. },
}
}
(): <> {
{
: ,
: ,
: product.,
: product.,
: product.,
: product.,
: product. ? { : , : product. } : ,
: {
: ,
: product.,
: product.,
: ,
: product.,
},
: product.
? {
: ,
: product.,
: product.,
}
: ,
}
}
Usage in Pages
Sitewide Schema (Root Layout)
Place Organization and WebSite schema in the root layout โ they apply to every page:
import { JsonLd } from '@/components/json-ld'
import { createOrganization, createWebSite } from '@/lib/schema'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<JsonLd
data={createOrganization({
name: 'Your Company',
logo: 'https://yourdomain.com/logo.png',
url: 'https://yourdomain.com',
sameAs: [
'https://twitter.com/yourcompany',
'https://linkedin.com/company/yourcompany',
],
})}
/>
<JsonLd data={createWebSite('https://yourdomain.com', 'Your Company')} />
{children}
</body>
</html>
)
}
Article Page
import { JsonLd } from '@/components/json-ld'
import { createArticle, createBreadcrumbList } from '@/lib/schema'
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getPost(slug)
return (
<>
<JsonLd
data={createArticle({
headline: post.title,
description: post.excerpt,
image: post.image,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
author: { name: post.author.name, url: `/authors/${post.author.slug}` },
url: `https://yourdomain.com/blog/${slug}`,
})}
/>
<JsonLd
data={createBreadcrumbList([
{ '', '//' },
{ '', '///' },
{ , `////${}` },
])}
/>
{/* page content */}
)
}
@graph Pattern for Multiple Schemas
Combine multiple schemas in a single <script> tag:
const schemas = {
'@context': 'https://schema.org',
'@graph': [
createOrganization({ name: 'Acme', logo: '/logo.png', url: 'https://acme.com' }),
createWebSite('https://acme.com', 'Acme'),
],
}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemas).replace(/</g, '\\u003c') }}
/>
Rich Results Impact (2026)
| Schema Type | Rich Result | CTR Lift | Use On |
|---|
| Organization | Knowledge panel | Brand queries | Root layout |
| WebSite + SearchAction | Sitelinks searchbox | Brand queries | Root layout |
| BreadcrumbList | Breadcrumb trail in SERP | +5-10% | All pages |
| Article | Article snippets | High | Blog/news posts |
| Product + Offer | Price, stock, ratings | +30-35% | Product pages |
| HowTo | Step display (mobile only) | +15-25% | Tutorial pages |
| Event | Event listing | +20-25% | Event pages |
FAQPage is restricted to government and health websites since 2023. Do not implement it for regular websites โ the rich result will not appear.
Validation
Always validate structured data before deploying:
- Google Rich Results Test: https://search.google.com/test/rich-results โ test by URL or paste code
- Schema.org Validator: https://validator.schema.org/ โ syntax validation
- Google Search Console โ Enhancements โ monitor after deployment (2-4 weeks for results)
For complete JSON-LD examples covering all schema types, see references/schema-examples.md.
Common Mistakes
| Mistake | Fix |
|---|
Using FAQPage on a regular website | Restricted to gov/health since 2023 โ remove it |
Missing @context field | Always include '@context': 'https://schema.org' |
| Two identical schema types on same page | Combine into one or use @graph |
| Hardcoded dates | Use dynamic dates from CMS/database |
| Missing sanitization | Always use .replace(/</g, '\\u003c') |
| Schema doesn't match visible content | Google penalizes mismatched schema and page content |