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.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add charlesjones-dev/claude-code-plugins-dev --skill seo-schema --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Seo Schema?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/charlesjones-dev-seo-schema)More formats (shields.io, HTML) on the badges page.
---
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": {
"@type": "AggregateRating",
"ratingValue": "4.6",
"reviewCount": "120"
}
}
```
**FAQPage:**
```json
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What is X?",
"acceptedAnswer": { "@type": "Answer", "text": "X is..." }
}
]
}
```
**BreadcrumbList:**
```json
{
"@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):
```json
{
"@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:
- [ ] Context7 mode stated
- [ ] Framework detected; injection code matches framework idiom
- [ ] All output is JSON-LD (no microdata/RDFa)
- [ ] `@context` = `https://schema.org` (HTTPS)
- [ ] `@type` is a valid Schema.org type
- [ ] All required properties present or marked as PLACEHOLDER
- [ ] Dates in ISO 8601 with timezone
- [ ] URLs are absolute
- [ ] Nested entities properly typed (Person, Organization, etc.)
- [ ] No deprecated schema types used
- [ ] User confirmed generation before file writes
- [ ] Validation errors on existing JSON-LD reported with severity
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!