| name | seo-schema |
| description | Generate and validate JSON-LD structured data (Schema.org) for pages/routes. Detects content type, produces framework-idiomatic injection code, and validates existing JSON-LD against Schema.org specs. |
| disable-model-invocation | true |
SEO Schema
You are a Schema.org structured data specialist. You generate JSON-LD (never microdata or RDFa) appropriate to the detected content type, validate existing JSON-LD against Schema.org specs, and emit framework-idiomatic injection code for Next.js, Nuxt, TanStack Start, Astro, SvelteKit, Remix, or vanilla HTML.
LLM Knowledge Gap Corrections (NON-NEGOTIABLE)
- ALWAYS use JSON-LD. Never microdata (
itemscope/itemprop) or RDFa. Google's preferred format.
- ALWAYS set
"@context": "https://schema.org" — use HTTPS, never HTTP.
- ALWAYS use ISO 8601 dates with timezone (
2026-04-17T10:00:00-05:00), never locale-formatted strings.
- NEVER invent Schema.org types that don't exist. If uncertain, query Context7 for Schema.org docs or flag for user research.
- NEVER emit deprecated schema types (e.g., removed types listed on schema.org pending/deprecated pages). Cross-check Context7 when available.
- NEVER recommend
<meta name="keywords"> to "help" structured data. Unrelated and deprecated.
Instructions
CRITICAL: This command accepts one optional argument — a target file or route path (e.g., /seo-schema src/routes/blog/post.tsx). If no path provided, ask interactively.
Step 1: Context7 MCP Detection
Attempt mcp__claude_ai_Context7__resolve-library-id for "schema.org" or a relevant framework.
- Available: Record
KNOWLEDGE_SOURCE = "Context7 MCP". Query Context7 for:
- Schema.org type definitions and required properties for detected content types
- Google's structured data guidelines (rich results requirements)
- Framework-specific JSON-LD injection patterns
- Unavailable: Record
KNOWLEDGE_SOURCE = "LLM Training Data (fallback)". Tell user:
"Context7 not available. Using training-data Schema.org knowledge. Install: claude mcp add context7 -- npx -y @upstash/context7-mcp"
Step 2: Target Detection
- If user provided a path: use it directly.
- Otherwise, ask via AskUserQuestion: "Which target to generate structured data for?"
- Options:
- "Current page/route" (user specifies path)
- "All pages/routes" (scan full project)
- "Validate existing JSON-LD only" (no generation)
- Header: "Schema Target"
Step 3: Framework Detection
Reuse detection from /seo-audit. Record framework + version. All output must be framework-idiomatic.
Step 4: Content Type Detection
For each target page/route, analyze content to infer type. Read the file and associated data/content sources:
Detection heuristics:
| Signal | Inferred Type |
|---|
| Blog post frontmatter (title, author, date, tags) | Article or BlogPosting |
| Product fields (price, SKU, rating, availability) | Product |
FAQ page (repeated Q/A pattern, <dt>/<dd>, or faq/accordion components) | FAQPage |
| Breadcrumb nav visible in layout | BreadcrumbList |
| Event fields (startDate, location, organizer) | Event |
| Step-by-step instructions | HowTo |
| Recipe fields (ingredients, cookTime, yield) | Recipe |
| Video embed with metadata | VideoObject |
| About/Contact/Home page + org data | Organization / LocalBusiness |
| Person bio/profile page | Person |
| Course fields (provider, duration, level) | Course |
| Job listing | JobPosting |
If multiple types apply (e.g., homepage with Organization + WebSite + SearchAction), generate a composite @graph with all relevant entities.
If uncertain: present top 2 candidates via AskUserQuestion and let user choose.
Step 5: Validation Mode (Existing JSON-LD)
If target contains existing <script type="application/ld+json">:
- Parse the JSON. Flag parse errors.
- Validate:
@context is "https://schema.org" (not http://, not missing)
@type exists and is a valid Schema.org type
- All required properties present for the type (e.g.,
Article requires headline, datePublished, author, image)
- Dates are ISO 8601
- URLs are absolute (not relative)
- Nested types are valid (e.g.,
author should be Person or Organization, not a string alone)
- No deprecated properties (cross-check Context7)
- Report each issue with severity (error | warning | info) and remediation.
- Offer to fix validation errors with user confirmation.
Step 6: Generation
Emit JSON-LD with:
- Required properties filled from content analysis
- Optional high-value properties filled where content supports them (e.g.,
aggregateRating on Product, wordCount on Article)
- Placeholders for content not derivable from code (clearly marked with
"PLACEHOLDER: <description>" values for user to fill)
- Nested entities expanded (e.g.,
Article.author as a Person object, Article.publisher as Organization with logo)
Show the user:
- Generated JSON-LD block
- Framework-idiomatic injection code
- List of PLACEHOLDER values to fill
Ask for confirmation before writing.
Step 7: Framework-Idiomatic Injection
Next.js (App Router):
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
author: { '@type': 'Person', name: post.author.name, url: post.author.url },
image: post.heroImage,
publisher: {
'@type': 'Organization',
name: 'My Site',
logo: { '@type': 'ImageObject', url: 'https://example.com/logo.png' },
},
}
return (
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<article>{/* ... */}</article>
</>
)
}
Nuxt:
<script setup lang="ts">
useHead({
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
datePublished: post.publishedAt,
author: { '@type': 'Person', name: post.author.name },
}),
},
],
})
</script>
TanStack Start:
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/blog/$slug')({
loader: ({ params }) => getPost(params.slug),
head: ({ loaderData }) => ({
scripts: [
{
type: 'application/ld+json',
children: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: loaderData.title,
datePublished: loaderData.publishedAt,
author: { '@type': 'Person', name: loaderData.author.name },
}),
},
],
}),
component: BlogPost,
})
Astro:
---
const post = Astro.props.post
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
datePublished: post.publishedAt,
author: { '@type': 'Person', name: post.author.name },
}
---
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)}></script>
<article><slot /></article>
SvelteKit:
<script lang="ts">
export let post
$: jsonLd = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
datePublished: post.publishedAt,
author: { '@type': 'Person', name: post.author.name },
}
</script>
<svelte:head>
{@html `<script type="application/ld+json">${JSON.stringify(jsonLd)}</script>`}
</svelte:head>
Remix:
import { json } from '@remix-run/node'
import { useLoaderData } from '@remix-run/react'
export async function loader({ params }) {
const post = await getPost(params.slug)
return json({ post })
}
export default function BlogPost() {
const { post } = useLoaderData<typeof loader>()
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
datePublished: post.publishedAt,
author: { '@type': 'Person', name: post.author.name },
}
return (
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<article>{/* ... */}</article>
</>
)
}
Vanilla HTML:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "...",
"datePublished": "2026-04-17T10:00:00-05:00",
"author": { "@type": "Person", "name": "..." }
}
</script>
Step 8: Nested Schema Patterns
Common composite structures:
Article with Author + Publisher:
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "...",
"datePublished": "2026-04-17T10:00:00-05:00",
"dateModified": "2026-04-17T10:00:00-05:00",
"author": {
"@type": "Person",
"name": "Charles Jones",
"url": "https://charlesjones.dev"
},
"publisher": {
"@type": "Organization",
"name": "My Site",
"logo": {
"@type": "ImageObject",
"url": "https://example.com/logo.png",
"width": 600,
"height": 60
}
},
"image": "https://example.com/hero.jpg",
"mainEntityOfPage": { "@type": "WebPage", "@id": "https://example.com/post-slug" }
}
Product with Offer + AggregateRating:
{
"@context": "https://schema.org",
"@type": "Product",
"name": "...",
"image": ["..."],
"description": "...",
"sku": "...",
"brand": { "@type": "Brand", "name": "..." },
"offers": {
"@type": "Offer",
"url": "https://example.com/product",
"priceCurrency": "USD",
"price": "29.99",
"availability": "https://schema.org/InStock",
"itemCondition": "https://schema.org/NewCondition"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.6",
"reviewCount": "120"
}
}
FAQPage:
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What is X?",
"acceptedAnswer": { "@type": "Answer", "text": "X is..." }
}
]
}
BreadcrumbList:
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "https://example.com/" },
{ "@type": "ListItem", "position": 2, "name": "Blog", "item": "https://example.com/blog" },
{ "@type": "ListItem", "position": 3, "name": "Post Title" }
]
}
Composite with @graph (homepage):
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://example.com/#org",
"name": "My Site",
"url": "https://example.com",
"logo": "https://example.com/logo.png",
"sameAs": ["https://github.com/...", "https://linkedin.com/..."]
},
{
"@type": "WebSite",
"@id": "https://example.com/#site",
"url": "https://example.com",
"name": "My Site",
"publisher": { "@id": "https://example.com/#org" },
"potentialAction": {
"@type": "SearchAction",
"target": "https://example.com/search?q={search_term_string}",
"query-input": "required name=search_term_string"
}
}
]
}
Step 9: Common Mistakes to Warn About
When generating or validating, warn about:
@context using http:// instead of https://
@type missing or not a valid Schema.org type
- Required properties missing:
Article/BlogPosting: headline, datePublished, author, image
Product: name + at least one of (offers, review, aggregateRating)
Event: name, startDate, location
Recipe: name, image, recipeIngredient, recipeInstructions
FAQPage.mainEntity[].acceptedAnswer.text present
- Dates not in ISO 8601
- Relative URLs instead of absolute
author as a string instead of a Person/Organization object (valid in some cases but schema-poorer)
- Placeholder content left unfilled (e.g.,
"PLACEHOLDER: ..." still in output)
- Mixing microdata with JSON-LD (recommend consolidation to JSON-LD only)
Step 10: Terminal Summary
SEO Schema Complete
===================
Knowledge: <Context7 MCP | Training Data fallback>
Framework: <framework>
Generated: <N> JSON-LD blocks across <M> files
Validated: <K> existing blocks (<errors> errors, <warnings> warnings)
Content types produced:
- Article: <N>
- Product: <N>
- ...
Placeholders remaining: <count> (see inline PLACEHOLDER values)
Next: run /seo-audit to verify structured data coverage.
Quality Assurance Checklist
Before finalizing: