How to Set Up Codex for Schema Markup (Full SKILL.md Included)

Key takeaways

What you get from this article A Codex skill that scans any page's structured data, tells you exactly what's there, which required properties are missing, and what's deprecated, then writes you ready...

What you get from this article

A Codex skill that scans any page's structured data, tells you exactly what's there, which required properties are missing, and what's deprecated, then writes you ready-to-paste JSON-LD for the page type you actually have. One small script, no API keys, and the judgment tables match Google's published type status as of June 2026.

Who this is for: anyone whose site has schema already but no idea if it's right, and anyone adding a site section (a product catalog, a blog, a local business page) that now needs markup. Also for the person who gets screenshots in Slack saying "why is my rich result not showing" and wants a repeatable answer instead of a guess.

Time: 10 minutes to install and verify, 5 to inspect an existing page, 20 to write the missing JSON-LD.

Definition of done: you run $codex-seo-schema <url> in Codex (or paste a URL and ask about its structured data) and get a table: which schema types are present, their status, and the exact issues. Then, if you ask for it, a corrected JSON-LD block you can paste into your template.

Why schema is the last well-defined SEO job

Structured data is one of the few parts of SEO where the rules are published, explicit, and testable. There is a spec, there's a validator, and Google publishes what it supports and when it kills something. No other ranking lever behaves that way: your content quality, links, and user experience are debated until judgment is exhausted, but schema is a checklist that either passes or doesn't.

That's also why it's the SEO task that feels most like programming. It deserves a scanner, not vibes.

Two traps to keep in mind, because the 2024-2026 cleanup is the reason this skill exists:

  • The type list changed a lot. FAQPage lost rich results for all sites on May 7, 2026. HowTo and SpecialAnnouncement and the June 2025 batch (CourseInfo, EstimatedSalary, LearningVideo, ClaimReview, VehicleListing) are dead or dying, and Practice Problem support was removed in January 2026. Plenty of general advice you'll find online was written before all of that.
  • The reverse trap exists too: Dataset markup is alive for Google Dataset Search, but has no Search rich-result surface. Don't rip it out because someone read "Dataset killed."

The status table in the skill is the thing you'll keep coming back to. It's organized as active types, the keep-if-useful cases, the deprecated list with dates and replacements, and a short "do not flag" section so the scanner doesn't become a false alarm factory.

How the skill works

Detection and validation in one scan

The scanner fetches the page and looks for all three markup formats: JSON-LD blocks, Microdata (itemscope), and RDFa (typeof). JSON-LD will always be the recommendation, because that's Google's stated preference, but the scan reports what's there rather than what you should have.

Then it validates each JSON-LD block on the checks that actually cause rich results to fail:

  • @context present
  • Required properties per type (Article needs headline, author, datePublished; Product needs name, offers; LocalBusiness needs name and address)
  • Absolute URLs (a relative url or logo silently breaks markup)
  • Dates in YYYY-MM-DD format or valid ISO 8601
  • No leftover placeholder text in a production snippet
  • No deprecated types

The judgment table

The skill ships the type status table as of June 2026. Active and recommendable: Organization, LocalBusiness, SoftwareApplication, Product, Article, BlogPosting, NewsArticle, Review, AggregateRating, BreadcrumbList, WebSite, Person, VideoObject, JobPosting, Course, and the others in the full list. Deprecated with replacements: HowTo (none), SpecialAnnouncement (none), ClaimReview (none), VehicleListing (Product), LearningVideo (VideoObject), EstimatedSalary (JobPosting with baseSalary), CourseInfo carousel (single Course card), PracticeProblem (none), Book Actions (none). FAQPage sits alone: keep an existing one (flag it Info, not Critical), never add one for SERP benefit.

Generation rules

When it writes markup for you:

  1. Identify the page type from what the page actually is
  2. Pick the type or types; several at once is fine (Organization plus Person per team member on an about page)
  3. Write valid JSON-LD with all required and recommended properties
  4. Include only truthful data. Anything the user must supply stays a marked placeholder like [Company Name]
  5. Validate the result before handing it over
  6. On review markup: no fake reviews, ever, and no undisclosed incentivized reviews. If the business gives free products for reviews, the disclosure has to be on the page or the markup doesn't go in.

The skill carries templates for the three types you'll use most: Organization, LocalBusiness, and Article/BlogPosting.

The full SKILL.md

Copy this exact file to ~/.codex/skills/codex-seo-schema/SKILL.md.

