Claude AI

Open Graph Audit Claude Skill

This skill audits a website's Open Graph and Twitter Card (social sharing) metadata and produces an actionable report on how the site's links render when shared. It crawls pages and probes each share image to find missing og:title, og:description, og:image, or og:url; broken, non-image, or relative og:image URLs; og:url that mismatches the canonical; missing twitter:card; over-length social titles and descriptions; and brand-default images reused across many pages.

What The Skill Checks

  • Required tags: missing og:title, og:description, og:image, or og:url, and missing twitter:card.
  • Image health: broken, non-image, or relative og:image and twitter:image URLs, and brand-default images reused across many pages.
  • Consistency and length: og:url that mismatches the canonical, and over-length social titles and descriptions.

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 every Open Graph and Twitter Card tag, then probe each share image and audit the resulting inventory. 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 open-graph-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
open-graph-audit
description
Audit a website's Open Graph and Twitter Card (social sharing) metadata for SEO and link previews. Crawls pages and probes each share image to find missing og:title, og:description, og:image, or og:url; broken or non-image or relative og:image URLs; og:url that mismatches the canonical; missing twitter:card; over-length social titles/descriptions; and brand-default images reused across many pages. Use when the user asks to audit, check, validate, or fix Open Graph tags, og: tags, Twitter Cards, social sharing previews, social metadata, or link preview images on a website.

Open Graph / Social Metadata Audit

Audit a website's Open Graph and Twitter Card tags and produce an actionable report on how the site's links render when shared.

When to use this skill

Use this skill when the user asks to:

  • Audit, validate, or check Open Graph (og:*) or Twitter Card (twitter:*) tags
  • Diagnose why links look wrong / have no image when shared on social or chat
  • Find missing or broken share images, or og:url that points at the wrong URL
  • Improve social click-through and link previews
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. Image probing — whether to probe each og:image/twitter:image for status and content type (default on; --no-probe to skip).
Workflow
Step 1: Crawl and probe social tags

Use scripts/extract_social.py:

python3 scripts/extract_social.py https://example.com --max-pages 500 --output social_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_social.py --from-cache page_cache.json --output social_inventory.json

It records each page's OG and Twitter Card tags, canonical, and meta robots, then probes each unique share image for HTTP status and content type.

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

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

CheckSeverity
Missing og:titleHigh
Missing og:imageHigh
Broken og:image (non-200)High
Missing og:descriptionMedium
Missing og:urlMedium
og:url mismatches canonicalMedium
og:image not an image / relativeMedium
Missing twitter:card (no OG fallback)Low
og:title / og:description too longLow
Duplicate/default share image across many pagesInfo
Step 4: Produce the report

Write a report following references/report-template.md: summary by severity, broken/missing images first, then completeness gaps, then a prioritized action list.

Step 5: Recommend fixes
  • Missing/broken image: add or repair an absolute https:// og:image (1200×630 recommended) that returns 200.
  • Missing og:title/description: add them (page title and meta description are good sources).
  • og:url missing/mismatched: set it to the canonical absolute URL.
  • Relative/non-image og:image: use an absolute URL to a real image file.
  • twitter:card: add summary_large_image for most content.
  • Base every recommendation on observed data; never assume an image loads.
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_social.py — crawl pages and probe share images
  • scripts/audit_social.py — run audit checks against the inventory

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

Open Graph / Twitter Card Audit Checks

Definitions, thresholds, and rationale. Open Graph (og:*) and Twitter Card (twitter:*) tags control how a page looks when shared on social platforms and in messaging apps. They don't directly affect rankings, but they drive click-through and referral traffic, and broken share images make links look untrustworthy.

High severity
1. Missing og:title
  • Definition: Indexable page with no og:title.
  • Why it matters: Platforms fall back to the <title> or a guess; the share card often shows truncated or wrong text.
  • Fix: Add og:title (can mirror or improve on the page title; up to ~90 chars).
