Claude AI

Pagination Audit Claude Skill

This skill audits a website's paginated series, blog listings, category pages, and search results, and produces an actionable report focused on keeping deep, paginated content indexable. It crawls pages and inspects canonical tags, meta robots, rel=next/prev, and page-number URL patterns to find paginated pages that canonicalize to page 1 (hiding deep content from the index), noindexed component pages, paginated pages missing a self-canonical, broken or inconsistent rel=next/prev, and redundant ?page=1 first pages.

What The Skill Checks

  • Indexation traps: paginated pages that canonicalize to page 1 (hiding deep content) and noindexed component pages.
  • Canonical and series signals: paginated pages missing a self-canonical and broken or inconsistent rel=next/prev.
  • URL hygiene: redundant ?page=1 first pages that duplicate the series root.

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 pagination signals (canonicals, meta robots, rel=next/prev, and page-number URL patterns), then audit the resulting series. 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 pagination-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
pagination-audit
description
Audit a website's pagination (paginated blog listings, category pages, and search results) for SEO. Crawls pages and inspects canonical tags, meta robots, rel=next/prev, and page-number URL patterns to find paginated pages that canonicalize to page 1 (hiding deep content from the index), noindexed component pages, paginated pages missing a self-canonical, broken or inconsistent rel=next/prev, and redundant ?page=1 first pages. Use when the user asks to audit, check, or fix pagination, paginated pages, rel=next/prev, page 2 indexing, infinite scroll SEO, or category/listing pagination on a website.

Pagination Audit

Audit a website's paginated series and produce an actionable SEO report focused on keeping deep, paginated content indexable.

When to use this skill

Use this skill when the user asks to:

  • Audit or fix pagination on blog/category/search listings
  • Diagnose why page 2+ of a listing isn't indexed
  • Check rel="next" / rel="prev" and canonical handling on paginated pages
  • Review infinite-scroll / "load more" SEO
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. Include listing pages with their ?page=//page/N/ URLs so the crawler sees the series.
  2. Scope — pages to crawl (default 500, --max-pages).
Workflow
Step 1: Crawl and inventory pagination signals

Use scripts/extract_pagination.py:

python3 scripts/extract_pagination.py https://example.com --max-pages 500 --output pagination_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_pagination.py --from-cache page_cache.json --output pagination_inventory.json

It records each page's canonical, meta robots, rel="next"/rel="prev" targets, and the page number detected from the URL (?page=N, /page/N, etc.).

Step 2: Run the audit checks
python3 scripts/audit_pagination.py pagination_inventory.json --output audit_report.json
Step 3: Evaluate the audit checks

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

CheckSeverity
Paginated page canonicalizes to page 1High
Paginated (page 2+) page is noindexedMedium
Paginated page missing a self-canonicalMedium
Broken rel=next/prev targetMedium
Inconsistent rel=next/prev reciprocityLow
First page carries a redundant ?page=1Low
rel=next/prev present (Google ignores it)Info
Step 4: Produce the report

Write a report following references/report-template.md: summary by severity, the canonical-to-page-1 issue first, then indexability and self-canonical gaps, framed as template-level fixes, and a prioritized action list.

Step 5: Recommend fixes
  • Canonical to page 1: make each paginated page self-canonical.
  • Noindex on page 2+: let component pages be indexable, or guarantee deep items are reachable (e.g. via the XML sitemap).
  • Missing self-canonical: add a self-referencing canonical to every page.
  • Broken/inconsistent rel=next/prev: fix the targets or remove them.
  • ?page=1 duplicate: serve page 1 at the bare URL and 301 ?page=1 to it.
  • Frame fixes at the template level; base everything on observed data.
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 and rationale
  • references/report-template.md — report output structure
  • scripts/extract_pagination.py — crawl and inventory pagination signals
  • scripts/audit_pagination.py — run pagination 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

Pagination Audit Checks

Definitions and rationale. Paginated series (blog listings, category pages, search results) are easy to misconfigure in ways that hide content from search engines. Google's current guidance: each page in a series should be a self-canonical, indexable URL; rel="next"/rel="prev" are no longer used for indexing (but aren't harmful).

