Claude AI

Content Quality Audit Claude Skill

This skill audits a website's content quality and produces an actionable report. It crawls pages and measures visible word count, text-to-HTML ratio, and content fingerprints to find thin content, exact duplicate body text across pages, near-duplicate (templated or boilerplate) pages, pages that are mostly markup with little text, and pages missing an H1. These checks surface pages unlikely to rank or add value, calibrated to the site as signals for human review.

What The Skill Checks

  • Thin and low-value pages: visible word count below the threshold and pages that are mostly markup with a low text-to-HTML ratio.
  • Duplication: exact-duplicate body text across pages and near-duplicate (templated or boilerplate) pages detected via content fingerprints.
  • Structural signals: pages missing an H1, with thin-word and similarity thresholds calibrated to the content type before running.

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 crawl the site and extract visible text and content fingerprints, then audit the resulting inventory for thinness and duplication. 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 content-quality-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 workflow, and the resources the skill relies on.

SKILL.mdView on GitHub
name
content-quality-audit
description
Audit a website's content quality for SEO. Crawls pages and measures visible word count, text-to-HTML ratio, and content fingerprints to find thin content, exact duplicate body text across pages, near-duplicate (templated/boilerplate) pages, pages that are mostly markup with little text, and pages missing an H1. Use when the user asks to audit, check, or fix thin content, duplicate or near-duplicate content, low-value pages, content depth, doorway pages, or text-to-HTML ratio on a website.

Content Quality Audit

Audit a website's content for thinness and duplication, and produce an actionable report. These checks surface pages unlikely to rank or add value; they are signals for human review, calibrated to the site.

When to use this skill

Use this skill when the user asks to:

  • Audit or fix thin / low-value content
  • Find duplicate or near-duplicate pages (templated/boilerplate content)
  • Check content depth or text-to-HTML ratio
  • Identify doorway-style pages
Inputs to collect
  1. Site URL or URL list — a live site root to crawl (auto-seeds from /sitemap.xml), or a text file with --url-list.
  2. Scope — pages to crawl (default 500, --max-pages).
  3. Thresholds — --thin-words (default 300) and --similarity (default 0.85). Calibrate to the content type before running.
Workflow
Step 1: Crawl and measure content

Use scripts/extract_content.py:

python3 scripts/extract_content.py https://example.com --max-pages 500 --output content_inventory.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_content.py --from-cache page_cache.json --output content_inventory.json

For each page it records visible word count, text-to-HTML ratio, title, H1, noindex, an exact content hash, and a 32-value MinHash signature for fast near-duplicate detection.

Step 2: Run the audit checks
python3 scripts/audit_content.py content_inventory.json --thin-words 300 --similarity 0.85 --output audit_report.json
Step 3: Evaluate the audit checks

Evaluate each check in references/audit-checks.md. Core checks:

CheckSeverity
Thin content (below word threshold)High
Duplicate content (identical body)High
Near-duplicate content (high MinHash similarity)Medium
Low text-to-HTML ratioMedium
Missing H1Low
Step 4: Produce the report

Write a report following references/report-template.md: summary with the median word count for calibration, thin and duplicate content first, near-duplicate clusters grouped as patterns, and a prioritized action list.

Step 5: Recommend fixes
  • Thin content: expand with useful content, consolidate several thin pages, or noindex pages that exist for non-search reasons.
  • Duplicate/near-duplicate: canonicalize to one URL, consolidate, or add substantial unique content (cross-check canonical-tag-audit).
  • Low ratio: increase real content or trim boilerplate markup.
  • Missing H1: add one descriptive H1 (see heading-structure-audit).
  • Always caveat JavaScript-rendered pages — this crawler doesn't run JS, so verify a flagged page isn't simply rendered client-side.
  • Base every recommendation on the measured signals; never invent content.
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
  • references/report-template.md — report output structure
  • scripts/extract_content.py — crawl and measure content signals
  • scripts/audit_content.py — run content-quality audit checks

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

Full definitions, severities, and rationale for every check across the high, medium, and low tiers.

references/audit-checks.mdView on GitHub

Content Quality Audit Checks