md
---
name: codex-seo-schema
description: Use when the user gives a URL or markup and asks about schema, JSON-LD, structured data, rich results, "is my markup right", "what schema should I add", or schema generation for a page type. Triggers on "schema check", "structured data audit", "validate JSON-LD", "add schema to my page", "why is my markup not showing".
---
# Schema Markup Analysis & Generation

Detect, validate, and generate Schema.org structured data. JSON-LD is the
primary format (Google's stated preference), but Microdata and RDFa still
exist in the wild and this skill checks for all three.

## Run

```bash
python3 ~/.codex/skills/codex-seo-schema/scripts/schema_scan.py <url> [--json]
```

The scanner returns, per JSON-LD block: the @types found, missing required
properties, relative URLs, invalid date formats, invalid JSON, plus a summary
of Microdata/RDFa presence and any deprecated type hits.

## Validation checklist

- `@context` present (`https://schema.org`)
- `@type` is a valid, supported type
- Required properties per type (Article needs headline, author, datePublished;
  Product needs name, offers; LocalBusiness needs name, address; etc.)
- All URLs absolute (relative `url`/`logo`/`image` are invalid for rich results)
- Dates in `YYYY-MM-DD` (or a valid ISO 8601 with time)
- No placeholder text like "TBD" or lorem ipsum left in a production snippet
- No deprecated types (see the status table below)

## Type status (as of June 2026)

### Active, recommend freely

Organization, LocalBusiness, SoftwareApplication, WebApplication, Product
(with Certification markup since April 2025), ProductGroup, Offer, Service,
Article, BlogPosting, NewsArticle, Review, AggregateRating, BreadcrumbList,
WebSite, WebPage, Person, ProfilePage, ContactPage, VideoObject, ImageObject,
Event, JobPosting, Course, DiscussionForumPosting. Video and specialized
types (BroadcastEvent, Clip, SeekToAction, SoftwareSourceCode) are also fine.

### No rich results, keep if useful

- **FAQPage**: rich results retired for ALL sites on 2026-05-07 (this
  supersedes the 2023 government/health restriction). No Google SERP benefit
  anymore. Flag an existing FAQPage at Info, not Critical: removal is not
  required for SEO, and don't add FAQPage to win snippets or AI mentions (no
  confirmed AI benefit). For a page where users genuinely submit answers to a
  question, use **QAPage** (comment-thread properties expanded 2026-03-24).

### Deprecated, never recommend

| Type | Retired | Replacement |
|---|---|---|
| HowTo | Sept 2023 (rich results) | none - keep as plain content |
| SpecialAnnouncement | 2025-07-31 | none |
| CourseInfo / EstimatedSalary / LearningVideo | June 2025 | single `Course` / `JobPosting` + baseSalary / `VideoObject` |
| ClaimReview | June 2025 | none - vocabulary alive, Google ignores it |
| VehicleListing | June 2025 | `Product` schema |
| PracticeProblem | 2026-01-06 (rich result support removed) | none |
| Book Actions | removed | none |

Don't send users to dead validators for those types: Rich Results Test and
Search Console reporting no longer cover CourseInfo, EstimatedSalary,
LearningVideo, SpecialAnnouncement, or VehicleListing.

### Do not flag

- **Dataset**: not discontinued. Consumed by Google Dataset Search (still
  live); it has no Google Search rich-result surface. Never advise removing it
  as though it were killed.
- QAPage, DiscussionForumPosting, Education Q&A (Quiz / eduQuestionType).
- For e-commerce, `hasAdultConsideration` (added 2026-05-20) is required for
  adult products. `Product.category` accepts Text, CategoryCode, or arrays.

## Generation

When generating schema for a page:

1. Identify the page type from content analysis (article, local business,
   product, FAQ-QA, video, job listing, event)
2. Select the appropriate type(s); more than one is fine when the page really
   is several things at once (e.g., Organization on the about page plus Person
   per team member)
3. Generate valid JSON-LD with all required and recommended properties
4. Only include truthful, verifiable data. Everything the user must fill in is
   an explicitly marked placeholder like `[Company Name]`
5. Validate the output (run it back through the scanner or a validator)
6. Review markup rules: reject fake reviews and undisclosed incentivized
   reviews. An incentive (free product, discount, compensation) must be
   clearly disclosed on the page, or the review markup must not be served.

### JavaScript rendering note

Per Google's December 2025 JS SEO guidance, JSON-LD injected via JavaScript
may face delayed processing. For time-sensitive markup (Product, Offer
especially), keep JSON-LD in the initial server-rendered HTML.

## Common templates

### Organization