High severity
1. Paginated page canonicalizes to page 1
  • Definition: A component page (page 2, 3, …) has a rel="canonical" pointing at page 1 of the series.
  • Why it matters: This tells Google page 2+ is a duplicate of page 1, so the items and links that only appear on later pages are dropped from the index. This is the single most damaging pagination mistake.
  • Fix: Make each paginated page self-canonical (canonical = its own URL).
Medium severity
2. Paginated page is noindexed
  • Definition: A component page (page 2+) carries noindex.
  • Why it matters: noindex on paginated pages eventually drops them and causes Google to crawl their outgoing links less — deep items lose discovery.
  • Fix: Let component pages be indexable, or ensure deep items are reachable by another path (e.g. the XML sitemap).
3. Paginated page missing a self-canonical
  • Definition: A page 2+ has no canonical at all.
  • Why it matters: Parameter and ordering variants of the listing can be treated as separate duplicate URLs.
  • Fix: Add a self-referencing canonical to every paginated page.
4. Broken rel=next/prev target
  • Definition: A rel="next"/rel="prev" points at a URL that is non-200 or redirects.
  • Why it matters: Signals a stale or off-by-one pagination template and confuses any consumer still reading these hints.
  • Fix: Point them at the correct live URLs, or remove them.
Low severity
5. Inconsistent rel=next/prev reciprocity
  • Definition: Page A's next is B, but B's prev is not A.
  • Fix: Make the chain reciprocal, or drop rel=next/prev entirely.
6. First page carries a redundant page parameter
  • Definition: The first page is served at ?page=1 (or /page/1) instead of the bare URL.
  • Why it matters: Creates a duplicate of the canonical listing URL.
  • Fix: Serve page 1 at the bare URL and 301 ?page=1 to it.
Informational (note, not counted as an issue)
  • rel=next/prev present — Google announced in 2019 it no longer uses these for indexing. They are not harmful and other engines may use them, so keeping them is fine; just don't rely on them to consolidate a series. Reported so you know they exist and aren't a substitute for the canonical/indexability fixes above.

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

Pagination Audit Report Template

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

# Pagination Audit — {site}

**Date:** {date}
**Scope:** {pages crawled}
**Pages analyzed:** {n} | **Paginated (page 2+) pages:** {n}

## Summary

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

{2–3 sentences: are paginated series self-canonical and indexable? Name the
dominant problem (most often "every page 2+ canonicalizes to page 1").}

## High-severity issues

### Paginated pages canonicalizing to page 1 ({n})
| Page | Page # | Current canonical | Fix |
|---|---|---|---|
| ... | 2 | {page-1 URL} | Self-canonical to its own URL |

## Medium-severity issues

### Noindexed paginated pages ({n})
| Page | Page # | Fix |
|---|---|---|
| ... | 3 | Make indexable, or ensure items are in the sitemap |