Definitions, thresholds, and rationale. These checks flag pages that are unlikely to rank or add value because they are too thin, duplicated, or buried under markup. They are signals for human review, not verdicts — "thin" depends on intent (a contact page is meant to be short).

High severity
1. Thin content
  • Definition: An indexable page with a visible word count well below the threshold (default 300).
  • Why it matters: Pages with little unique content rarely satisfy a query and can drag down site-wide quality signals when there are many of them.
  • Fix: Expand with genuinely useful content, consolidate several thin pages into one strong page, or noindex pages that exist for non-search reasons.
  • Caveat: This crawler doesn't run JavaScript — a page populated client-side will look thin. Verify before acting.
2. Duplicate content
  • Definition: Two or more indexable pages whose visible body text is identical (same normalized content hash).
  • Why it matters: Duplicates compete with each other and split signals; search engines pick one and may ignore the rest.
  • Fix: Canonicalize the duplicates to one URL, consolidate them, or differentiate the content. (Cross-check the canonical-tag-audit skill.)
Medium severity
3. Near-duplicate content
  • Definition: Two pages whose MinHash similarity exceeds the threshold (default 0.85) — nearly the same text, e.g. templated pages where only a name or city changes.
  • Why it matters: Doorway-style near-duplicates add little unique value and can be treated as duplicates.
  • Fix: Add substantial unique content per page, or consolidate the set behind one canonical.
4. Low text-to-HTML ratio
  • Definition: Visible text is a very small fraction (default < 10%) of the raw HTML.
  • Why it matters: Often a sign of heavy templating/boilerplate with little real content, or a JS-driven page with minimal server-rendered text.
  • Fix: Increase the proportion of real content, trim bloated markup, or verify server-side rendering of the main content.
Low severity
5. Missing H1
  • Definition: An indexable page with no <h1>.
  • Why it matters: The H1 frames the page topic for users and assistive tech and reinforces relevance. (For full heading analysis, use the heading-structure-audit skill.)
  • Fix: Add a single descriptive H1 reflecting the page's primary topic.
Notes
  • Tune the thresholds to the site. A documentation site and a news site have very different "normal" word counts; pass --thin-words and --similarity accordingly. The summary reports the median word count to help calibrate.
  • JavaScript rendering is the most common false positive across these checks — always confirm a flagged page isn't simply rendered client-side.

Report Template (references/report-template.md)

The output structure the skill follows, including the sections issues are grouped into.

references/report-template.mdView on GitHub

Content Quality Audit Report Template

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

# Content Quality Audit — {site}

**Date:** {date}
**Scope:** {pages crawled}
**Pages analyzed:** {n} | **Median word count:** {n}

## Summary

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

{2–3 sentences: overall content depth (use the median word count), and the
dominant problem (e.g. a large set of near-duplicate templated pages).}

## High-severity issues

### Thin content ({n})
| Page | Words | Suggested action |
|---|---|---|
| ... | 84 | Expand / consolidate / noindex |

### Duplicate content ({n} groups)
| Pages | Words | Suggested action |
|---|---|---|
| {url1}, {url2}, … | 320 | Canonicalize / consolidate / differentiate |

## Medium-severity issues

### Near-duplicate content ({n} pairs)
| Pages | Similarity | Suggested action |
|---|---|---|
| {url1} ↔ {url2} | 0.92 | Add unique content or consolidate |

### Low text-to-HTML ratio ({n})
{table: page, ratio, words}

## Low-severity issues

### Missing H1 ({n})
{brief table}

## Prioritized action list

1. {Highest-impact fix — usually a template-level near-duplicate cluster or a set
   of thin pages to consolidate. Name the pattern and example URLs.}
2. ...

> Note: this audit does not execute JavaScript. Verify that any flagged page isn't
> simply rendered client-side before treating it as thin or empty.
Guidance
  • Calibrate to the site. State the threshold used and the median word count so the user can judge whether "thin" is fair for their content type.
  • Group near-duplicate and thin pages into patterns (e.g. "120 location pages share one template") with a single recommended fix and example URLs.
  • Distinguish fix the content from noindex it — not every thin page should rank; some should simply be removed from the index.
  • Always caveat JavaScript-rendered pages.
  • Base everything on the measured signals; never invent word counts or content.

