Claude AI

Full SEO Audit Claude Skill

This skill runs a complete technical-SEO audit of a website by orchestrating every individual audit skill, sitemap, robots.txt, redirects, internal and external links, metadata, headings, images, schema markup, canonical tags, Open Graph, mixed content, pagination, soft 404s, content quality, keyword cannibalization, site architecture, Core Web Vitals, and llms.txt, then consolidates the findings into one prioritized report ranked by impact, not by which audit found the issue.

What The Skill Checks

  • Crawlability and indexation: sitemap, robots.txt, redirects, canonical tags, pagination, and soft 404s.
  • On-page and content: metadata, headings, images, schema markup, Open Graph, content quality, and keyword cannibalization.
  • Site health and AI readiness: internal and external links, site architecture, mixed content, Core Web Vitals, and llms.txt, all consolidated into one impact-ranked report.

How The Skill Is Packaged

The skill follows the standard Claude Agent Skill structure: a SKILL.md orchestrator with YAML frontmatter and workflow instructions, a references/ folder with a coverage map of every audit it runs and the consolidated report template, and a scripts/ folder with the shared fetch_pages.py crawler that fetches the site once into a page_cache.json every page-based sub-audit reads from, plus a Python script that merges the individual reports into one prioritized output. Every report can be exported to a shareable Word .docx or a flat .csv of findings via the bundled md_to_docx.py and findings_to_csv.py scripts. Install it with npx claude-seo-skills install full-seo-audit, or copy the skill folder into your Claude skills directory; Claude invokes it automatically when a request matches its description.

Skill Files

Every file in the skill is shown below, copied from the Claude-SEO-Skills repository, so you can review exactly what the skill instructs Claude to do before installing it.

SKILL.md

The orchestrator definition: frontmatter, inputs to collect, and how it runs and sequences the per-area skills.

SKILL.mdView on GitHub
name
full-seo-audit
description
Run a complete technical-SEO audit of a website by orchestrating every individual audit (sitemap, robots.txt, redirects, internal links, external links, metadata, headings, images, schema markup, canonical tags, Open Graph, mixed content, pagination, soft 404s, content quality, keyword cannibalization, site architecture, Core Web Vitals, and llms.txt) and consolidating the results into one prioritized report. Use when the user asks for a full, complete, comprehensive, end-to-end, or site-wide SEO audit, a technical SEO review, or a single report covering many SEO issues at once.

Full SEO Audit

Run the individual SEO audit skills, then merge their findings into one consolidated, prioritized report. This skill is the orchestrator; the real work is done by the per-area skills it coordinates.

When to use this skill

Use this when the user wants the whole picture rather than one area:

  • "Run a full / complete / comprehensive SEO audit of example.com"
  • "Do a technical SEO review of my site"
  • "Give me one report covering everything that's wrong with my SEO"

For a single area ("check my sitemap", "audit my titles"), use that area's skill directly instead.