### Paginated pages missing a self-canonical ({n})
{table: page, page #, fix}

### Broken rel=next/prev targets ({n})
{table: page, rel, target, status}

## Low-severity issues

{rel=next/prev reciprocity issues; ?page=1 duplicates. Brief list.}

## Note: rel=next/prev usage ({n} pages)

These pages still declare `rel="next"`/`rel="prev"`. Google no longer uses them
for indexing; harmless to keep, but not a substitute for the fixes above.

## Prioritized action list

1. {Highest-impact fix — usually a single template change making page 2+
   self-canonical and indexable across the whole site. Name the affected sections.}
2. ...
Guidance
  • Pagination problems are almost always template-level — one fix to the listing template clears every paginated page. Frame the action list that way and give a couple of example URLs per pattern.
  • Lead with the canonical-to-page-1 issue; it's the one that actually de-indexes content.
  • Every row names the exact URL, its page number, and the corrected behavior.
  • Base everything on the crawled inventory; never invent page numbers or targets.

Pagination Extractor Script (scripts/extract_pagination.py)

Crawls pages and inventories pagination signals: canonical tags, meta robots, rel=next/prev, and page-number URL patterns.

scripts/extract_pagination.pyView on GitHub
#!/usr/bin/env python3
"""Crawl a site and inventory pagination signals (rel=next/prev, canonical, page params).

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

For each page it records status, final_url, canonical, meta robots, the
`rel="next"` / `rel="prev"` link targets (from <link> tags and the HTTP Link
header), and the detected page number from the URL (?page=N, /page/N, etc.).

Uses only the Python standard library.
"""

import argparse
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 = "PaginationAuditBot/1.0 (+claude-skill)"
TIMEOUT = 15

LINK_HEADER_RE = re.compile(r'<([^>]+)>\s*;\s*rel\s*=\s*"?(next|prev)"?', re.I)
# Page-number patterns: ?page=2, &p=2, /page/2, /page/2/, .../2 in a paged path.
PAGE_PATTERNS = [
    re.compile(r'[?&](?:page|p|pg|paged)=(\d+)', re.I),
    re.compile(r'/page/(\d+)/?', re.I),
    re.compile(r'/p/(\d+)/?', re.I),
]


def page_number(url):
    """Return the page number encoded in the URL, or None if it isn't paginated."""
    for pat in PAGE_PATTERNS:
        m = pat.search(url)
        if m:
            return int(m.group(1))
    return None


class PaginationParser(HTMLParser):
    def __init__(self, base_url):
        super().__init__(convert_charrefs=True)
        self.base_url = base_url
        self.rel_next = None
        self.rel_prev = None
        self.canonical = None
        self.meta_robots = None
        self.links = []

    def handle_starttag(self, tag, attrs):
        a = dict(attrs)
        if tag == "link":
            rel = (a.get("rel") or "").lower()
            href = a.get("href")
            if href:
                resolved = urllib.parse.urljoin(self.base_url, href.strip())
                if "next" in rel:
                    self.rel_next = resolved
                elif "prev" in rel:
                    self.rel_prev = resolved
                elif rel == "canonical":
                    self.canonical = resolved
        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])


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 or "xml" in ctype) else None
            return resp.status, resp.geturl(), body, dict(resp.headers)
    except urllib.error.HTTPError as e:
        return e.code, url, None, dict(e.headers or {})
    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, _h = 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 parse_page(url, html, headers):
    p = PaginationParser(url)
    try:
        p.feed(html)
    except Exception:
        pass
    for href, rel in LINK_HEADER_RE.findall(headers.get("Link", "") or ""):
        resolved = urllib.parse.urljoin(url, href.strip())
        if rel.lower() == "next" and not p.rel_next:
            p.rel_next = resolved
        elif rel.lower() == "prev" and not p.rel_prev:
            p.rel_prev = resolved
    return {"canonical": p.canonical, "meta_robots": p.meta_robots,
            "rel_next": p.rel_next, "rel_prev": p.rel_prev,
            "page_number": page_number(url)}, p.links


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

    Reads the cached HTML *and response headers* (the Link: rel=next/prev headers
    this audit needs). No network.
    """
    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, "canonical": None,
                          "meta_robots": None, "rel_next": None, "rel_prev": None,
                          "page_number": page_number(url)}
            continue
        info, _links = parse_page(final_url, html, page.get("headers") or {})
        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, headers = fetch(url)
        print(f"[{status}] {url}", file=sys.stderr)
        if html is None:
            pages[url] = {"status": status, "final_url": final_url, "canonical": None,
                          "meta_robots": None, "rel_next": None, "rel_prev": None,
                          "page_number": page_number(url)}
            continue
        info, links = parse_page(final_url, html, headers)
        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="Build a pagination inventory 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="pagination_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))

    paginated = sum(1 for p in pages.values()
                    if p.get("page_number") or p.get("rel_next") or p.get("rel_prev"))
    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 ({paginated} pagination-related) to {args.output}",
          file=sys.stderr)


if __name__ == "__main__":
    main()

Pagination Auditor Script (scripts/audit_pagination.py)

Audits each paginated series against the checks and outputs the structured report data.

scripts/audit_pagination.pyView on GitHub
#!/usr/bin/env python3
"""Audit a pagination inventory (from extract_pagination.py) and emit findings.

Usage:
    python3 audit_pagination.py pagination_inventory.json [--output audit_report.json]

Checks (see references/audit-checks.md):
- paginated_canonical_to_page1 (high): page 2+ canonicalizes to page 1, hiding its
  content/links from the index (Google says each page should self-canonicalize)