```json
{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": "[Company Name]",
  "url": "[Website URL]",
  "logo": "[Logo URL]",
  "contactPoint": {
    "@type": "ContactPoint",
    "telephone": "[Phone]",
    "contactType": "customer service"
  },
  "sameAs": ["[Facebook URL]", "[LinkedIn URL]", "[Twitter URL]"]
}
```

### LocalBusiness

```json
{
  "@context": "https://schema.org",
  "@type": "LocalBusiness",
  "name": "[Business Name]",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "[Street]",
    "addressLocality": "[City]",
    "addressRegion": "[State]",
    "postalCode": "[ZIP]",
    "addressCountry": "US"
  },
  "telephone": "[Phone]",
  "openingHours": "Mo-Fr 09:00-17:00",
  "geo": {"@type": "GeoCoordinates", "latitude": "[Lat]", "longitude": "[Long]"}
}
```

### Article / BlogPosting

```json
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "[Title]",
  "author": {"@type": "Person", "name": "[Author Name]"},
  "datePublished": "[YYYY-MM-DD]",
  "dateModified": "[YYYY-MM-DD]",
  "image": "[Image URL]",
  "publisher": {
    "@type": "Organization",
    "name": "[Publisher]",
    "logo": {"@type": "ImageObject", "url": "[Logo URL]"}
  }
}
```

## Output contract

```
SCHEMA-REPORT

| Schema | Type | Status | Issues |
|--------|------|--------|--------|
| ...    | ...  | OK/WARN/FAIL | ... |

Recommendations:
- Missing schema opportunities (what this page type normally carries)
- Validation fixes (each with the exact property and expected value)
- Generated code for implementation (ready-to-paste JSON-LD)
```

Every recommendation carries the prove-it check: "add the snippet to the
page, then re-run the scanner on the live URL until the block reports OK, and
verify in Search Console's rich-results report where available." If a
recommendation depends on data the page can't support, say so (e.g. no
aggregate rating markup while the business has no reviews).

## Errors

| Scenario | Action |
|---|---|
| URL unreachable | Report the connection error and status; check auth needs |
| No markup found | Report none detected; recommend types based on page content |
| Invalid JSON-LD | Point at the exact syntax error (brace, trailing comma, unquoted key) and provide corrected JSON-LD |
| Deprecated type found | Flag with its retirement date; give the replacement or say none exists |

The scanner script

Copy this exact file to ~/.codex/skills/codex-seo-schema/scripts/schema_scan.py.

python
#!/usr/bin/env python3
"""Schema markup collector: JSON-LD blocks, microdata, RDFa, and validations.
Standard library only."""

import json
import re
import sys
import urllib.error
import urllib.request

TIMEOUT = 15
UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
      "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36 "
      "codex-seo-schema/1.0")

# required + recommended properties for the types this skill checks
REQUIRED = {
    "Article": ["headline", "author", "datePublished"],
    "BlogPosting": ["headline", "author", "datePublished"],
    "NewsArticle": ["headline", "author", "datePublished"],
    "Organization": ["name", "url"],
    "LocalBusiness": ["name", "address"],
    "Product": ["name", "offers"],
    "Service": ["name", "provider"],
    "WebPage": ["name", "url"],
    "WebSite": ["name", "url"],
    "Person": ["name"],
    "BreadcrumbList": ["itemListElement"],
    "VideoObject": ["name", "thumbnailUrl", "uploadDate"],
    "JobPosting": ["title", "datePosted", "description", "employmentType"],
}
DEPRECATED = {
    "HowTo": "retired Sept 2023",
    "SpecialAnnouncement": "deprecated 2025-07-31",
    "FAQPage": "rich results retired for all sites 2026-05-07 (keep if useful, no SERP benefit)",
    "PracticeProblem": "support removed 2026-01-06",
    "CourseInfo": "retired June 2025",
    "EstimatedSalary": "retired June 2025",
    "LearningVideo": "retired June 2025",
    "ClaimReview": "retired June 2025 (vocabulary alive, Google ignores it)",
    "VehicleListing": "retired June 2025",
}


def fetch(url):
    req = urllib.request.Request(url, headers={"User-Agent": UA})
    try:
        resp = urllib.request.urlopen(req, timeout=TIMEOUT)
        return resp.status, resp.geturl(), resp.read().decode("utf-8", "replace")
    except urllib.error.HTTPError as e:
        return e.code, url, ""
    except Exception as e:
        return "ERR:" + str(e), url, ""