Content Extractor Script (scripts/extract_content.py)

Crawls pages and measures visible word count, text-to-HTML ratio, and content fingerprints for similarity comparison.

scripts/extract_content.pyView on GitHub
#!/usr/bin/env python3
"""Crawl a site and capture content-quality signals (length, ratio, dedupe sigs).

Usage:
    python3 extract_content.py https://example.com [--max-pages 500] [--output content_inventory.json]
    python3 extract_content.py urls.txt --url-list [--output content_inventory.json]

For each page it records status, title, first H1, visible word count, the
text-to-HTML ratio, whether the page declares noindex, an exact content hash, and
a 32-value MinHash signature of its word shingles so the auditor can find exact
and near-duplicate bodies cheaply (no pairwise text comparison, no dependencies).

Uses only the Python standard library.
"""

import argparse
import hashlib
import json
import re
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 = "ContentQualityAuditBot/1.0 (+claude-skill)"
TIMEOUT = 15
NUM_HASHES = 32
SHINGLE = 4
MASK = (1 << 32) - 1
SEEDS = [0x9E3779B1 * (i + 1) & MASK for i in range(NUM_HASHES)]
SKIP_TEXT_TAGS = {"script", "style", "noscript", "template", "svg"}


class ContentParser(HTMLParser):
    def __init__(self, base_url):
        super().__init__(convert_charrefs=True)
        self.base_url = base_url
        self.title = None
        self.h1 = None
        self.text_chars = 0
        self.tokens = []
        self.links = []
        self._in_title = False
        self._title = []
        self._in_h1 = False
        self._h1 = []
        self._h1_done = False
        self._skip = 0

    def handle_starttag(self, tag, attrs):
        a = dict(attrs)
        if tag in SKIP_TEXT_TAGS:
            self._skip += 1
        if tag == "title":
            self._in_title, self._title = True, []
        elif tag == "h1" and not self._h1_done:
            self._in_h1, self._h1 = True, []
        elif tag == "meta" and (a.get("name") or "").lower() == "robots":
            self.meta_robots = a.get("content")
        elif tag == "a" and a.get("href"):
            href = a["href"].strip()
            if not href.startswith(("mailto:", "tel:", "javascript:", "#")):
                self.links.append(
                    urllib.parse.urldefrag(urllib.parse.urljoin(self.base_url, href))[0])

    meta_robots = None

    def handle_endtag(self, tag):
        if tag in SKIP_TEXT_TAGS and self._skip > 0:
            self._skip -= 1
        if tag == "title":
            self._in_title = False
            self.title = " ".join("".join(self._title).split()) or None
        elif tag == "h1" and self._in_h1:
            self._in_h1, self._h1_done = False, True
            self.h1 = " ".join("".join(self._h1).split()) or None

    def handle_data(self, data):
        if self._in_title:
            self._title.append(data)
        if self._in_h1:
            self._h1.append(data)
        if self._skip == 0:
            stripped = data.strip()
            if stripped:
                self.text_chars += len(stripped)
                self.tokens.extend(re.findall(r"[a-z0-9']+", stripped.lower()))


def minhash(tokens):
    if len(tokens) < SHINGLE:
        shingles = {" ".join(tokens)} if tokens else set()
    else:
        shingles = {" ".join(tokens[i:i + SHINGLE])
                    for i in range(len(tokens) - SHINGLE + 1)}
    if not shingles:
        return None
    hashed = [int(hashlib.md5(s.encode()).hexdigest()[:8], 16) for s in shingles]
    return [min((h ^ seed) & MASK for h in hashed) for seed in SEEDS]


def fetch(url):
    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 = resp.read(2_000_000).decode("utf-8", errors="replace") \
                if "html" in ctype else None
            return resp.status, resp.geturl(), body
    except urllib.error.HTTPError as e:
        return e.code, url, None
    except Exception:
        return 0, url, None


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