2. Missing og:image
  • Definition: Indexable page with no og:image (or og:image:secure_url).
  • Why it matters: Shares render as a bare text link with no thumbnail, sharply reducing engagement.
  • Fix: Add an absolute https:// og:image (recommended 1200×630).
3. Broken og:image
  • Definition: The og:image URL returns a non-200 status.
  • Why it matters: The share preview shows no image at all — worse than a missing tag because it looks broken.
  • Fix: Point at a live image URL; verify it returns 200.
Medium severity
4. Missing og:description
  • Definition: No og:description.
  • Fix: Add a concise description (the meta description is a fine source).
5. Missing og:url
  • Definition: No og:url.
  • Why it matters: og:url is the canonical URL for sharing; without it, engagement can split across tracking-parameter variants of the URL.
  • Fix: Set og:url to the page's canonical absolute URL.
6. og:url mismatch
  • Definition: og:url differs from the page's canonical (or final) URL.
  • Why it matters: Splits social signals or points shares at the wrong URL.
  • Fix: Make og:url equal the canonical URL.
7. og:image not an image / relative
  • Definition: The og:image content-type isn't image/*, or the URL is relative rather than absolute.
  • Why it matters: Most platforms require an absolute URL and a real image; relative or non-image values are ignored.
  • Fix: Use an absolute https:// URL to a real image file.
Low severity
8. Missing twitter:card
  • Definition: No twitter:card and no OG tags to fall back on.
  • Why it matters: Twitter/X can use OG as a fallback, but with neither, links render plain. (If OG is present, this is informational.)
  • Fix: Add twitter:card (summary_large_image for most content).
9. og:title / og:description too long
  • Definition: og:title over ~90 chars or og:description over ~300 chars.
  • Why it matters: Platforms truncate; the cut-off point may drop the key message.
  • Fix: Trim to fit.
Informational (note, not counted as an issue)
  • Duplicate og:image across many pages — one share image reused on 5+ pages. Fine for a brand default, but section- or page-specific images earn more engagement. Listed so you can decide.

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

Open Graph / Twitter Card Audit Report Template

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

# Open Graph / Social Metadata Audit — {site}

**Date:** {date}
**Scope:** {pages crawled, share images probed}
**Pages analyzed:** {n} | **Share images probed:** {n}

## Summary

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

{2–3 sentences: do pages have complete, valid OG tags with working images? Name
the dominant gap.}

## High-severity issues

### Missing og:image / broken og:image ({n})
| Page | Problem | Fix |
|---|---|---|
| ... | no og:image / 404 image | Add/repair an absolute 1200×630 https image |

### Missing og:title ({n})
| Page | Suggested og:title |
|---|---|
| ... | ... |

## Medium-severity issues

{Subsections for missing og:description, missing/mismatched og:url, non-image or
relative og:image. Table with page, current value, corrected value.}

## Low-severity issues

{twitter:card gaps, over-length og:title/og:description, as a brief list.}

## Shared / default images to review ({n})

| Share image | Pages using it | Note |
|---|---|---|
| ... | {n} | brand default vs. should be page-specific |

## Prioritized action list

1. {Highest-impact fix with exact pages and concrete values}
2. ...
Guidance
  • Lead with broken and missing images — those have the most visible impact on shares.
  • Every row names the exact page, the current value, and a concrete replacement. Suggested og:image URLs must be absolute https://.
  • Group template-level gaps (e.g. the whole blog missing og:image) as one fix.
  • Treat duplicate/default images as a judgement call to surface, not a defect.
  • Base every status on the probed image data; never assume an image works.

Social Tag Extractor Script (scripts/extract_social.py)

Crawls pages and extracts every Open Graph (og:*) and Twitter Card (twitter:*) tag.

