TYPESCRIPTseointermediate
Schema Markup for Headless CMS (Next.js, Contentful, Sanity)
Implement dynamic Schema.org structured data with headless CMS and React/Next.js
Faisal Yaqoob
November 28, 2025
#nextjs#react#headless-cms#contentful#sanity#strapi#schema#seo#typescript
Code
typescript
1 // app/components/OrganizationSchema.tsx 2 export default function OrganizationSchema() { 3 const schema = { 4 "@context": "https://schema.org", 5 "@type": "Organization", 6 "name": "Your Company Name", 7 "url": process.env.NEXT_PUBLIC_SITE_URL, 8 "logo": `${process.env.NEXT_PUBLIC_SITE_URL}/logo.png`, 9 "description": "Your company description", 10 "contactPoint": { 11 "@type": "ContactPoint", 12 "telephone": "+1-555-123-4567", 13 "contactType": "customer service", 14 "email": "contact@yourcompany.com" 15 }, 16 "sameAs": [ 17 "https://www.facebook.com/yourcompany", 18 "https://www.twitter.com/yourcompany", 19 "https://www.linkedin.com/company/yourcompany", 20 "https://www.instagram.com/yourcompany" 21 ] 22 }; 23
24 return ( 25 <script 26 type="application/ld+json" 27 dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }} 28 /> 29 ); 30 }
Schema Markup for Headless CMS
Implement dynamic Schema.org structured data for headless CMS architectures using Next.js, React, and popular CMS platforms like Contentful, Sanity, and Strapi.
Next.js App Router (Next.js 13+)
Organization Schema Component
// app/components/OrganizationSchema.tsx
export default function OrganizationSchema() {
const schema = {
"@context": "https://schema.org",
"@type": "Organization",
"name": "Your Company Name",
"url": process.env.NEXT_PUBLIC_SITE_URL,
"logo": `${process.env.NEXT_PUBLIC_SITE_URL}/logo.png`,
"description": "Your company description",
"contactPoint": {
"@type": "ContactPoint",
"telephone": "+1-555-123-4567",
"contactType": "customer service",
"email": "contact@yourcompany.com"
},
"sameAs": [
"https://www.facebook.com/yourcompany",
"https://www.twitter.com/yourcompany",
"https://www.linkedin.com/company/yourcompany",
"https://www.instagram.com/yourcompany"
]
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
Dynamic Article Schema with Contentful
// app/blog/[slug]/page.tsx
import { Metadata } from 'next';
import { getArticle } from '@/lib/contentful';
interface ArticleSchemaProps {
article: {
title: string;
description: string;
author: string;
publishedDate: string;
modifiedDate: string;
image: string;
slug: string;
};
}
function ArticleSchema({ article }: ArticleSchemaProps) {
const schema = {
"@context": "https://schema.org",
"@type": "Article",
"headline": article.title,
"description": article.description,
"image": article.image,
"author": {
"@type": "Person",
"name": article.author
},
"publisher": {
"@type": "Organization",
"name": "Your Site Name",
"logo": {
"@type": "ImageObject",
"url": `${process.env.NEXT_PUBLIC_SITE_URL}/logo.png`
}
},
"datePublished": article.publishedDate,
"dateModified": article.modifiedDate,
"mainEntityOfPage": {
"@type": "WebPage",
"@id": `${process.env.NEXT_PUBLIC_SITE_URL}/blog/${article.slug}`
}
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
export default async function ArticlePage({ params }: { params: { slug: string } }) {
const article = await getArticle(params.slug);
return (
<>
<ArticleSchema article={article} />
{/* Your page content */}
</>
);
}
Contentful Integration
// lib/contentful.ts
import { createClient } from 'contentful';
const client = createClient({
space: process.env.CONTENTFUL_SPACE_ID!,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
});
export async function getArticle(slug: string) {
const response = await client.getEntries({
content_type: 'blogPost',
'fields.slug': slug,
limit: 1,
});
if (!response.items.length) {
throw new Error('Article not found');
}
const article = response.items[0];
return {
title: article.fields.title as string,
description: article.fields.excerpt as string,
author: article.fields.author?.fields?.name as string || 'Anonymous',
publishedDate: article.sys.createdAt,
modifiedDate: article.sys.updatedAt,
image: `https:${article.fields.coverImage?.fields?.file?.url}`,
slug: article.fields.slug as string,
content: article.fields.content as string,
};
}
Sanity CMS Integration
// lib/sanity.ts
import { createClient } from '@sanity/client';
const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
apiVersion: '2024-01-01',
useCdn: true,
});
export async function getProduct(slug: string) {
const query = `
*[_type == "product" && slug.current == $slug][0]{
name,
description,
"image": image.asset->url,
price,
currency,
sku,
brand,
inStock,
"rating": coalesce(rating, 0),
"reviewCount": coalesce(reviewCount, 0)
}
`;
return await client.fetch(query, { slug });
}
Product Schema with Sanity
// app/products/[slug]/page.tsx
import { getProduct } from '@/lib/sanity';
interface ProductSchemaProps {
product: {
name: string;
description: string;
image: string;
price: number;
currency: string;
sku: string;
brand: string;
inStock: boolean;
rating: number;
reviewCount: number;
};
slug: string;
}
function ProductSchema({ product, slug }: ProductSchemaProps) {
const schema = {
"@context": "https://schema.org",
"@type": "Product",
"name": product.name,
"description": product.description,
"image": product.image,
"brand": {
"@type": "Brand",
"name": product.brand
},
"sku": product.sku,
"offers": {
"@type": "Offer",
"url": `${process.env.NEXT_PUBLIC_SITE_URL}/products/${slug}`,
"priceCurrency": product.currency,
"price": product.price,
"availability": product.inStock
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock"
},
...(product.reviewCount > 0 && {
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": product.rating,
"reviewCount": product.reviewCount
}
})
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
export default async function ProductPage({ params }: { params: { slug: string } }) {
const product = await getProduct(params.slug);
return (
<>
<ProductSchema product={product} slug={params.slug} />
{/* Your page content */}
</>
);
}
Strapi Integration
// lib/strapi.ts
export async function getStrapiData(endpoint: string) {
const response = await fetch(
`${process.env.NEXT_PUBLIC_STRAPI_URL}/api/${endpoint}`,
{
headers: {
'Authorization': `Bearer ${process.env.STRAPI_API_TOKEN}`,
},
next: { revalidate: 60 } // ISR: revalidate every 60 seconds
}
);
if (!response.ok) {
throw new Error('Failed to fetch data from Strapi');
}
return response.json();
}
export async function getBlogPost(slug: string) {
const data = await getStrapiData(
`blog-posts?filters[slug][$eq]=${slug}&populate=*`
);
if (!data.data || data.data.length === 0) {
throw new Error('Blog post not found');
}
const post = data.data[0];
return {
title: post.attributes.title,
description: post.attributes.description,
content: post.attributes.content,
author: post.attributes.author?.data?.attributes?.name || 'Anonymous',
publishedAt: post.attributes.publishedAt,
updatedAt: post.attributes.updatedAt,
image: post.attributes.coverImage?.data?.attributes?.url
? `${process.env.NEXT_PUBLIC_STRAPI_URL}${post.attributes.coverImage.data.attributes.url}`
: null,
slug: post.attributes.slug,
};
}
Reusable Schema Hook
// hooks/useSchema.ts
import { useMemo } from 'react';
export function useArticleSchema(article: any) {
return useMemo(() => ({
"@context": "https://schema.org",
"@type": "Article",
"headline": article.title,
"description": article.description,
"image": article.image,
"author": {
"@type": "Person",
"name": article.author
},
"publisher": {
"@type": "Organization",
"name": "Your Site Name",
"logo": {
"@type": "ImageObject",
"url": `${process.env.NEXT_PUBLIC_SITE_URL}/logo.png`
}
},
"datePublished": article.publishedDate,
"dateModified": article.modifiedDate,
}), [article]);
}
export function useProductSchema(product: any) {
return useMemo(() => ({
"@context": "https://schema.org",
"@type": "Product",
"name": product.name,
"description": product.description,
"image": product.image,
"brand": {
"@type": "Brand",
"name": product.brand
},
"offers": {
"@type": "Offer",
"priceCurrency": product.currency,
"price": product.price,
"availability": product.inStock
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock"
}
}), [product]);
}
Generic Schema Component
// components/Schema.tsx
interface SchemaProps {
data: Record<string, any>;
}
export default function Schema({ data }: SchemaProps) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
);
}
// Usage
import Schema from '@/components/Schema';
import { useArticleSchema } from '@/hooks/useSchema';
function ArticlePage({ article }: { article: any }) {
const schema = useArticleSchema(article);
return (
<>
<Schema data={schema} />
{/* Your content */}
</>
);
}
FAQ Schema with CMS Data
// components/FAQSchema.tsx
interface FAQItem {
question: string;
answer: string;
}
interface FAQSchemaProps {
faqs: FAQItem[];
}
export default function FAQSchema({ faqs }: FAQSchemaProps) {
const schema = {
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": faqs.map(faq => ({
"@type": "Question",
"name": faq.question,
"acceptedAnswer": {
"@type": "Answer",
"text": faq.answer
}
}))
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
// Fetch FAQs from CMS
export async function getFAQs() {
const data = await getStrapiData('faqs?populate=*');
return data.data.map((item: any) => ({
question: item.attributes.question,
answer: item.attributes.answer
}));
}
Next.js Pages Router (Legacy)
// pages/blog/[slug].tsx
import Head from 'next/head';
import { GetStaticProps, GetStaticPaths } from 'next';
interface ArticlePageProps {
article: {
title: string;
description: string;
author: string;
publishedDate: string;
image: string;
};
}
export default function ArticlePage({ article }: ArticlePageProps) {
const schema = {
"@context": "https://schema.org",
"@type": "Article",
"headline": article.title,
"description": article.description,
"author": {
"@type": "Person",
"name": article.author
},
"datePublished": article.publishedDate,
"image": article.image
};
return (
<>
<Head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
</Head>
{/* Your page content */}
</>
);
}
export const getStaticProps: GetStaticProps = async ({ params }) => {
const article = await getArticle(params?.slug as string);
return {
props: { article },
revalidate: 60 // ISR: revalidate every 60 seconds
};
};
Layout-Level Schema (App Router)
// app/layout.tsx
import OrganizationSchema from '@/components/OrganizationSchema';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<OrganizationSchema />
</head>
<body>{children}</body>
</html>
);
}
TypeScript Schema Types
// types/schema.ts
export interface Organization {
"@context": "https://schema.org";
"@type": "Organization";
name: string;
url: string;
logo: string;
description?: string;
contactPoint?: ContactPoint;
sameAs?: string[];
}
export interface Article {
"@context": "https://schema.org";
"@type": "Article" | "BlogPosting";
headline: string;
description: string;
image: string;
author: Person;
publisher: Organization;
datePublished: string;
dateModified?: string;
mainEntityOfPage?: WebPage;
}
export interface Product {
"@context": "https://schema.org";
"@type": "Product";
name: string;
description: string;
image: string | string[];
brand: Brand;
sku?: string;
offers: Offer;
aggregateRating?: AggregateRating;
}
interface Person {
"@type": "Person";
name: string;
url?: string;
}
interface Brand {
"@type": "Brand";
name: string;
}
interface Offer {
"@type": "Offer";
url?: string;
priceCurrency: string;
price: number | string;
availability: string;
seller?: Organization;
}
interface AggregateRating {
"@type": "AggregateRating";
ratingValue: number;
reviewCount: number;
}
interface ContactPoint {
"@type": "ContactPoint";
telephone: string;
contactType: string;
email?: string;
}
interface WebPage {
"@type": "WebPage";
"@id": string;
}
Metadata API (Next.js 13+)
// app/blog/[slug]/page.tsx
import { Metadata } from 'next';
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
const article = await getArticle(params.slug);
return {
title: article.title,
description: article.description,
openGraph: {
title: article.title,
description: article.description,
images: [article.image],
type: 'article',
publishedTime: article.publishedDate,
authors: [article.author],
},
};
}
Environment Variables
# .env.local
# Contentful
CONTENTFUL_SPACE_ID=your_space_id
CONTENTFUL_ACCESS_TOKEN=your_access_token
# Sanity
NEXT_PUBLIC_SANITY_PROJECT_ID=your_project_id
NEXT_PUBLIC_SANITY_DATASET=production
# Strapi
NEXT_PUBLIC_STRAPI_URL=http://localhost:1337
STRAPI_API_TOKEN=your_api_token
# Site
NEXT_PUBLIC_SITE_URL=https://yoursite.com
Best Practices
- Server Components: Use Server Components (App Router) for automatic schema injection
- Type Safety: Define TypeScript types for all schema structures
- Reusability: Create reusable schema components and hooks
- Environment Variables: Use env vars for site URLs and API endpoints
- ISR/SSG: Leverage Incremental Static Regeneration for performance
- Validation: Validate schema with Google Rich Results Test
- Error Handling: Handle missing CMS data gracefully
- SEO Metadata: Combine schema with Next.js Metadata API
Testing
# Test your schema locally
npm run dev
# Open page and view source (Ctrl+U / Cmd+Option+U)
# Search for "application/ld+json"
# Use Google Rich Results Test
https://search.google.com/test/rich-results
# Use Schema Validator
https://validator.schema.org/
Features
- Type-Safe: Full TypeScript support
- CMS Agnostic: Works with any headless CMS
- Dynamic: Auto-generated from CMS data
- Reusable: Modular components and hooks
- Performance: SSG/ISR optimization
- Modern: Next.js 13+ App Router support
- SEO Optimized: Combine with Metadata API
- Scalable: Enterprise-ready architecture
Dependencies
- react
- next
Related Snippets
Add Schema Markup to Squarespace Website
Implement Schema.org structured data on Squarespace using Code Injection
JAVASCRIPTseobeginner
javascriptPreview
// Add to Settings > Advanced > Code Injection > Header
<script type="application/ld+json">
{
"@context": "https://schema.org",
...#squarespace#schema#seo+2
11/28/2025
View
Add Schema Markup to Webflow Website
Implement Schema.org structured data in Webflow using Custom Code
JAVASCRIPTseobeginner
javascriptPreview
// Add to Page Settings > Custom Code > Head Code
<script type="application/ld+json">
{
"@context": "https://schema.org",
...#webflow#schema#seo+3
11/28/2025
View
Add Schema Markup to Weebly Website
Implement Schema.org structured data on Weebly using Embed Code
JAVASCRIPTseobeginner
javascriptPreview
// Add using Embed Code element in header or footer
<script type="application/ld+json">
{
"@context": "https://schema.org",
...#weebly#schema#seo+2
11/28/2025
View