Inputs to collect
  1. Site URL — the live site root (e.g., https://example.com).
  2. Scope — how many pages to crawl (default ~200) and how many sitemap URLs to probe. Larger sites should be sampled.
  3. Focus — run everything (default), or a chosen subset (e.g., "just crawlability and indexation"). Map the user's intent to the skills below.
  4. API key (optional) — Core Web Vitals field data needs a Google PageSpeed Insights key; without it, that audit uses lab proxies only.
Coverage map

Each row is a separate skill. Run the ones in scope.

AreaSkillCrawls / fetches
Indexation: sitemapssitemap-auditXML sitemaps
Crawlability: robots.txtrobots-txt-auditrobots.txt
Crawlability: architecturesite-architecture-auditsite + robots + sitemap
Redirectsredirect-auditURL list / crawl
Internal linksinternal-link-auditcrawl
External linksexternal-link-auditcrawl
Canonical tagscanonical-tag-auditcrawl
Paginationpagination-auditcrawl
Soft 404ssoft-404-auditcrawl
Titles & descriptionsmeta-data-auditcrawl
Headingsheading-structure-auditcrawl
Imagesimage-seo-auditcrawl
Structured dataschema-markup-auditcrawl
Social metadataopen-graph-auditcrawl
Content qualitycontent-quality-auditcrawl
Keyword cannibalizationkeyword-cannibalization-auditcrawl
HTTPS / mixed contentmixed-content-auditcrawl
Page experiencecore-web-vitals-auditper-URL
AI discoverabilityllms-txt-audit/llms.txt
Workflow
Step 1: Crawl once into a shared page cache

The page-based audits all read the same HTML, so crawl the site a single time with the shared fetch stage and let every extractor read from that cache:

mkdir -p audits
python3 scripts/fetch_pages.py https://example.com --max-pages 200 --output audits/page_cache.json
Step 2: Extract + audit each in-scope area

For each page-based area in scope, run its extractor with --from-cache (pure, no extra crawl) then its audit script. Save each audit output to the shared folder using a stable, skill-named filename so they can be merged:

# repeat per in-scope page-based skill — all read the ONE page_cache.json
python3 ../meta-data-audit/scripts/extract_metadata.py --from-cache audits/page_cache.json --output audits/meta.inventory.json
python3 ../meta-data-audit/scripts/audit_metadata.py audits/meta.inventory.json --output audits/meta-data-audit.audit_report.json

python3 ../heading-structure-audit/scripts/extract_headings.py --from-cache audits/page_cache.json --output audits/head.inventory.json
python3 ../heading-structure-audit/scripts/audit_headings.py audits/head.inventory.json --output audits/heading-structure-audit.audit_report.json

The skills that read --from-cache are: meta-data, heading-structure, image-seo, schema-markup, open-graph, content-quality, keyword-cannibalization, mixed-content, canonical-tag, pagination, soft-404, and external-link. (A few also run a small secondary probe — canonical targets, share images, the soft-404 missing-page probe — on top of the cached crawl.)

The non-page audits fetch their own thing and don't use the page cache: run them per their own SKILL.md — sitemap-audit, robots-txt-audit, llms-txt-audit, redirect-audit, core-web-vitals-audit, plus internal-link-audit and site-architecture-audit (which crawl with depth tracking of their own).

Step 3: Consolidate

Merge every *.audit_report.json into one cross-skill view:

python3 scripts/consolidate_reports.py --dir audits --output consolidated_report.json

The consolidator emits total issues by severity, a per-skill breakdown, and a flat list of every failing check ranked by severity then count.

Step 4: Produce the consolidated report

Write the report following references/report-template.md:

  1. Executive summary — overall health, issue counts by severity, the 3–5 highest-impact fixes across all areas.
  2. Scorecard — one row per audited area with its issue counts and a status.
  3. Findings by area — a short section per skill summarizing its top issues and linking to (or inlining) that skill's own detailed report.
  4. Prioritized roadmap — a single cross-area action list ordered by impact, not by area.
Step 5: Prioritize across areas

Rank fixes by impact, not by which skill found them. Apply this order:

  1. Indexation blockers first — anything stopping pages from being crawled or indexed (robots Disallow: /, sitewide noindex, sitemap full of dead URLs, broken canonicals) outranks on-page polish.
  2. Sitewide/template issues over one-off issues — a bug in one template affecting 500 pages beats a single page's long title.
  3. High-traffic, high-intent pages first within a given fix.
  4. Group related findings across skills (e.g., the same http→https migration surfaces in sitemap, redirect, internal-link, canonical, and mixed-content audits — present it once as a single root-cause fix).
Optional: export the report
As a Word document (.docx)

If the user wants the report as a .docx (for example, to share with stakeholders or attach to a ticket), save the Markdown report to a file and convert it:

python3 scripts/md_to_docx.py report.md --output report.docx

scripts/md_to_docx.py uses only the Python standard library (no pip install) and renders headings, tables, lists, links, bold/italic, and code blocks. Offer this whenever a user asks for a Word doc, a .docx, or a shareable/downloadable report.

As a CSV of findings (.csv)

If the user wants the raw findings as a spreadsheet (for filtering, sorting, or triage in Sheets or Excel), convert the audit's audit_report.json directly:

python3 scripts/findings_to_csv.py audit_report.json --output findings.csv

scripts/findings_to_csv.py is also standard-library only. It writes one row per finding, with check and severity columns prepended and list fields (e.g. the pages sharing a duplicate value) joined with ; . Unlike the .docx, which reformats the written report, the CSV is a direct dump of the structured findings — offer it whenever a user wants the data itself, a spreadsheet, or to slice findings by check or severity.

Resources
  • scripts/md_to_docx.py — convert the Markdown report into a Word (.docx) document (standard library only)
  • scripts/findings_to_csv.py — flatten the audit findings JSON into a CSV, one row per finding (standard library only)
  • references/report-template.md — consolidated report structure
  • references/coverage-map.md — what each sub-skill covers and what it needs
  • scripts/consolidate_reports.py — merge per-skill audit reports into one ranked report

Coverage Map (references/coverage-map.md)

Maps every SEO area to the individual skill that covers it, so the orchestrator knows what to run and how findings map back.

references/coverage-map.mdView on GitHub

Coverage map

What each sub-skill audits, the scripts it runs, and what it needs as input. Use this to decide which skills are in scope for a given request and to map a user's words ("indexation", "page speed", "international") to the right skills.

Crawlability & indexation
SkillFindsNeeds
robots-txt-auditSitewide Disallow: /, syntax errors, blocked CSS/JS, missing/unreachable Sitemap:, unsupported directivesrobots.txt
sitemap-auditDead/redirecting/noindexed/blocked URLs in sitemaps, size limits, lastmod hygienesitemap(s)
site-architecture-auditHost/protocol canonicalization, sitemap parity, crawl/index conflicts, click depth, URL qualitysite + robots + sitemap
redirect-auditRedirect chains/loops, 302-for-permanent, HTTPS→HTTP, meta/JS redirects, dropped query stringsURL list or crawl
soft-404-auditPages that return 200 but are really "not found" / emptycrawl
Links
SkillFindsNeeds
internal-link-auditBroken links, orphans, redirect chains, click depth, anchor quality, equity distributioncrawl
external-link-auditBroken/redirected outbound links, insecure HTTP, rel="sponsored" compliance, unsafe target="_blank"crawl
pagination-auditrel=next/prev, canonical-on-paginated mistakes, orphaned/duplicated paginated pagescrawl
On-page & indexation signals
SkillFindsNeeds
meta-data-auditMissing/duplicate/long/short/generic titles & descriptions, stuffingcrawl
heading-structure-auditMissing/multiple H1s, skipped levels, empty/long/duplicate headingscrawl
canonical-tag-auditMissing/duplicate/relative canonicals, canonical to non-200/redirect/noindex, cross-host, self vs. clustercrawl
schema-markup-auditMissing/invalid/incomplete JSON-LD, Microdata, RDFa vs. Google requirementscrawl
open-graph-auditMissing/duplicate OG & Twitter Card tags, bad image dimensions, og:url mismatchescrawl
Content
SkillFindsNeeds
content-quality-auditThin content, low text-to-HTML, duplicate/near-duplicate bodies, missing main contentcrawl
keyword-cannibalization-auditMultiple pages targeting the same query via title/H1/overlapcrawl
Security & delivery
SkillFindsNeeds
mixed-content-auditHTTP subresources on HTTPS pages, insecure form actions, upgradeable linkscrawl
core-web-vitals-auditLCP/CLS/INP/TTFB field data (with API key) + lab proxiesper-URL; optional PSI key
AI discoverability
SkillFindsNeeds
llms-txt-auditMissing/malformed /llms.txt, broken links, missing descriptions/llms.txt
Sharing one crawl

Most on-page skills crawl HTML. To avoid hammering the site, prefer running the broadest crawler first (e.g. site-architecture-audit or internal-link-audit) and feeding the discovered URL set to the others via their --url-list mode where supported, or cap --max-pages consistently across skills.

Consolidated Report Template (references/report-template.md)

The structure of the single, cross-area report, with findings grouped and ranked by impact.

references/report-template.mdView on GitHub

Full SEO Audit Report Template

Use this structure for the consolidated report delivered to the user.

# Full SEO Audit — {site}

**Date:** {date}
**Scope:** {pages crawled, sitemap URLs probed, areas audited}
**Areas audited:** {n} of {total}

## Executive summary

| Severity | Issues |
|---|---|
| High | {n} |
| Medium | {n} |
| Low | {n} |

{3–5 sentences: overall health, the single biggest theme (e.g. an incomplete
http→https migration), and the highest-impact fixes.}

**Top 5 fixes (across all areas):**

1. {Highest-impact fix, the area(s) it touches, and the pages affected}
2. ...

## Scorecard

| Area | High | Medium | Low | Status |
|---|---|---|---|---|
| Sitemaps | {n} | {n} | {n} | 🔴 / 🟡 / 🟢 |
| robots.txt | … | … | … | … |
| Internal links | … | … | … | … |
| Metadata | … | … | … | … |
| … one row per audited area … | | | | |

## Findings by area

### {Area name}

{2–4 sentence summary of this area's top issues, with counts. Link to or inline
the area's detailed report. Do not repeat every row here — point to the per-skill
report for the full list.}

{Repeat per audited area, ordered by severity of findings.}

## Prioritized roadmap

Ordered by impact across all areas, not by which audit found the issue.

1. **{Fix}** — {severity}, {pages/areas affected}. {One line on why it's first.}
2. ...

### Quick wins (low effort, real impact)

- {Bulleted shortlist of cheap fixes}

### Root-cause groupings

- **{e.g. http→https migration}** surfaces across {sitemap, redirects, internal
  links, canonical, mixed content}. Fix once at the source: {the fix}.
Guidance
  • Prioritize across areas, not within them. A single template bug affecting 500 pages outranks a worse-sounding issue on one page. Indexation blockers (robots, noindex, broken canonicals, dead sitemap URLs) come before on-page polish.
  • Collapse duplicated root causes. The same migration or template defect often trips several audits; present it once with all the symptoms it explains.
  • Don't drown the reader. The consolidated report summarizes; the per-skill reports hold the exhaustive tables. Link to them.
  • Every claim traces to data. Pull counts from consolidated_report.json and specifics from each skill's audit_report.json. Never invent URLs or numbers.
  • State coverage honestly. List which areas were audited and which were skipped (and why — e.g. "Core Web Vitals field data skipped: no PSI key").

Shared Page Crawler (scripts/fetch_pages.py)

Crawls the site a single time and stores the raw HTML and response facts in one page_cache.json, so every page-based sub-audit extracts from the same snapshot instead of re-crawling per area.

scripts/fetch_pages.pyView on GitHub
#!/usr/bin/env python3
"""Crawl a website once and write a raw page cache that every audit can extract from.

This is the *fetch* stage of the pipeline, split out from per-skill collectors so a
site is crawled a single time and all audits read from the same snapshot:

    fetch (this script)  ->  page_cache.json  ->  extract_*.py (pure)  ->  *_inventory.json

The cache stores raw HTML and the response facts every collector needs, and nothing
audit-specific — each skill's extractor pulls only the bits it cares about. Because
fetching is the only network stage, everything downstream is pure and re-runnable
offline.

Usage:
    python3 fetch_pages.py https://example.com [--max-pages 500] [--output page_cache.json]
    python3 fetch_pages.py ./site-folder --local  [--output page_cache.json]
    python3 fetch_pages.py urls.txt --url-list     [--output page_cache.json]

Output JSON schema:
{
  "site": "https://example.com",
  "mode": "crawl" | "local" | "url-list",
  "fetched_at": "2026-06-19T12:00:00Z",
  "pages": {
    "<requested url or local relpath>": {
      "status": 200,                # HTTP status; 0 = network/connection error
      "final_url": "<url after redirects>",
      "content_type": "text/html; charset=utf-8",
      "headers": {"Link": "...", "X-Robots-Tag": "..."},  # response headers
      "html": "<!doctype html>..."  # null for non-HTML or failed fetches
    }
  }
}

Uses only the Python standard library.
"""

import argparse
import datetime
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
from collections import deque
from html.parser import HTMLParser

USER_AGENT = "SEOAuditBot/1.0 (+claude-skill)"
TIMEOUT = 15
MAX_BYTES = 2_000_000


class LinkParser(HTMLParser):
    """Pull same-document <a href> links so the crawler can do a same-host BFS.

    The fetch stage only needs links to know where to crawl next — every other
    tag is left for the per-skill extractors to parse from the cached HTML.
    """

    def __init__(self, base_url):
        super().__init__(convert_charrefs=True)
        self.base_url = base_url
        self.links = []

    def handle_starttag(self, tag, attrs):
        if tag != "a":
            return
        a = dict(attrs)
        href = (a.get("href") or "").strip()
        if not href or href.startswith(("mailto:", "tel:", "javascript:", "#")):
            return
        target = urllib.parse.urljoin(self.base_url, href)
        self.links.append(urllib.parse.urldefrag(target)[0])


def fetch(url):
    """Return (status, final_url, content_type, headers, html_or_none)."""
    req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
    try:
        with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
            ctype = resp.headers.get("Content-Type", "")
            body = None
            if "html" in ctype or "xml" in ctype:
                body = resp.read(MAX_BYTES).decode("utf-8", errors="replace")
            return resp.status, resp.geturl(), ctype, dict(resp.headers), body
    except urllib.error.HTTPError as e:
        hdrs = dict(e.headers or {})
        return e.code, url, hdrs.get("Content-Type", ""), hdrs, None
    except Exception:
        return 0, url, "", {}, None  # 0 = network/connection error


def norm_host(url):
    host = urllib.parse.urlparse(url).netloc.lower()
    return host[4:] if host.startswith("www.") else host


def links_in(final_url, html):
    parser = LinkParser(final_url)
    try:
        parser.feed(html)
    except Exception:
        pass
    return parser.links


def pages_from_sitemap(site):
    _status, _final, _ctype, _headers, body = fetch(urllib.parse.urljoin(site, "/sitemap.xml"))
    if not body:
        return []
    urls = []
    try:
        root = ET.fromstring(body)
        for loc in root.iter("{http://www.sitemaps.org/schemas/sitemap/0.9}loc"):
            if loc.text:
                urls.append(loc.text.strip())
    except ET.ParseError:
        return []
    return urls


def record(status, final_url, content_type, headers, html):
    return {"status": status, "final_url": final_url,
            "content_type": content_type, "headers": headers, "html": html}


def crawl(site, max_pages):
    start = site.rstrip("/")
    host = norm_host(start)
    seeds = pages_from_sitemap(start) or [start]
    queue = deque(seeds[:max_pages])
    seen = set(queue)
    pages = {}
    while queue and len(pages) < max_pages:
        url = queue.popleft()
        status, final_url, ctype, headers, html = fetch(url)
        print(f"[{status}] {url}", file=sys.stderr)
        pages[url] = record(status, final_url, ctype, headers, html)
        if not html:
            continue
        for link in links_in(final_url, html):
            if norm_host(link) == host and link not in seen:
                seen.add(link)
                queue.append(link)
    return pages


def scan_local(folder):
    pages = {}
    for root, _dirs, files in os.walk(folder):
        for name in files:
            if name.lower().endswith((".html", ".htm")):
                path = os.path.join(root, name)
                with open(path, encoding="utf-8", errors="replace") as f:
                    html = f.read()
                rel = os.path.relpath(path, folder)
                pages[rel] = record(200, "file://" + os.path.abspath(path),
                                    "text/html", {}, html)
    return pages


def scan_url_list(path, max_pages):
    pages = {}
    with open(path, encoding="utf-8") as f:
        urls = [line.strip() for line in f if line.strip() and not line.startswith("#")]
    for url in urls[:max_pages]:
        status, final_url, ctype, headers, html = fetch(url)
        print(f"[{status}] {url}", file=sys.stderr)
        pages[url] = record(status, final_url, ctype, headers, html)
    return pages


def main():
    ap = argparse.ArgumentParser(description="Crawl once into a raw page cache for SEO audits.")
    ap.add_argument("source", help="Site root URL, local folder (--local), or URL list file (--url-list)")
    ap.add_argument("--max-pages", type=int, default=500)
    ap.add_argument("--output", default="page_cache.json")
    ap.add_argument("--local", action="store_true", help="Treat source as a local folder of HTML files")
    ap.add_argument("--url-list", action="store_true", help="Treat source as a text file of URLs")
    args = ap.parse_args()

    if args.local:
        mode, pages = "local", scan_local(args.source)
    elif args.url_list:
        mode, pages = "url-list", scan_url_list(args.source, args.max_pages)
    else:
        mode, pages = "crawl", crawl(args.source, args.max_pages)

    cache = {
        "site": args.source,
        "mode": mode,
        "fetched_at": datetime.datetime.now(datetime.timezone.utc)
        .strftime("%Y-%m-%dT%H:%M:%SZ"),
        "pages": pages,
    }
    with open(args.output, "w", encoding="utf-8") as f:
        json.dump(cache, f, indent=2, ensure_ascii=False)
    html_pages = sum(1 for p in pages.values() if p["html"])
    print(f"Cached {len(pages)} pages ({html_pages} with HTML) to {args.output}",
          file=sys.stderr)


if __name__ == "__main__":
    main()

Report Consolidator Script (scripts/consolidate_reports.py)

Merges the output of every individual audit into one prioritized report ranked by impact across areas.

scripts/consolidate_reports.pyView on GitHub
#!/usr/bin/env python3
"""Merge multiple per-skill audit_report.json files into one consolidated report.

Each individual audit skill writes an audit report shaped like:

    { "summary": {...}, "severity": {"<check>": "high"|"medium"|"low"|"info", ...},
      "findings": {"<check>": [ ... ], ...} }

This script reads several of those (by file, glob, or directory) and produces a
single cross-skill view: total issues by severity, a per-skill breakdown, and a
flat, severity-ranked list of every failing check.

Usage:
    python3 consolidate_reports.py report1.json report2.json ... [--output consolidated.json]
    python3 consolidate_reports.py --dir ./audits [--output consolidated.json]

The skill name for each report is inferred from the filename (e.g.
`sitemap-audit.audit_report.json` or `sitemap_audit_report.json` -> "sitemap").
Override per file with `name=path`:

    python3 consolidate_reports.py sitemap=sm.json meta=md.json

Uses only the Python standard library.
"""

import argparse
import glob
import json
import os
import sys

SEVERITY_ORDER = {"high": 0, "medium": 1, "low": 2, "info": 3}


def infer_name(path):
    base = os.path.basename(path)
    for suffix in (".audit_report.json", "_audit_report.json", ".report.json",
                   "_report.json", ".json"):
        if base.endswith(suffix):
            base = base[: -len(suffix)]
            break
    return base.replace("_", "-").replace(".", "-") or "report"


def load_report(name, path):
    try:
        with open(path, encoding="utf-8") as f:
            data = json.load(f)
    except (OSError, json.JSONDecodeError) as e:
        print(f"  skipped {path}: {e}", file=sys.stderr)
        return None
    severity = data.get("severity", {})
    findings = data.get("findings", {})
    checks = []
    for check, items in findings.items():
        count = len(items) if isinstance(items, list) else int(bool(items))
        if count == 0:
            continue
        checks.append({
            "skill": name,
            "check": check,
            "severity": severity.get(check, "info"),
            "count": count,
        })
    return checks


def main():
    ap = argparse.ArgumentParser(description="Consolidate per-skill audit reports.")
    ap.add_argument("reports", nargs="*",
                    help="Report JSON files, optionally as name=path. Or use --dir.")
    ap.add_argument("--dir", help="Directory to scan for *report*.json files")
    ap.add_argument("--output", default="consolidated_report.json")
    args = ap.parse_args()

    inputs = []  # (name, path)
    for token in args.reports:
        if "=" in token and not os.path.exists(token):
            name, path = token.split("=", 1)
            inputs.append((name, path))
        else:
            for path in glob.glob(token) or [token]:
                inputs.append((infer_name(path), path))
    if args.dir:
        for path in sorted(glob.glob(os.path.join(args.dir, "*report*.json"))):
            inputs.append((infer_name(path), path))

    if not inputs:
        ap.error("no report files given (pass files or --dir)")

    all_checks = []
    per_skill = {}
    for name, path in inputs:
        checks = load_report(name, path)
        if checks is None:
            continue
        all_checks.extend(checks)
        bucket = per_skill.setdefault(name, {"high": 0, "medium": 0, "low": 0, "info": 0})
        for c in checks:
            bucket[c["severity"]] = bucket.get(c["severity"], 0) + c["count"]

    all_checks.sort(key=lambda c: (SEVERITY_ORDER.get(c["severity"], 9),
                                   -c["count"], c["skill"], c["check"]))

    totals = {"high": 0, "medium": 0, "low": 0, "info": 0}
    for c in all_checks:
        totals[c["severity"]] = totals.get(c["severity"], 0) + c["count"]

    consolidated = {
        "summary": {
            "skills_audited": sorted(per_skill),
            "issues_by_severity": totals,
            "total_issues": sum(totals.values()),
            "per_skill": per_skill,
        },
        "checks_ranked": all_checks,
    }

    with open(args.output, "w", encoding="utf-8") as f:
        json.dump(consolidated, f, indent=2, ensure_ascii=False)

    print(json.dumps(consolidated["summary"], indent=2))
    print(f"\nConsolidated {len(per_skill)} report(s), "
          f"{consolidated['summary']['total_issues']} total issues, "
          f"written to {args.output}", file=sys.stderr)


if __name__ == "__main__":
    main()

Explore Other Claude SEO Skills

Internal Link Audit Audits a site's internal linking structure for broken links, orphan pages, redirect chains, click depth, and anchor text quality. internal-link-audit Open External Link Audit Audits a site's external links for broken targets, redirects, insecure HTTP links, missing rel attributes on affiliate links, and unsafe target="_blank" usage. external-link-audit Open Meta Data Audit Audits a site's title tags and meta descriptions for missing, duplicate, too-long, too-short, or multiple tags, generic boilerplate titles, and keyword stuffing. meta-data-audit Open Schema Markup Audit Audits a site's structured data: extracting JSON-LD, Microdata, and RDFa, then validating it against schema.org and Google rich result requirements. schema-markup-audit Open Site Architecture Audit Audits a site's architecture: hostname and protocol canonicalization, robots.txt, XML sitemap parity, crawl and index directive conflicts, URL structure, click depth, and directory taxonomy. site-architecture-audit Open Heading Structure Audit Audits a site's heading hierarchy for missing or multiple H1s, skipped heading levels, empty headings, headings that don't start at H1, overly long headings, and duplicate heading text. heading-structure-audit Open Image SEO Audit Audits a site's images for SEO and Core Web Vitals: missing alt text, absent width/height, missing lazy-loading, oversized files, legacy formats, generic filenames, and broken images. image-seo-audit Open Core Web Vitals Audit Audits a site's Core Web Vitals and page-experience signals (LCP, CLS, INP, TTFB), pulling real CrUX field data from PageSpeed Insights and collecting lab proxies like render-blocking resources, page weight, and image hygiene. core-web-vitals-audit Open Redirect Audit Audits a site's redirects for redirect chains and loops, redirects ending in 404s or errors, 302s used for permanent moves, HTTPS-to-HTTP downgrades, client-side redirects, dropped query strings, and canonicals pointing at redirecting URLs. redirect-audit Open Robots.txt Audit Audits a site's robots.txt for a redirected or HTML-served file, a 5xx read as block-everything, a sitewide Disallow, syntax errors, blocked CSS/JS, missing Sitemap directives, unsupported directives, oversized files, and encoding issues. robots-txt-audit Open Sitemap Audit Audits a site's XML sitemaps: discovering them from robots.txt, following sitemap index files, and validating every URL for broken or redirecting targets, noindexed or canonicalized pages, wrong host or protocol, duplicates, oversized sitemaps, and invalid lastmod values. sitemap-audit Open Llms.txt Audit Audits a site's /llms.txt for AI and LLM discoverability: missing files, HTML served instead of markdown, a missing H1 or summary, broken or redirecting links, links missing descriptions, wrong-host or relative URLs, and a companion /llms-full.txt. llms-txt-audit Open Canonical Tag Audit Audits a site's rel=canonical tags for missing canonicals, multiple conflicting canonicals, canonicals pointing at non-200, redirecting, or noindexed URLs, relative or cross-host canonicals, https-to-http downgrades, and Link-header conflicts. canonical-tag-audit Open Content Quality Audit Audits a site's content quality for thin content, exact-duplicate body text across pages, near-duplicate (templated) pages, low text-to-HTML ratio, and pages missing an H1. content-quality-audit Open Keyword Cannibalization Audit Audits a site for keyword cannibalization: pages with duplicate title targets, clusters of pages whose title, H1, and keyword signatures overlap, and pages sharing the same primary keyword phrase, with a recommended hierarchy. keyword-cannibalization-audit Open Mixed Content Audit Audits an HTTPS site for mixed content: active mixed content (http scripts, styles, iframes browsers block), insecure form actions, passive mixed content (http images and media), protocol-relative URLs, and pages still served over http. mixed-content-audit Open Open Graph Audit Audits a site's Open Graph and Twitter Card metadata: missing og:title, og:description, og:image, or og:url, broken or relative og:image URLs, og:url that mismatches the canonical, missing twitter:card, over-length social titles, and reused default images. open-graph-audit Open Pagination Audit Audits a site's pagination: paginated pages canonicalizing to page 1 and hiding deep content, noindexed component pages, paginated pages missing a self-canonical, broken or inconsistent rel=next/prev, and redundant ?page=1 first pages. pagination-audit Open Soft 404 Audit Audits a site for soft 404s: error pages served with a 200 status, servers that return 200 for every unknown URL, thin pages matching the site's error template, and empty or near-empty pages. soft-404-audit Open