scripts/extract_social.pyView on GitHub
#!/usr/bin/env python3
"""Crawl a site and build an Open Graph / Twitter Card inventory, probing og:image.

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

Records per page: status, final_url, canonical, meta robots, and all OG and
Twitter Card tags (title, description, image, url, type, card). Each unique
og:image / twitter:image is probed once for HTTP status and content type so the
auditor can flag broken or non-image share images.

Uses only the Python standard library.
"""

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

OG_KEYS = {"og:title", "og:description", "og:image", "og:image:secure_url",
           "og:url", "og:type", "og:site_name", "og:image:width", "og:image:height"}
TW_KEYS = {"twitter:card", "twitter:title", "twitter:description", "twitter:image",
           "twitter:site"}


class SocialParser(HTMLParser):
    def __init__(self, base_url):
        super().__init__(convert_charrefs=True)
        self.base_url = base_url
        self.og = {}
        self.tw = {}
        self.canonical = None
        self.meta_robots = None
        self.links = []

    def handle_starttag(self, tag, attrs):
        a = dict(attrs)
        if tag == "meta":
            prop = (a.get("property") or "").lower()
            name = (a.get("name") or "").lower()
            content = (a.get("content") or "").strip()
            if prop in OG_KEYS and prop not in self.og:
                self.og[prop] = content
            elif name in TW_KEYS and name not in self.tw:
                self.tw[name] = content
            elif name == "robots":
                self.meta_robots = a.get("content")
        elif tag == "link" and (a.get("rel") or "").lower() == "canonical" and a.get("href"):
            self.canonical = urllib.parse.urljoin(self.base_url, a["href"])
        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 = None
            if "html" in ctype or "xml" in ctype:
                body = resp.read(2_000_000).decode("utf-8", errors="replace")
            return resp.status, resp.geturl(), body, ctype
    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, _c = 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):
    p = SocialParser(url)
    try:
        p.feed(html)
    except Exception:
        pass
    return {"og": p.og, "twitter": p.tw, "canonical": p.canonical,
            "meta_robots": p.meta_robots}, p.links


def empty(status, final_url):
    return {"status": status, "final_url": final_url, "og": {}, "twitter": {},
            "canonical": None, "meta_robots": None}


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

    No network — parses the cached HTML for exactly the fields this audit needs.
    (The optional share-image probe still hits the network unless --no-probe.)
    """
    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] = empty(status, final_url)
            continue
        info, _links = parse_page(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, _c = fetch(url)
        print(f"[{status}] {url}", file=sys.stderr)
        if html is None:
            pages[url] = empty(status, final_url)
            continue
        info, links = parse_page(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 probe_images(pages):
    wanted = set()
    for p in pages.values():
        for key in ("og:image", "og:image:secure_url"):
            if p.get("og", {}).get(key):
                wanted.add(p["og"][key])
        if p.get("twitter", {}).get("twitter:image"):
            wanted.add(p["twitter"]["twitter:image"])
    images = {}
    for url in sorted(wanted):
        if not urllib.parse.urlparse(url).scheme:
            images[url] = {"status": 0, "content_type": "", "absolute": False}
            continue
        status, _f, _b, ctype = fetch(url)
        print(f"  og:image [{status}] {url}", file=sys.stderr)
        images[url] = {"status": status, "content_type": ctype, "absolute": True}
    return images


def main():
    ap = argparse.ArgumentParser(description="Build an Open Graph / Twitter Card inventory.")
    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="social_inventory.json")
    ap.add_argument("--url-list", action="store_true")
    ap.add_argument("--no-probe", action="store_true", help="skip probing share images")
    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))

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


if __name__ == "__main__":
    main()

Social Tag Auditor Script (scripts/audit_social.py)

Probes each share image for status and content type, then runs the audit checks and outputs the structured report data.

scripts/audit_social.pyView on GitHub
#!/usr/bin/env python3
"""Audit an Open Graph / Twitter Card inventory and emit findings.

Usage:
    python3 audit_social.py social_inventory.json [--output audit_report.json]

