Claude AI

External Link Audit Claude Skill

This skill audits a website's external (outbound) links and produces an actionable SEO report. It crawls pages to find broken external links, redirected targets, insecure HTTP links, missing rel attributes on affiliate or sponsored links, unsafe target="_blank" usage, and risky link destinations, then recommends concrete fixes.

What The Skill Checks

  • Broken external links (link rot) and redirected targets, with the exact source page, target URL, and anchor text for each issue.
  • Affiliate or paid links missing rel="sponsored", user-generated content links missing rel="ugc", and insecure http:// outbound links where the destination supports HTTPS.
  • Unsafe target="_blank" usage without rel="noopener", generic anchor text on external links, and pages or templates with excessive or sitewide external links.

How The Skill Is Packaged

The skill follows the standard Claude Agent Skill structure: a SKILL.md file with YAML frontmatter and workflow instructions, a references/ folder with the full audit check definitions and report template, and a scripts/ folder with Python scripts that extract external links from the site and check every target. 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 external-link-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 skill definition: frontmatter, inputs to collect, the five-step audit workflow, and the resources the skill relies on.

SKILL.mdView on GitHub
name
external-link-audit
description
Audit a website's external (outbound) links for SEO. Crawls pages to find broken external links, redirected targets, insecure HTTP links, missing rel attributes on affiliate or sponsored links, unsafe target="_blank" usage, and risky or low-quality link destinations. Use when the user asks to audit, analyze, check, or clean up external links, outbound links, broken links to other sites, or affiliate link compliance.

External Link Audit

Audit the external (outbound) links of a website and produce an actionable SEO report.

When to use this skill

Use this skill when the user asks to:

  • Audit or analyze external/outbound links on a website
  • Find broken external links (link rot) or redirected destinations
  • Check affiliate, sponsored, or paid links for correct rel attribution
  • Find insecure http:// outbound links or unsafe target="_blank" usage
  • Review which external domains a site links to and how often
Inputs to collect