def pages_from_sitemap(site):
    status, _f, body = fetch(urllib.parse.urljoin(site, "/sitemap.xml"))
    if status != 200 or not body:
        return []
    try:
        root = ET.fromstring(body)
        return [loc.text.strip() for loc in
                root.iter("{http://www.sitemaps.org/schemas/sitemap/0.9}loc") if loc.text]
    except ET.ParseError:
        return []


def analyze(url, html):
    p = ContentParser(url)
    try:
        p.feed(html)
    except Exception:
        pass
    word_count = len(p.tokens)
    ratio = round(p.text_chars / len(html), 4) if html else 0
    norm_text = " ".join(p.tokens)
    info = {
        "title": p.title, "h1": p.h1, "word_count": word_count,
        "text_to_html_ratio": ratio,
        "noindex": "noindex" in (p.meta_robots or "").lower(),
        "content_hash": hashlib.md5(norm_text.encode()).hexdigest() if norm_text else None,
        "minhash": minhash(p.tokens),
    }
    return info, p.links


def extract_from_cache(cache_path):
    """Pure: turn a shared page cache (from fetch_pages.py) into a content inventory.

    No network — analyzes the cached HTML for exactly the signals this audit needs.
    """
    with open(cache_path, encoding="utf-8") as f:
        cache = json.load(f)
    pages = {}
    for url, page in cache.get("pages", {}).items():
        html = page.get("html")
        status = page.get("status", 0)
        final_url = page.get("final_url") or url
        if not html:
            pages[url] = {"status": status, "final_url": final_url, "title": None,
                          "h1": None, "word_count": 0, "text_to_html_ratio": 0,
                          "noindex": False, "content_hash": None, "minhash": None}
            continue
        info, _links = analyze(final_url, html)
        info["status"] = status
        info["final_url"] = final_url
        pages[url] = info
    return cache.get("site", cache_path), pages


def collect(urls, max_pages, crawl_host=None):
    pages, queue, seen = {}, deque(urls), set(urls)
    while queue and len(pages) < max_pages:
        url = queue.popleft()
        status, final_url, html = fetch(url)
        print(f"[{status}] {url}", file=sys.stderr)
        if html is None:
            pages[url] = {"status": status, "final_url": final_url, "title": None,
                          "h1": None, "word_count": 0, "text_to_html_ratio": 0,
                          "noindex": False, "content_hash": None, "minhash": None}
            continue
        info, links = analyze(final_url, html)
        info["status"] = status
        info["final_url"] = final_url
        pages[url] = info
        if crawl_host:
            for link in links:
                if norm_host(link) == crawl_host and link not in seen:
                    seen.add(link)
                    queue.append(link)
    return pages