- paginated_noindex (medium): a component (page 2+) page is noindexed
- paginated_missing_self_canonical (medium): page 2+ has no canonical at all
- rel_next_prev_broken (medium): a rel=next/prev target is non-200/redirects
- rel_next_prev_inconsistent (low): A's next is B but B's prev isn't A
- page1_has_page_param (low): the first page carries a redundant ?page=1
- rel_next_prev_present (info): Google no longer uses rel=next/prev for indexing

Uses only the Python standard library.
"""

import argparse
import json
import re
import sys
import urllib.parse

PAGE_PARAM_RE = re.compile(r'([?&])(?:page|p|pg|paged)=\d+', re.I)
PAGE_PATH_RE = re.compile(r'/(?:page|p)/\d+/?', re.I)


def strip_page(url):
    """Return the URL with its page-number component removed (i.e. the page-1 URL)."""
    if not url:
        return ""
    u = PAGE_PATH_RE.sub("/", url)
    u = PAGE_PARAM_RE.sub(lambda m: m.group(1), u)
    u = re.sub(r'[?&]$', '', u)
    return urllib.parse.urldefrag(u)[0].rstrip("/")


def norm(url):
    return urllib.parse.urldefrag(url)[0].rstrip("/") if url else ""


def main():
    ap = argparse.ArgumentParser(description="Run pagination audit checks.")
    ap.add_argument("inventory")
    ap.add_argument("--output", default="audit_report.json")
    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 (
        "paginated_canonical_to_page1", "paginated_noindex",
        "paginated_missing_self_canonical", "rel_next_prev_broken",
        "rel_next_prev_inconsistent", "page1_has_page_param",
        "rel_next_prev_present")}

    # index by normalized final URL for reciprocity + target status lookups
    by_url = {}
    for u, p in pages.items():
        by_url[norm(p.get("final_url", u))] = (u, p)

    for url, page in pages.items():
        if page.get("status", 0) != 200:
            continue
        pn = page.get("page_number")
        canonical = page.get("canonical")
        is_noindex = "noindex" in (page.get("meta_robots") or "").lower()
        rel_next, rel_prev = page.get("rel_next"), page.get("rel_prev")

        if rel_next or rel_prev:
            findings["rel_next_prev_present"].append(
                {"page": url, "next": rel_next, "prev": rel_prev})

        # Component pages (page 2+).
        if pn and pn >= 2:
            if is_noindex:
                findings["paginated_noindex"].append({"page": url, "page_number": pn})
            if not canonical:
                findings["paginated_missing_self_canonical"].append(
                    {"page": url, "page_number": pn})
            elif norm(canonical) == strip_page(page.get("final_url", url)) \
                    and norm(canonical) != norm(page.get("final_url", url)):
                findings["paginated_canonical_to_page1"].append(
                    {"page": url, "page_number": pn, "canonical": canonical})

        if pn == 1:
            findings["page1_has_page_param"].append({"page": url})

        # rel=next/prev target health + reciprocity.
        for rel, target in (("next", rel_next), ("prev", rel_prev)):
            if not target:
                continue
            t = by_url.get(norm(target))
            if t:
                _tu, tp = t
                if tp.get("status", 0) != 200:
                    findings["rel_next_prev_broken"].append(
                        {"page": url, "rel": rel, "target": target,
                         "status": tp.get("status")})
                back = tp.get("rel_prev") if rel == "next" else tp.get("rel_next")
                if back is not None and norm(back) != norm(page.get("final_url", url)):
                    findings["rel_next_prev_inconsistent"].append(
                        {"page": url, "rel": rel, "target": target,
                         "target_points_back_to": back})

    severity = {
        "paginated_canonical_to_page1": "high",
        "paginated_noindex": "medium", "paginated_missing_self_canonical": "medium",
        "rel_next_prev_broken": "medium",
        "rel_next_prev_inconsistent": "low", "page1_has_page_param": "low",
        "rel_next_prev_present": "info",
    }
    summary = {
        "pages_analyzed": sum(1 for p in pages.values() if p.get("status", 0) == 200),
        "paginated_pages": sum(1 for p in pages.values() if (p.get("page_number") or 0) >= 2),
        "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

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