Before starting, confirm with the user:

  1. Site URL or local files — a live site root URL (e.g., https://example.com), a sitemap URL, or a local folder of HTML files.
  2. Scope — full site, a specific section (e.g., /blog/), or a list of URLs.
  3. Crawl limits — max pages (default 500) and max depth (default 5) for live crawls.
  4. Known paid/affiliate domains — domains that must carry rel="sponsored" (e.g., amzn.to, affiliate networks), if any.
Workflow
Step 1: Gather the page set and extract external links
  • If a sitemap is available (/sitemap.xml), fetch it to get the canonical page list.
  • Otherwise, crawl from the homepage following only same-host links.
  • For local HTML folders, enumerate all .html files.

Use scripts/extract_external_links.py to crawl the site and collect every external link into a JSON inventory:

python3 scripts/extract_external_links.py https://example.com --max-pages 500 --max-depth 5 --output external_links.json
# already crawled once (e.g. in a full SEO audit)? skip the crawl and reuse the shared cache:
# python3 scripts/fetch_pages.py https://example.com --output page_cache.json
# python3 scripts/extract_external_links.py --from-cache page_cache.json --output external_links.json

For every external link, the inventory records: source page, target URL, target domain, anchor text, rel attributes, target attribute, and whether the link is in <nav>/<footer> or in main content.

Step 2: Check link targets and run the audit

Use scripts/check_external_links.py to resolve the HTTP status of each external target (deduplicated per URL) and run all audit checks:

python3 scripts/check_external_links.py external_links.json --output audit_report.json

Add --sponsored-domains amzn.to,partner.example to flag affiliate domains missing rel="sponsored". Add --skip-status-checks to run only the static checks without network requests.

Step 3: Evaluate the audit checks

Evaluate each check defined in references/audit-checks.md. The core checks are:

CheckSeverity
Broken external links (4xx/5xx or unresolvable targets)High
Affiliate/paid links missing rel="sponsored"High
Insecure http:// external linksMedium
Redirected external links (3xx targets)Medium
target="_blank" without rel="noopener"/noreferrerMedium
User-generated content links missing rel="ugc"/nofollowMedium
Generic anchor text on external linksLow
Excessive external links on a page (> 100)Low
Sitewide (nav/footer) external linksLow
Step 4: Produce the report

Write a report following the structure in references/report-template.md:

  1. Summary — pages crawled, external links found, unique domains, issue counts by severity
  2. Domain overview — most-linked external domains with link counts and follow/nofollow split
  3. Issues — one section per failing check, listing source page, target URL, anchor text, and a specific fix
  4. Prioritized action list — ordered by severity and estimated impact
Step 5: Recommend fixes

For each issue, give a concrete, copy-pasteable recommendation, e.g.:

  • Remove or replace the dead link https://gone.example/page on /blog/post-1; link to an archived copy or an equivalent live resource
  • Update http://partner.example on /resources to https://partner.example
  • Add rel="sponsored" to the affiliate link https://amzn.to/xyz on /reviews/widget
  • Add rel="noopener" to the target="_blank" link on /about
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/audit-checks.md — full definitions, thresholds, and rationale for every audit check
  • references/report-template.md — report output structure
  • scripts/extract_external_links.py — crawl a site and build the external link inventory
  • scripts/check_external_links.py — resolve target statuses and run audit checks

Audit Checks Reference (references/audit-checks.md)

Full definitions, severities, and rationale for all nine audit checks the skill evaluates, from broken links to affiliate link compliance.

references/audit-checks.mdView on GitHub

External Link Audit Checks

Full definitions, thresholds, and rationale for each audit check.

High severity
1. Broken external links
  • Definition: An external <a href> whose target returns HTTP 4xx/5xx, or whose host cannot be resolved (link rot).
  • Why it matters: Dead outbound links harm user experience and signal an unmaintained page; pages full of rot tend to lose trust and rankings.
  • Fix: Replace with a live equivalent resource or an archived copy (e.g., Wayback Machine), or remove the link and keep the plain text.
2. Affiliate/paid links missing rel="sponsored"
  • Definition: Links to known affiliate or paid-placement domains (or links with affiliate URL parameters such as tag=, ref=, aff_id=) that lack rel="sponsored" (or legacy nofollow).
  • Why it matters: Google requires paid/affiliate links to be qualified; unflagged paid links risk manual link-scheme penalties.
  • Fix: Add rel="sponsored" (optionally sponsored nofollow) to every paid or affiliate link.
Medium severity
3. Insecure http:// external links
  • Definition: Outbound links using http:// where the destination supports HTTPS.
  • Why it matters: Sends users through an insecure hop, leaks referrer data, and often produces an extra redirect.
  • Fix: Update the link to the https:// URL. If the destination has no HTTPS, consider whether to keep the link at all.
4. Redirected external links
  • Definition: An external link whose target returns 3xx.
  • Threshold: Flag all 3xx targets; treat chains of 2+ hops and redirects to a different domain as higher priority (the destination content may have changed).
  • Fix: Update links to point directly to the final destination URL after verifying it still matches the intended content.
5. target="_blank" without rel="noopener"/noreferrer
  • Definition: External links opening a new tab without rel="noopener" or rel="noreferrer".
  • Why it matters: Legacy browsers expose window.opener to the destination page (reverse tabnabbing); it can also hurt performance.
  • Fix: Add rel="noopener" (modern browsers imply it, but explicit is safest for older browsers).
6. User-generated content links missing rel="ugc"/nofollow
  • Definition: External links inside comments, forums, or other user-submitted areas without rel="ugc" or rel="nofollow".
  • Detection: Ask the user which page sections accept user-generated content if unclear.
  • Why it matters: Unqualified UGC links pass equity to spam targets and invite comment spam.
  • Fix: Add rel="ugc" (or nofollow) to all links in user-submitted content, ideally via the platform template.
Low severity
7. Generic anchor text on external links
  • Definition: External links whose anchor text is non-descriptive.
  • Detection list: "click here", "here", "read more", "learn more", "more", "this page", "link", "source", bare URLs, empty anchors (image links without alt text).
  • Fix: Use descriptive anchor text that tells users and crawlers what the destination is.
8. Excessive external links
  • Definition: Pages with more than ~100 outgoing external links.
  • Why it matters: Can look like a link farm or directory, dilutes the value of each citation, and overwhelms readers.
  • Fix: Trim to the most valuable references; split genuine resource lists into curated, focused pages.
9. Sitewide external links
  • Definition: The same external link appearing in <nav> or <footer> across most pages (e.g., "designed by", widget credits, badge links).
  • Why it matters: Sitewide followed outbound links are a classic spam pattern and leak equity from every page.
  • Fix: Keep only deliberate sitewide links; add rel="nofollow"/sponsored to credit or reciprocal links, or move them to a single about/credits page.
Additional signals to note (informational)
  • Domain concentration — if a large share of external links point to one domain, confirm it's intentional (not injected spam).
  • Suspicious destinations — links to parked domains, expired-domain redirects, gambling/pharma/adult sites the user didn't intend to cite; flag for manual review as possible hacked-content injection.
  • Tracking-parameter bloat — outbound URLs carrying long tracking query strings (utm_*, fbclid); link the clean canonical URL where practical.
  • Linking to competitors with keyword-rich anchors — note for the user to confirm it's intentional.

Report Template (references/report-template.md)

The output structure the skill follows when writing the final audit report.

references/report-template.mdView on GitHub

External Link Audit Report Template

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

# External Link Audit — {site}

**Date:** {date}
**Scope:** {scope description}
**Pages crawled:** {n} | **External links analyzed:** {n} | **Unique domains:** {n}

## Summary

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

{2–3 sentence overview of the site's outbound link health and the biggest wins.}

## Domain overview

| Domain | Links | Followed | Nofollow/Sponsored/UGC | Notes |
|---|---|---|---|---|
| ... | ... | ... | ... | ... |

## High-severity issues

### Broken external links ({n})

| Source page | Broken target | Anchor text | Status | Suggested fix |
|---|---|---|---|---|
| ... | ... | ... | 404 | Replace with {live or archived URL} / remove |

### Affiliate/paid links missing rel="sponsored" ({n})

| Source page | Target | Anchor text | Current rel | Suggested fix |
|---|---|---|---|---|
| ... | ... | ... | (none) | Add rel="sponsored" |

## Medium-severity issues

{One subsection per failing check, same table style: affected pages, details, specific fix.}

## Low-severity issues

{Brief list or table.}

## Prioritized action list

1. {Highest-impact fix, with exact pages/links}
2. ...
Guidance
  • Every issue row must name the exact source URL, target URL, and a concrete fix — never generic advice.
  • Cap tables at ~20 rows in the report body; attach or offer the full list (CSV/JSON) if larger.
  • Order the action list by severity first, then by estimated user/equity impact (sitewide and high-traffic pages first).
  • In the domain overview, sort by link count descending and flag any domain the user may not have intended to link.

Link Extractor Script (scripts/extract_external_links.py)

Crawls a live site or local HTML folder within the configured page and depth limits and extracts every external link into a JSON inventory, including anchor text, rel attributes, and placement context.

scripts/extract_external_links.pyView on GitHub
#!/usr/bin/env python3
"""Crawl a website and build an external (outbound) link inventory as JSON.

Usage:
    python3 extract_external_links.py https://example.com [--max-pages 500] [--max-depth 5] [--output external_links.json]

Output JSON schema:
{
  "site": "https://example.com",
  "pages_crawled": 42,
  "links": [
    {
      "source": "<page url>",
      "target": "<external url>",
      "domain": "<target host>",
      "anchor": "...",
      "rel": "nofollow sponsored|null",
      "target_attr": "_blank|null",
      "location": "nav|footer|content"
    }
  ]
}

Uses only the Python standard library.
"""

import argparse
import json
import sys
import urllib.error
import urllib.parse
import urllib.request
from collections import deque
from html.parser import HTMLParser

USER_AGENT = "ExternalLinkAuditBot/1.0 (+claude-skill)"
TIMEOUT = 15


class LinkParser(HTMLParser):
    def __init__(self, base_url):
        super().__init__()
        self.base_url = base_url
        self.links = []
        self._current = None  # link being captured
        self._anchor_parts = []
        self._context = []  # nav/footer nesting

    def handle_starttag(self, tag, attrs):
        a = dict(attrs)
        if tag in ("nav", "footer"):
            self._context.append(tag)
        elif tag == "a" and a.get("href"):
            href = a["href"].strip()
            if href.startswith(("mailto:", "tel:", "javascript:", "#")):
                return
            target = urllib.parse.urljoin(self.base_url, href)
            target = urllib.parse.urldefrag(target)[0]
            location = self._context[-1] if self._context else "content"
            self._current = {
                "target": target,
                "anchor": "",
                "rel": a.get("rel") or None,
                "target_attr": a.get("target") or None,
                "location": location,
            }
            self._anchor_parts = []
        elif tag == "img" and self._current is not None and a.get("alt"):
            self._anchor_parts.append(a["alt"])

    def handle_endtag(self, tag):
        if tag in ("nav", "footer") and self._context and self._context[-1] == tag:
            self._context.pop()
        elif tag == "a" and self._current is not None:
            self._current["anchor"] = " ".join(" ".join(self._anchor_parts).split())
            self.links.append(self._current)
            self._current = None

    def handle_data(self, data):
        if self._current is not None:
            self._anchor_parts.append(data)


def fetch(url):
    """Return (status, final_url, 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 "text/html" in ctype:
                body = resp.read(2_000_000).decode("utf-8", errors="replace")
            return resp.status, resp.geturl(), body
    except urllib.error.HTTPError as e:
        return e.code, url, 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 external_in(source_url, final_url, html, host):
    """Return (external_link_records, internal_targets) parsed from one page."""
    parser = LinkParser(final_url)
    try:
        parser.feed(html)
    except Exception:
        pass
    external, internal = [], []
    for link in parser.links:
        target = link["target"]
        if not target.startswith(("http://", "https://")):
            continue
        if norm_host(target) == host:
            internal.append(target)
            continue
        external.append({"source": source_url, "domain": norm_host(target), **link})
    return external, internal


def extract_from_cache(cache_path):
    """Pure: collect external links from a shared page cache (from fetch_pages.py).

    No network — the link checking lives in check_external_links.py, which still
    probes the external URLs.
    """
    with open(cache_path, encoding="utf-8") as f:
        cache = json.load(f)
    pages = cache.get("pages", {})
    site = cache.get("site", cache_path)
    host = norm_host(site) if site.startswith(("http://", "https://")) else ""
    if not host:
        for page in pages.values():
            if page.get("final_url", "").startswith(("http://", "https://")):
                host = norm_host(page["final_url"])
                break
    external_links, crawled = [], 0
    for url, page in pages.items():
        html = page.get("html")
        if not html:
            continue
        crawled += 1
        out, _internal = external_in(url, page.get("final_url") or url, html, host)
        external_links.extend(out)
    return site, crawled, external_links


def main():
    ap = argparse.ArgumentParser(description="Crawl a site and collect external links.")
    ap.add_argument("start_url", nargs="?")
    ap.add_argument("--from-cache",
                    help="Extract from a shared page cache produced by fetch_pages.py (no network)")
    ap.add_argument("--max-pages", type=int, default=500)
    ap.add_argument("--max-depth", type=int, default=5)
    ap.add_argument("--output", default="external_links.json")
    args = ap.parse_args()

    if args.from_cache:
        start, pages_crawled, external_links = extract_from_cache(args.from_cache)
    elif not args.start_url:
        ap.error("provide a start_url, or --from-cache page_cache.json")
    else:
        start = args.start_url.rstrip("/")
        host = norm_host(start)
        external_links = []
        crawled = set()
        queue = deque([(start, 0)])
        seen = {start}

        while queue and len(crawled) < args.max_pages:
            url, depth = queue.popleft()
            status, final_url, html = fetch(url)
            crawled.add(url)
            print(f"[{status}] depth={depth} {url}", file=sys.stderr)

            if not html:
                continue
            out, internal = external_in(url, final_url, html, host)
            external_links.extend(out)
            for target in internal:
                if target not in seen and depth + 1 <= args.max_depth:
                    seen.add(target)
                    queue.append((target, depth + 1))
        pages_crawled = len(crawled)

    with open(args.output, "w") as f:
        json.dump({"site": start, "pages_crawled": pages_crawled,
                   "links": external_links}, f, indent=2)
    print(f"Wrote {len(external_links)} external links from {pages_crawled} pages to {args.output}",
          file=sys.stderr)


if __name__ == "__main__":
    main()

Link Checker Script (scripts/check_external_links.py)

Resolves the HTTP status of each external target, runs the audit checks against the link inventory, and outputs the structured audit report data.

scripts/check_external_links.pyView on GitHub
#!/usr/bin/env python3
"""Check external link targets and run audit checks on an inventory from extract_external_links.py.

Usage:
    python3 check_external_links.py external_links.json [--output audit_report.json]
        [--sponsored-domains amzn.to,partner.example] [--skip-status-checks]

Checks implemented (see references/audit-checks.md):
- broken_links: external links to 4xx/5xx or unresolvable targets (high)
- unflagged_sponsored: links to known affiliate/paid domains missing rel sponsored/nofollow (high)
- insecure_links: http:// external links (medium)
- redirected_links: external links to 3xx targets (medium)
- unsafe_blank: target="_blank" without rel noopener/noreferrer (medium)
- generic_anchors: weak anchor text (low)
- excessive_external: pages with > 100 external links (low)
- sitewide_links: same nav/footer external link on 5+ pages (low)

Uses only the Python standard library.
"""

import argparse
import json
import sys
import urllib.error
import urllib.request
from collections import Counter, defaultdict

USER_AGENT = "ExternalLinkAuditBot/1.0 (+claude-skill)"
TIMEOUT = 15

GENERIC_ANCHORS = {
    "click here", "here", "read more", "learn more", "more",
    "this page", "link", "this", "source", "website", "see more", "",
}
MAX_EXTERNAL_LINKS = 100
SITEWIDE_MIN_PAGES = 5
AFFILIATE_PARAMS = ("tag=", "ref=", "aff_id=", "affid=", "affiliate=", "utm_medium=affiliate")


def check_status(url):
    """Return final HTTP status without following redirects (HEAD, GET fallback)."""

    class NoRedirect(urllib.request.HTTPRedirectHandler):
        def redirect_request(self, *args, **kwargs):
            return None

    opener = urllib.request.build_opener(NoRedirect)
    for method in ("HEAD", "GET"):
        req = urllib.request.Request(url, method=method,
                                     headers={"User-Agent": USER_AGENT})
        try:
            with opener.open(req, timeout=TIMEOUT) as resp:
                return resp.status
        except urllib.error.HTTPError as e:
            if e.code in (405, 501) and method == "HEAD":
                continue  # retry with GET
            return e.code
        except Exception:
            return 0  # 0 = DNS/network/connection error
    return 0


def main():
    ap = argparse.ArgumentParser(description="Run external link audit checks.")
    ap.add_argument("inventory", help="external_links.json from extract_external_links.py")
    ap.add_argument("--output", default="audit_report.json")
    ap.add_argument("--sponsored-domains", default="",
                    help="comma-separated affiliate/paid domains requiring rel=sponsored")
    ap.add_argument("--skip-status-checks", action="store_true",
                    help="skip network requests; run only static checks")
    args = ap.parse_args()

    with open(args.inventory) as f:
        data = json.load(f)
    links = data["links"]
    sponsored_domains = {d.strip().lower() for d in args.sponsored_domains.split(",") if d.strip()}

    statuses = {}
    if not args.skip_status_checks:
        for target in sorted({l["target"] for l in links}):
            statuses[target] = check_status(target)
            print(f"[{statuses[target]}] {target}", file=sys.stderr)

    findings = {
        "broken_links": [], "unflagged_sponsored": [], "insecure_links": [],
        "redirected_links": [], "unsafe_blank": [], "generic_anchors": [],
        "excessive_external": [], "sitewide_links": [],
    }

    per_page = Counter()
    sitewide = defaultdict(set)  # nav/footer target -> source pages
    domains = Counter()
    domain_followed = Counter()

    for link in links:
        src, target, anchor = link["source"], link["target"], link["anchor"]
        rel = (link.get("rel") or "").lower()
        entry = {"source": src, "target": target, "anchor": anchor}

        per_page[src] += 1
        domains[link["domain"]] += 1
        qualified = any(r in rel for r in ("nofollow", "sponsored", "ugc"))
        if not qualified:
            domain_followed[link["domain"]] += 1
        if link["location"] in ("nav", "footer"):
            sitewide[target].add(src)

        status = statuses.get(target)
        if status is not None:
            if status >= 400 or status == 0:
                findings["broken_links"].append({**entry, "status": status})
            elif 300 <= status < 400:
                findings["redirected_links"].append({**entry, "status": status})

        if target.startswith("http://"):
            findings["insecure_links"].append(entry)

        is_affiliate = (link["domain"] in sponsored_domains
                        or any(p in target for p in AFFILIATE_PARAMS))
        if is_affiliate and "sponsored" not in rel and "nofollow" not in rel:
            findings["unflagged_sponsored"].append({**entry, "rel": link.get("rel")})

        if (link.get("target_attr") == "_blank"
                and "noopener" not in rel and "noreferrer" not in rel):
            findings["unsafe_blank"].append({**entry, "rel": link.get("rel")})

        if anchor.lower().strip() in GENERIC_ANCHORS:
            findings["generic_anchors"].append(entry)

    for page, count in per_page.items():
        if count > MAX_EXTERNAL_LINKS:
            findings["excessive_external"].append({"page": page, "external_links": count})

    for target, sources in sitewide.items():
        if len(sources) >= SITEWIDE_MIN_PAGES:
            findings["sitewide_links"].append({"target": target, "pages": len(sources)})

    severity = {
        "broken_links": "high", "unflagged_sponsored": "high",
        "insecure_links": "medium", "redirected_links": "medium",
        "unsafe_blank": "medium",
        "generic_anchors": "low", "excessive_external": "low", "sitewide_links": "low",
    }
    summary = {
        "pages_crawled": data.get("pages_crawled"),
        "external_links": len(links),
        "unique_domains": len(domains),
        "issues_by_check": {k: len(v) for k, v in findings.items()},
        "issues_by_severity": {
            level: sum(len(findings[k]) for k, s in severity.items() if s == level)
            for level in ("high", "medium", "low")
        },
    }
    domain_overview = [
        {"domain": d, "links": n, "followed": domain_followed[d],
         "qualified": n - domain_followed[d]}
        for d, n in domains.most_common()
    ]
    report = {"summary": summary, "severity": severity, "findings": findings,
              "domain_overview": domain_overview, "statuses": statuses}

    with open(args.output, "w") as f:
        json.dump(report, f, indent=2)
    print(json.dumps(summary, indent=2))
    print(f"Full report written to {args.output}", file=sys.stderr)


if __name__ == "__main__":
    main()

Explore Other Claude SEO Skills

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 Full SEO Audit The orchestrator skill that runs every individual audit below and consolidates the findings into one prioritized, cross-area report ranked by impact, for a complete technical-SEO review in a single pass. full-seo-audit Open 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