def item_uris(obj):
    """Collect all @type values inside a JSON-LD object graph."""
    out = []

    def walk(v):
        if isinstance(v, dict):
            t = v.get("@type")
            if isinstance(t, str):
                out.append(t)
            elif isinstance(t, list):
                out.extend(t)
            for k in ("itemListElement", "author", "publisher", "provider", "image", "offers"):
                if k in v:
                    walk(v[k])
        elif isinstance(v, list):
            for x in v:
                walk(x)
    walk(obj)
    return out


def check_block(d, issues):
    if "@context" not in d:
        issues.append("missing @context")
    types = d.get("@type")
    types = [types] if isinstance(types, str) else (types or [])
    for t in types:
        need = REQUIRED.get(t)
        if need:
            got = [k for k in need if k in d]
            miss = [k for k in need if k not in d]
            if miss:
                issues.append("%s missing %s" % (t, ", ".join(miss)))
    for k in ("url", "image", "logo", "mainEntityOfPage", "sameAs"):
        v = d.get(k)
        vals = v if isinstance(v, list) else [v]
        for x in vals:
            if isinstance(x, str) and x.startswith("/"):
                issues.append("%s is relative: %s" % (k, x))
            elif isinstance(x, dict) and "url" in x and isinstance(x["url"], str) and x["url"].startswith("/"):
                issues.append("url is relative: %s" % x["url"])
    for k in ("datePublished", "dateModified", "uploadDate", "datePosted"):
        v = d.get(k)
        if v and not re.match(r"^\d{4}-\d{2}-\d{2}", str(v)):
            issues.append("%s bad date format: %s" % (k, v))


def main():
    url = sys.argv[1] if len(sys.argv) > 1 else ""
    if not url:
        print("usage: python3 schema_scan.py <url> [--json]")
        sys.exit(1)
    want_json = "--json" in sys.argv
    if not url.startswith("http"):
        url = "https://" + url
    status, final, html = fetch(url)
    r = {"url": url, "status": status}
    if not html:
        r["error"] = "no HTML body; status=" + str(status)
        print(json.dumps(r, indent=2) if want_json else r["error"])
        sys.exit(0 if status == 200 else 1)

    def m(p):
        return re.findall(p, html, re.I)

    r["formats"] = {
        "jsonld": len(m(r'<script[^>]*type=["\']application/ld\+json["\'][^>]*>')),
        "microdata": len(m(r'(?i)itemscope')),
        "rdfa": len(m(r'(?i)typeof=["\'][^"\']+["\']')),
    }
    blocks, all_types = [], set()
    for js in m(r'<script[^>]*type=["\']application/ld\+json["\'][^>]*>(.*?)</script>'):
        payload = js[js.find("{"):]
        try:
            d = json.loads(payload)
            issues = []
            check_block(d, issues)
            types = item_uris(d)
            all_types.update(types)
            blocks.append({"types": types, "issues": issues})
        except Exception as e:
            blocks.append({"types": [], "issues": ["invalid JSON: %s" % str(e)[:80] if not want_json else "invalid JSON"]})
    r["jsonld_blocks"] = len(blocks)
    r["types_detected"] = sorted(all_types)
    r["deprecated_hits"] = sorted(t for t in all_types if t in DEPRECATED)
    r["blocks"] = [{k: v for k, v in b.items()} for b in blocks]

    if want_json:
        print(json.dumps(r, indent=2))
    else:
        print("schema_scan %s  status %s" % (r["url"], r["status"]))
        print("  formats: JSON-LD %d  microdata %d  RDFa %d" % (
            r["formats"]["jsonld"], r["formats"]["microdata"], r["formats"]["rdfa"]))
        print("  types: %s" % (", ".join(r["types_detected"]) or "none found"))
        for i, b in enumerate(blocks):
            label = ", ".join(b["types"]) or "unparsed"
            print("  block %d [%s]: %s" % (i + 1, label, "; ".join(b["issues"]) or "ok"))
        if r["deprecated_hits"]:
            for t in r["deprecated_hits"]:
                print("  DEPRECATED %s (%s)" % (t, DEPRECATED[t]))


if __name__ == "__main__":
    main()

Install it in three commands

bash
mkdir -p ~/.codex/skills/codex-seo-schema/scripts
# save the two files above at:
#   ~/.codex/skills/codex-seo-schema/SKILL.md
#   ~/.codex/skills/codex-seo-schema/scripts/schema_scan.py
python3 ~/.codex/skills/codex-seo-schema/scripts/schema_scan.py https://example.com