def main():
    ap = argparse.ArgumentParser(description="Capture content-quality signals for a site.")
    ap.add_argument("source", 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("--output", default="content_inventory.json")
    ap.add_argument("--url-list", action="store_true")
    args = ap.parse_args()

    if args.from_cache:
        site, pages = extract_from_cache(args.from_cache)
    elif not args.source:
        ap.error("provide a source, or --from-cache page_cache.json")
    elif args.url_list:
        with open(args.source, encoding="utf-8") as f:
            urls = [l.strip() for l in f if l.strip() and not l.startswith("#")]
        site, pages = args.source, collect(urls[: args.max_pages], args.max_pages)
    else:
        start = args.source.rstrip("/")
        seeds = pages_from_sitemap(start) or [start]
        site, pages = args.source, collect(seeds[: args.max_pages], args.max_pages,
                                           crawl_host=norm_host(start))

    with open(args.output, "w", encoding="utf-8") as f:
        json.dump({"site": site, "pages": pages}, f, indent=2, ensure_ascii=False)
    print(f"Wrote {len(pages)} pages to {args.output}", file=sys.stderr)


if __name__ == "__main__":
    main()

Content Auditor Script (scripts/audit_content.py)

Compares fingerprints across the crawl to flag thin, duplicate, and near-duplicate pages, then outputs the structured report data.

scripts/audit_content.pyView on GitHub
#!/usr/bin/env python3
"""Audit a content inventory (from extract_content.py) and emit findings.

Usage:
    python3 audit_content.py content_inventory.json [--output audit_report.json]
        [--thin-words 300] [--similarity 0.85]

Checks (see references/audit-checks.md):
- thin_content (high): indexable page well under the word-count threshold
- duplicate_content (high): two or more indexable pages with identical body text
- near_duplicate_content (medium): pages whose MinHash similarity exceeds the
  threshold (templated/boilerplate pages with little unique text)
- low_text_to_html_ratio (medium): page is mostly markup, little text
- missing_h1 (low): indexable page with no H1 to frame the content

Near-duplicate detection uses the 32-value MinHash signatures from the collector,
so it is O(n^2) over small fixed-size signatures — fast and dependency-free.

Uses only the Python standard library.
"""

import argparse
import json
import sys
from collections import defaultdict


def mh_similarity(a, b):
    if not a or not b or len(a) != len(b):
        return 0.0
    return sum(1 for x, y in zip(a, b) if x == y) / len(a)


def main():
    ap = argparse.ArgumentParser(description="Run content-quality audit checks.")
    ap.add_argument("inventory")
    ap.add_argument("--output", default="audit_report.json")
    ap.add_argument("--thin-words", type=int, default=300)
    ap.add_argument("--similarity", type=float, default=0.85,
                    help="MinHash similarity above which pages are near-duplicates")
    ap.add_argument("--min-ratio", type=float, default=0.1,
                    help="text-to-HTML ratio below which a page is mostly markup")
    args = ap.parse_args()

    with open(args.inventory, encoding="utf-8") as f:
        data = json.load(f)
    pages = data.get("pages", {})

    findings = {k: [] for k in (
        "thin_content", "duplicate_content", "near_duplicate_content",
        "low_text_to_html_ratio", "missing_h1")}

    indexable = {u: p for u, p in pages.items()
                 if p.get("status", 0) == 200 and not p.get("noindex")}

    # Exact duplicates by content hash.
    hash_groups = defaultdict(list)
    for url, p in indexable.items():
        if p.get("content_hash"):
            hash_groups[p["content_hash"]].append(url)
    exact_dupe_urls = set()
    for h, urls in hash_groups.items():
        if len(urls) > 1:
            findings["duplicate_content"].append(
                {"pages": sorted(urls), "word_count": indexable[urls[0]].get("word_count")})
            exact_dupe_urls.update(urls)

    # Per-page checks.
    for url, p in indexable.items():
        wc = p.get("word_count", 0)
        if wc < args.thin_words:
            findings["thin_content"].append(
                {"page": url, "word_count": wc, "title": p.get("title")})
        if 0 < p.get("text_to_html_ratio", 0) < args.min_ratio:
            findings["low_text_to_html_ratio"].append(
                {"page": url, "ratio": p.get("text_to_html_ratio"), "word_count": wc})
        if not p.get("h1"):
            findings["missing_h1"].append({"page": url, "title": p.get("title")})

    # Near-duplicates via MinHash (skip pages already flagged as exact dupes).
    items = [(u, p["minhash"]) for u, p in indexable.items()
             if p.get("minhash") and u not in exact_dupe_urls]
    seen_pairs = set()
    for i in range(len(items)):
        ui, si = items[i]
        for j in range(i + 1, len(items)):
            uj, sj = items[j]
            sim = mh_similarity(si, sj)
            if sim >= args.similarity:
                key = tuple(sorted((ui, uj)))
                if key not in seen_pairs:
                    seen_pairs.add(key)
                    findings["near_duplicate_content"].append(
                        {"pages": list(key), "similarity": round(sim, 2)})

    severity = {
        "thin_content": "high", "duplicate_content": "high",
        "near_duplicate_content": "medium", "low_text_to_html_ratio": "medium",
        "missing_h1": "low",
    }
    word_counts = [p.get("word_count", 0) for p in indexable.values()]
    summary = {
        "pages_analyzed": len(indexable),
        "median_word_count": sorted(word_counts)[len(word_counts) // 2] if word_counts else 0,
        "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", "info")},
    }
    report = {"summary": summary, "severity": severity, "findings": findings}
    with open(args.output, "w", encoding="utf-8") as f:
        json.dump(report, f, indent=2, ensure_ascii=False)
    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

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 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