Checks (see references/audit-checks.md):
- missing_og_title / missing_og_description / missing_og_image (high/medium)
- missing_og_url (medium); og_url_mismatch (medium) vs. canonical/final URL
- og_image_broken (high): og:image returns non-200
- og_image_not_image (medium): og:image content-type is not image/*
- og_image_relative (medium): og:image is not an absolute URL
- missing_twitter_card (low): no twitter:card and no OG to fall back on
- og_description_too_long (low > 300 chars) / og_title_too_long (low > 90)
- duplicate_og_image (info): same share image across many pages

Uses only the Python standard library.
"""

import argparse
import json
import sys
import urllib.parse
from collections import defaultdict

OG_TITLE_MAX = 90
OG_DESC_MAX = 300


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


def main():
    ap = argparse.ArgumentParser(description="Run Open Graph / Twitter Card 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", {})
    images = data.get("images", {})

    findings = {k: [] for k in (
        "missing_og_title", "missing_og_description", "missing_og_image",
        "missing_og_url", "og_url_mismatch", "og_image_broken",
        "og_image_not_image", "og_image_relative", "missing_twitter_card",
        "og_title_too_long", "og_description_too_long", "duplicate_og_image")}

    image_use = defaultdict(list)

    for url, page in pages.items():
        if page.get("status", 0) != 200:
            continue
        if "noindex" in (page.get("meta_robots") or "").lower():
            continue
        og = page.get("og", {})
        tw = page.get("twitter", {})
        final_url = page.get("final_url", url)
        canonical = page.get("canonical")

        if not og.get("og:title"):
            findings["missing_og_title"].append({"page": url})
        elif len(og["og:title"]) > OG_TITLE_MAX:
            findings["og_title_too_long"].append(
                {"page": url, "length": len(og["og:title"]), "value": og["og:title"]})

        if not og.get("og:description"):
            findings["missing_og_description"].append({"page": url})
        elif len(og["og:description"]) > OG_DESC_MAX:
            findings["og_description_too_long"].append(
                {"page": url, "length": len(og["og:description"])})

        image = og.get("og:image") or og.get("og:image:secure_url")
        if not image:
            findings["missing_og_image"].append({"page": url})
        else:
            image_use[image].append(url)
            info = images.get(image, {})
            if info:
                if not info.get("absolute", True):
                    findings["og_image_relative"].append({"page": url, "og_image": image})
                elif info.get("status", 0) == 0 or info.get("status", 0) >= 400:
                    findings["og_image_broken"].append(
                        {"page": url, "og_image": image, "status": info.get("status")})
                elif "image" not in (info.get("content_type") or ""):
                    findings["og_image_not_image"].append(
                        {"page": url, "og_image": image, "content_type": info.get("content_type")})

        og_url = og.get("og:url")
        if not og_url:
            findings["missing_og_url"].append({"page": url})
        else:
            expected = norm(canonical) or norm(final_url)
            if norm(og_url) != expected:
                findings["og_url_mismatch"].append(
                    {"page": url, "og_url": og_url, "expected": expected})

        if not tw.get("twitter:card") and not og:
            findings["missing_twitter_card"].append({"page": url})

    for image, urls in image_use.items():
        if len(urls) > 5:
            findings["duplicate_og_image"].append(
                {"og_image": image, "page_count": len(urls), "examples": sorted(urls)[:5]})

    severity = {
        "missing_og_title": "high", "missing_og_image": "high", "og_image_broken": "high",
        "missing_og_description": "medium", "missing_og_url": "medium",
        "og_url_mismatch": "medium", "og_image_not_image": "medium",
        "og_image_relative": "medium",
        "missing_twitter_card": "low", "og_title_too_long": "low",
        "og_description_too_long": "low", "duplicate_og_image": "info",
    }
    summary = {
        "pages_analyzed": sum(1 for p in pages.values() if p.get("status", 0) == 200),
        "share_images_probed": len(images),
        "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

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