The last command should print the formats found (JSON-LD count, microdata, RDFa), the types detected, and a per-block issue line (or ok). If it says none found, the page genuinely has no schema, which is itself a finding.

A real run, and the finding that surprised us

Here is the actual output of the scanner on one of the articles from this very series, https://auspia.ai/blog/codex-seo-technical-audit:

Code
schema_scan https://auspia.ai/blog/codex-seo-technical-audit  status 200
  formats: JSON-LD 1  microdata 0  RDFa 0
  types: BlogPosting, Organization
  block 1 [BlogPosting, Organization]: BlogPosting missing author

One JSON-LD block, two types, one issue. The direct markup on that page is mostly right: BlogPosting with headline, description, image, URL, canonical datePublished, and a publisher Organization. The problem is the missing author property: the page renders a byline, but the JSON-LD doesn't carry it. For an Article-type markup, author is required, and this is exactly the kind of thing that looks fine in a browser and fails validation.

That finding is consistent with the content-quality scan from the E-E-A-T article in this series, which also reported no author meta on the same page. Two different scanners, pointing at the same fix: the article template should emit the author into the JSON-LD, not just into the visible page.

This is the nice property of schema work: the scan output maps one-to-one to the fix. Missing author in BlogPosting means: add "author": {"@type": "Person", "name": "..."} to the block in your article template. It's a five-minute change, and it's the same change most article markup on the web is missing.

Troubleshooting

Scenario

Action

python3: command not found

Install Python 3, or use python3.11

Page returns 401/403

Behind auth; ask for a public URL or pasted HTML

No JSON-LD found

That's a result, not an error. Recommend the right types for the page content

One block reports invalid JSON

The scanner names the likely spot; open the source and check for trailing commas or a missing brace

Deprecated type detected in a big existing site

Flag it with its date and note whether a replacement exists. Bulk changes at once are risky; prioritize page types that drive traffic

Codex doesn't trigger

/skills to check, or invoke $codex-seo-schema explicitly

Paste this article into Codex

Paste the whole article up to this line into a new Codex conversation and say:

text
Read the two code fences marked "The full SKILL.md" and "The scanner script" above.
1. Create ~/.codex/skills/codex-seo-schema/SKILL.md
2. Create ~/.codex/skills/codex-seo-schema/scripts/schema_scan.py
3. Run: python3 ~/.codex/skills/codex-seo-schema/scripts/schema_scan.py https://example.com
4. If that works, scan my site's most important pages and show me a fix list. For each issue, give me the corrected JSON-LD block.
Do not install or modify any other files.

Once installed, this is the skill that will answer every "why is my rich result not showing" question from now on. Run the scan first; the answer is almost always in the table.

FAQ

Should I keep my FAQPage markup? Keep it (flag as Info, not Critical) but know it earns no Google rich result since May 7, 2026. Don't remove it just for SEO, and don't add it to new pages hoping for snippets or AI citations. If a page is genuine user Q&A (users can submit answers), use QAPage.

Is schema a ranking factor? Not directly, and this skill never claims it is. What schema does is make your content eligible for rich results, give search engines unambiguous facts (who wrote this, what's the price), and support AI systems extracting entities. The scanner's prove-it checks are about markup being valid, not about "adding schema will rank you."

My schema was generated by a plugin/CMS. Should I trust it? Run the scanner on a live page. The check takes seconds, and it will tell you whether the template fills required properties, whether dates and URLs are absolute, and whether the block is valid JSON.

Can I add schema through JavaScript? You can, but Google's December 2025 guidance says JS-injected structured data may process more slowly. Put time-sensitive markup (Product, Offer) in the server-rendered HTML.

Does Dataset markup still matter? Yes, for Google Dataset Search. It has no regular rich-result surface, so it shows up in a special place, but the vocabulary is not dead and should not be flagged for removal.

Next in the series (post 06 of 20): [How to Set Up Codex for Image SEO (Full SKILL.md Included)](https://auspia.ai/blog/codex-seo-images) - image seo audit and optimization targets.

Previous in the series: How to Build an SEO Content Brief with Codex (Full SKILL.md Included). The full series roadmap lists all 20 posts.

Author: Victor Lane, GEO Audit Specialist with 300+ Readiness Reviews at Auspia. Victor builds the checklists and scorecards behind Auspia's technical and visibility readiness reviews.

Based on the open-source claude-seo project by AgriciDaniel (MIT license, GitHub). This series adapts it for the Codex runtime: rewritten methodology, a Codex-native SKILL.md, and new evidence collectors written from scratch for this series.

Explore this topic

Keep following the same growth thread