Introduction
As developers, we spend countless hours optimising our applications for performance, accessibility, and user experience. Yet there’s one optimization that often gets overlooked: structured data. It’s the secret sauce that helps search engines and AI systems understand your content, but implementing it correctly has traditionally been painful.
In this article, we’ll explore how to add type-safe JSON-LD structured data to your Next.js applications using Schema Sentry—a library designed to make structured data generation as easy as writing TypeScript.
What is Structured Data and Why Should You Care?
Structured data is a standardised format (JSON-LD) that describes your page content to machines. Think of it as metadata that tells Google, Bing, ChatGPT, and other systems:
Real-World Impact
Without structured data, search engines guess. With it, they know. This leads to:
Rich search results - Articles with publication dates, products with prices, FAQs with expandable answers
Better AI visibility - ChatGPT, Claude, and Perplexity use structured data to cite and recommend content
Higher click-through rates - Rich snippets stand out in search results
The Traditional Approach (And Its Problems)
Here’s how most developers add structured data today:
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "Article",
"headline": "My Blog Post",
"author": {
"@type": "Person",
"name": "Jane Doe"
},
"datePublished": "2026-02-10"
})
}}
/>
What’s Wrong With This?
No type safety - Typo in "@type": "Aticle"? Good luck finding it
No validation - Invalid schema? You’ll only know when Google rejects it
Maintenance nightmare - Change a field name? Update it everywhere manually
No autocomplete - IDE can’t help you remember schema.org field names
These aren’t just annoyances—they directly impact your SEO and AI discoverability.
Introducing Schema Sentry
Schema Sentry solves these problems by providing type-safe builders for JSON-LD structured data. It works with both Next.js App Router and Pages Router.
Installation
# Using npm
npm install @schemasentry/next
npm install -D @schemasentry/cli
# Using pnpm
pnpm add @schemasentry/next
pnpm add -D @schemasentry/cli
# Using yarn
yarn add @schemasentry/next
yarn add -D @schemasentry/cli
Basic Usage
Let’s start with a simple example—adding Article schema to a blog post:
App Router (Next.js 13+)
// app/blog/[slug]/page.tsx
import { Schema, Article, BreadcrumbList } from "@schemasentry/next";
export default function BlogPost({ params }: { params: { slug: string } }) {
const article = Article({
headline: "Getting Started with Next.js",
authorName: "Jane Doe",
datePublished: "2026-02-10",
dateModified: "2026-02-10",
url: `https://example.com/blog/${params.slug}`,
description: "Learn Next.js from scratch",
image: "https://example.com/images/nextjs.jpg"
});
const breadcrumbs = BreadcrumbList({
items: [
{ name: "Home", url: "https://example.com" },
{ name: "Blog", url: "https://example.com/blog" },
{ name: "Getting Started", url: `https://example.com/blog/${params.slug}` }
]
});
return (
<>
<Schema data={[article, breadcrumbs]} />
<article>
{/* Your content here */}
</article>
</>
);
}
Pages Router (Classic Next.js)
// pages/blog/[slug].tsx
import Head from "next/head";
import { Schema, Article, BreadcrumbList } from "@schemasentry/next";
export default function BlogPost() {
const article = Article({
headline: "Getting Started with Next.js",
authorName: "Jane Doe",
datePublished: "2026-02-10",
url: "https://example.com/blog/post",
description: "Learn Next.js from scratch"
});
return (
<>
<Head>
<title>Getting Started - My Blog</title>
</Head>
<Schema data={article} />
<article>{/* content */}</article>
</>
);
}
Key difference: App Router doesn’t need next/head because it supports native metadata API.
Supported Schema Types
Schema Sentry includes builders for the most commonly used schema.org types:
1. Organization
Perfect for company information that appears on multiple pages:
import { Organization } from "@schemasentry/next";
const org = Organization({
name: "Acme Corporation",
url: "https://acme.com",
logo: "https://acme.com/logo.png",
description: "Building the future of web development",
sameAs: [
"https://twitter.com/acme",
"https://linkedin.com/company/acme",
"https://github.com/acme"
]
});
2. Product (E-commerce)
Essential for any e-commerce site:
import { Product } from "@schemasentry/next";
const product = {
...Product({
name: "Premium Widget",
description: "The ultimate productivity tool",
url: "https://acme.com/products/widget",
image: "https://acme.com/images/widget.jpg",
brandName: "Acme",
sku: "WIDGET-001"
}),
offers: {
"@type": "Offer",
price: "99.00",
priceCurrency: "USD",
availability: "https://schema.org/InStock"
},
aggregateRating: {
"@type": "AggregateRating",
ratingValue: "4.8",
reviewCount: "1247"
}
};
3. FAQPage
Great for help sections and documentation:
import { FAQPage } from "@schemasentry/next";
const faq = FAQPage({
questions: [
{
question: "What is Schema Sentry?",
answer: "A type-safe library for generating JSON-LD structured data."
},
{
question: "Does it work with Pages Router?",
answer: "Yes! It works with both App Router and Pages Router."
}
]
});
4. HowTo
Perfect for tutorials and step-by-step guides:
import { HowTo } from "@schemasentry/next";
const howto = HowTo({
name: "Install Schema Sentry",
steps: [
{
name: "Install packages",
text: "Run npm install @schemasentry/next"
},
{
name: "Import components",
text: "Import Schema and builders from the package"
},
{
name: "Use in your pages",
text: "Add the Schema component to your page components"
}
]
});
5. BreadcrumbList
Helps search engines understand your site structure:
import { BreadcrumbList } from "@schemasentry/next";
const breadcrumbs = BreadcrumbList({
items: [
{ name: "Home", url: "https://example.com" },
{ name: "Products", url: "https://example.com/products" },
{ name: "Widgets", url: "https://example.com/products/widgets" }
]
});
Combining Multiple Schemas
You can combine multiple schema types on a single page:
import { Schema, Organization, Product, BreadcrumbList } from "@schemasentry/next";
export default function ProductPage() {
const org = Organization({
name: "Acme Corp",
url: "https://acme.com"
});
const product = Product({
name: "Premium Widget",
description: "...",
url: "...",
brandName: "Acme"
});
const breadcrumbs = BreadcrumbList({
items: [
{ name: "Home", url: "..." },
{ name: "Products", url: "..." },
{ name: "Premium Widget", url: "..." }
]
});
return (
<>
{/* Pass an array of schemas */}
<Schema data={[org, product, breadcrumbs]} />
{/* Page content */}
</>
);
}
CI/CD Validation
Here’s where Schema Sentry really shines. You can validate your structured data as part of your CI/CD pipeline.
Step 1: Create a Manifest
Create schema-sentry.manifest.json at your project root:
{
"routes": {
"/": ["Organization", "WebSite"],
"/blog": ["WebSite"],
"/blog/getting-started": ["Article"],
"/blog/typescript-tips": ["Article"],
"/products/widget": ["Organization", "Product"],
"/faq": ["FAQPage", "Organization"],
"/howto/installation": ["HowTo", "Organization"]
}
}
This manifest defines which schema types you expect on each route.
Step 2: Add CLI Commands
Add these scripts to your package.json:
{
"scripts": {
"schema:validate": "schemasentry validate --manifest ./schema-sentry.manifest.json --data ./schema-sentry.data.json",
"schema:collect": "schemasentry collect --root ./.next/server/app --output ./schema-sentry.data.collected.json",
"schema:drift": "schemasentry collect --root ./.next/server/app --check --data ./schema-sentry.data.json"
}
}
Step 3: Add to GitHub Actions
Create .github/workflows/schema-check.yml:
name: Schema Validation
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build application
run: pnpm build
- name: Validate schema
run: pnpm schema:validate
Now your CI will fail if:
A page is missing expected schema
Schema types don’t match the manifest
Invalid JSON-LD is generated
This prevents broken structured data from ever reaching production.
Why This Matters for AI
Structured data isn’t just for traditional SEO anymore. AI systems like ChatGPT, Claude, and Perplexity actively use structured data to:
Understand content - Know what your page is about without guessing
Generate accurate citations - Reference your content correctly in responses
Recommend your pages - Include your content in AI-generated answers
Without proper schema, your content is essentially invisible to the AI discovery layer. This is becoming increasingly important as more users turn to AI assistants for information.
Performance Considerations
Schema Sentry has zero runtime overhead. All schema generation happens at build time, and the component simply injects a tag with the JSON-LD. The resulting HTML is identical to hand-written JSON-LD—just without the errors.
Complete Working Example
Let’s put it all together with a complete product page:
// pages/products/[id].tsx or app/products/[id]/page.tsx
import Head from "next/head";
import { Schema, Organization, Product, BreadcrumbList } from "@schemasentry/next";
export default function ProductPage({ product }: { product: any }) {
const org = Organization({
name: "Acme Store",
url: "https://store.acme.com",
logo: "https://store.acme.com/logo.png"
});
const productSchema = {
...Product({
name: product.name,
description: product.description,
url: `https://store.acme.com/products/${product.id}`,
image: product.image,
brandName: product.brand,
sku: product.sku
}),
offers: {
"@type": "Offer",
price: product.price,
priceCurrency: "USD",
availability: product.inStock
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock"
},
aggregateRating: product.rating ? {
"@type": "AggregateRating",
ratingValue: product.rating.value,
reviewCount: product.rating.count
} : undefined
};
const breadcrumbs = BreadcrumbList({
items: [
{ name: "Home", url: "https://store.acme.com" },
{ name: "Products", url: "https://store.acme.com/products" },
{ name: product.name, url: `https://store.acme.com/products/${product.id}` }
]
});
return (
<>
<Head>
<title>{product.name} - Acme Store</title>
<meta name="description" content={product.description} />
</Head>
<Schema data={[org, productSchema, breadcrumbs]} />
<div className="product-page">
{/* Product display */}
</div>
</>
);
}
Best Practices
Always include Organization schema on key pages to establish site ownership
Use BreadcrumbList on deep pages to help search engines understand navigation
Keep Article dates updated when you modify content (use dateModified)
Test with Google’s Rich Results Test before deploying
Validate in CI/CD to catch errors early
Conclusion
Adding structured data doesn’t have to be painful. With Schema Sentry, you get:
✅ Full TypeScript autocomplete and type safety
✅ Compile-time validation
✅ Zero runtime overhead
✅ Works with both App Router and Pages Router
✅ CI/CD integration for automated validation
Your future self—and your search rankings—will thank you.
Have you implemented structured data on your Next.js site? What challenges did you face? Share your experience in the comments !
Summary
Adding structured data to Next.js applications can significantly improve SEO, AI visibility, and search result quality. This guide demonstrated how Schema Sentry enables type-safe JSON-LD generation, supports both App Router and Pages Router, provides multiple schema builders, integrates with CI/CD pipelines, and ensures zero runtime overhead while preventing structured data errors from reaching production.