- 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)
1. **ALWAYS use JSON-LD.** Never microdata (`itemscope`/`itemprop`) or RDFa. Google's preferred format.
2. **ALWAYS set `"@context": "https://schema.org"`** — use HTTPS, never HTTP.
3. **ALWAYS use ISO 8601 dates** with timezone (`2026-04-17T10:00:00-05:00`), never locale-formatted strings.
4. **NEVER invent Schema.org types** that don't exist. If uncertain, query Context7 for Schema.org docs or flag for user research.
5. **NEVER emit deprecated schema types** (e.g., removed types listed on schema.org pending/deprecated pages). Cross-check Context7 when available.
6. **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
1. If user provided a path: use it directly.
2. 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">`:
1. Parse the JSON. Flag parse errors.
2. 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)
3. Report each issue with severity (error | warning | info) and remediation.
4. 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:**
1. Generated JSON-LD block
2. Framework-idiomatic injection code
3. List of PLACEHOLDER values to fill
Ask for confirmation before writing.
### Step 7: Framework-Idiomatic Injection
**Next.js (App Router):**
```tsx
// app/blog/[slug]/page.tsx
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:**
```vue
<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:**
```tsx
// src/routes/blog/$slug.tsx
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:**
```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:**
```svelte
<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:**
```tsx
// app/routes/blog.$slug.tsx
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:**
```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:**
```json
{
"@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:**
```json
{
"@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": {
GitHubで見る