seo-ai-crawlers · diff
git:20260906.fa9c08a to git:20260906.683d4d0
62 added, 18 removed. Audit A to A.
---
name: seo-ai-crawlers
- description: Audit AI crawler access and citability for a page — confirm retrieval/citation bots (OAI-SearchBot, Claude-SearchBot, PerplexityBot) are allowed and the Googlebot vs Google-Extended split is correct, classify training vs search/retrieval vs user-fetch user-agents, check the page is server-rendered enough for non-JS AI crawlers, and generate a choice-gated robots.txt preset. Module M14. Feeds the AI Visibility score.
+ description: Audit AI crawler access and Google AI-feature eligibility for a page — check the snippet controls that actually gate AI Overviews and AI Mode (noindex, nosnippet, max-snippet, data-nosnippet), resolve per-bot effective access from robots.txt for the audited path (training vs retrieval vs user-fetch tokens), read Content-Signal posture, diff what different user-agents are served, confirm non-JS crawlers can read the page, and generate a choice-gated robots.txt preset. Module M14. Feeds the AI Visibility score.
allowed-tools: Read, Grep, Glob, WebFetch, Bash
---
# seo-ai-crawlers (M14)
- Controls whether AI search engines can crawl and cite the page, and whether they can read it without JS. The training-vs-search-vs-fetch distinction is everything. Reference: `references/ai-crawlers.md`. AI discovery files (llms.txt, agents.md, /.well-known/ucp, agentic sitemap) are module M21 — see `seo-ai-discovery`.
+ Two separate questions live in this module, and only one of them has vendor documentation behind it:
+ 1. **Google's AI features.** A page can appear in AI Overviews / AI Mode only if it is indexed and eligible to be shown in Google Search **with a snippet**. Those gates are documented, so `M14.ai_eligibility.*` carries the only `established` findings here.
+ 2. **Third-party AI crawlers.** Who may fetch and cite the page — OpenAI, Anthropic, Perplexity, Apple, Amazon, Meta, Mistral, DuckDuckGo, Cohere. Access is documented per user-agent; the citation *effect* is not, so those findings stay `directional`.
+
+ The training-vs-retrieval-vs-user-fetch distinction decides every robots recommendation. Reference: `references/ai-crawlers.md` (UA table with sources, Content-Signal, eligibility gates, presets). AI discovery files (llms.txt, agents.md, `/.well-known/ucp`, agentic sitemap) are module M21 — see `seo-ai-discovery`; agentic-commerce readiness is M18 (`seo-ecommerce`).
+
## Inputs
- Work from the PageSnapshot named in your dispatch envelope: read `parsed` from `<run_dir>/pages/<slug>.json` plus `robots_directives`, `headers` (`x-robots-tag`), and `render{needed,used,delta}`; the parsed robots file with per-bot verdicts for the audited URL is `<run_dir>/site/robots.json`; Grep `pages/<slug>.html` for verbatim evidence. Deterministic findings already emitted by `audit.mjs` are listed in `<run_dir>/findings.deterministic.json` — do not re-emit those ids; add model-judged findings only. If invoked directly with a URL/path and no snapshot exists, first run `node "${CLAUDE_PLUGIN_ROOT}/scripts/snapshot.mjs" <target> --out "${CLAUDE_PLUGIN_DATA}/runs"` and use the printed snapshot path. <!-- reconcile-flags-v0.2 -->
+ Work from the PageSnapshot named in your dispatch envelope: read `parsed` from `<run_dir>/pages/<slug>.json` plus `robots_directives`, `headers` (`x-robots-tag`), and `render{needed,used,delta}`; Grep `pages/<slug>.html` for verbatim evidence. `<run_dir>/site/robots.json` carries, for the **audited path**, `verdicts`, `effective_access{[bot]:{class, fetch, url_allowed, via, rule, signals, doc_url, robots_reliability}}`, `content_signals{present, placement, global, by_group[], malformed[], cloudflare_managed_hint}`, `groups`, and `ua_table_version`. When the run included a user-agent diff, `<run_dir>/ua-diff.json` holds per-UA status/title/h1/word_count/canonical/robots/jsonld. Deterministic findings already emitted by `audit.mjs` are listed in `<run_dir>/findings.deterministic.json` — do not re-emit those ids; add model-judged findings only. If invoked directly with a URL/path and no snapshot exists, first run `node "${CLAUDE_PLUGIN_ROOT}/scripts/snapshot.mjs" <target> --out "${CLAUDE_PLUGIN_DATA}/runs"` and use the printed snapshot path. <!-- reconcile-flags-v0.2 -->
## Audits
Working from the PageSnapshot (`parsed_rendered` when `render.used` is not `none`, else `parsed`) plus `site/robots.json`:
- 1. **Citation access**: are retrieval/citation bots — `OAI-SearchBot`, `Claude-SearchBot`, `PerplexityBot`, `Bingbot` — actually allowed (not caught by a broad `Disallow: /` or a wildcard block)? Confirm `Googlebot` is not blocked and the `Googlebot` (search) vs `Google-Extended` (Gemini training control) split is correct.
- 2. **User-agent classification**: bucket every AI agent in `robots.txt` into **training** (GPTBot, ClaudeBot, Google-Extended, Applebot-Extended, CCBot), **search/retrieval** (OAI-SearchBot, Claude-SearchBot, PerplexityBot), and **user-triggered fetch** (ChatGPT-User, Claude-User, Perplexity-User). Match user-agents case-insensitively; treat the table in `references/ai-crawlers.md` as a starting set, not exhaustive.
- 3. **Renderability for non-JS crawlers**: use the M4 `render` block — most AI crawlers do not execute JS. If primary content only appears in `rendered_html_path` and is absent from `raw_html_path`, flag it as invisible to AI retrieval.
+ 1. **Google AI-feature eligibility (the established gate).** Collect **every** `<meta name="robots">` / `<meta name="googlebot">` tag *and* every `X-Robots-Tag` header value (header and meta both count; the most restrictive wins). Read `noindex`/`none`, `nosnippet`, `max-snippet:N`, and the `data-nosnippet` attributes — for `data-nosnippet`, measure how much of the main content it covers and specifically whether it wraps the H1 or the lead passage. Also confirm Googlebot itself is not disallowed for this path. Record the **scope**: a directive coming from a template or a site-wide header is `scope: template|site`; one hand-written into a single document is `scope: page`.
+ 2. **Effective per-bot access.** Read `effective_access` for the audited path — not for `/`. A site can allow `/` and disallow `/blog/`, so root-level posture is not an answer. Check the retrieval/citation bots (`OAI-SearchBot`, `Claude-SearchBot`, `PerplexityBot`, `Amazonbot`, `DuckAssistBot`, `Google-CloudVertexBot`), the classic engines (`Googlebot`, `Bingbot`, `Applebot`), the training tokens (`GPTBot`, `ClaudeBot`, `Google-Extended`, `Applebot-Extended`, `CCBot`, `Meta-ExternalAgent`, `cohere-ai`, `Bytespider`), the user-fetch tokens (`ChatGPT-User`, `Claude-User`, `Claude-Web`, `Perplexity-User`, `Meta-ExternalFetcher`, `MistralAI-User`), and `OAI-AdsBot`. Current OpenAI tokens ship as `GPTBot/1.4` and `OAI-SearchBot/1.4`; match user-agents case-insensitively on the token, never on the full UA string. Anthropic publishes its crawler IP ranges at `claude.com/crawling/bots.json`. Quote `ua_table_version` in the report so a stale table is visible.
+ 3. **Content-Signal posture.** `content_signals` reports the `search` / `ai-input` / `ai-train` keys, whether the line is global (Cloudflare's managed robots.txt appends it after all groups) or inside a `User-agent` group, and any malformed pairs. A group line overrides the global line for that group. **A `Disallow` always wins over a signal**: `Disallow: /` blocks the bot whatever `ai-train` says, and `Allow` + `ai-input=no` means the page is still fetchable while consent is withheld — say exactly that, never "blocked".
+ 4. **User-agent divergence.** Compare what the origin serves to `default`, `googlebot`, `gptbot`, `oai-searchbot`, and `claude-searchbot`: differing status, title, H1, word count, canonical, robots directives, or JSON-LD. Separate a *bot challenge* (403/429/503, interstitial, CAPTCHA markup, near-zero word count) from a genuine content difference. The search-axis cloaking judgment — Googlebot vs a browser being served different content — belongs to **M2** (`M2.cloaking.ua_content_divergence`); M14 owns only the AI-bot access side.
+ 5. **Renderability for non-JS crawlers.** Use the M4 `render` block. If the H1, the primary body, or the JSON-LD exists only in `rendered_html_path` and is absent from `raw_html_path`, the page is not reliably readable by AI crawlers that do not execute JavaScript.
+ 6. **Google-Extended sanity.** If `Google-Extended` is disallowed, state plainly that this does **not** remove the site from AI Overviews or AI Mode — it limits Gemini training and grounding only.
+
## Fixes
- - **AUTO** (`fixable: auto`): a citation-friendly `robots.txt` preset, **choice-gated** — the user picks `allow-citations` (default: allow search/retrieval, opt out of training), `allow-all`, or `block-all`. Deterministic, additive, verifiable; emitted as a diff for `fix`.
- - **ADVISORY** (`fixable: advisory`): edge/WAF block for bots that ignore `robots.txt` (e.g. Bytespider) — the tool never writes infra config.
+ - **AUTO** (`fixable: auto`): a citation-friendly `robots.txt` preset, **choice-gated** — the user picks `allow-citations` (allow search/retrieval, opt out of training), `allow-all`, or `block-all`. Presets in `references/ai-crawlers.md`; each ends with a `Content-Signal:` line that restates the same intent in that vocabulary. Deterministic, additive, verifiable; emitted as a diff for `fix`.
+ - **PROPOSED** (`fixable: proposed`): removing a `nosnippet` / `max-snippet:0` / `noindex` that the user confirms is unintended, and lifting `data-nosnippet` off the primary content. Never auto-written: a snippet control is often deliberate (paywall, licensing).
+ - **ADVISORY** (`fixable: advisory`): edge/WAF rules — unblocking a challenged AI bot, or blocking an agent that ignores `robots.txt` (e.g. `Bytespider`). The tool never writes infrastructure config.
**Never fabricate** sitemap URLs, contact emails, or link targets — ask the user or leave a clearly-marked `TODO` placeholder.
## Verification
- - `node "${CLAUDE_PLUGIN_ROOT}/scripts/parse-robots-sitemap.mjs" --snapshot <pages/<slug>.json>` (or `--url <u>`) (`robots_parse`) — parses `robots.txt`, resolves the effective directive for each AI user-agent against the audited path, and confirms it. <!-- reconcile-flags-v0.2 -->
- - `dom_assert` against the M4 render result for the renderability check (content present in `raw_html_path`).
- - When the required data tier is unavailable (e.g. `robots.txt` unfetchable, no M4 render result), status is `needs_api` — **never** a false `pass`.
+ - `node "${CLAUDE_PLUGIN_ROOT}/scripts/parse-robots-sitemap.mjs" --url <final_url> --path <audited path>` (method `robots_parse`) — resolves the effective directive per user-agent **against the audited path** and returns `content_signals`, `effective_access`, and `ua_table_version`. Use `--robots <robots_url>` or `--file <path>` for an offline robots file. <!-- reconcile-flags-v0.2 -->
+ - `node "${CLAUDE_PLUGIN_ROOT}/scripts/ai-eligibility.mjs" --snapshot <pages/<slug>.json>` (or `--url <u>`) — every meta-robots/`X-Robots-Tag` directive, `max-snippet` value, and the `data-nosnippet` coverage share, including whether it wraps the H1/lead (methods `dom_assert` + `header_check`). <!-- reconcile-flags-v0.2 -->
+ - `node "${CLAUDE_PLUGIN_ROOT}/scripts/ua-diff.mjs" --url <final_url> --ua default,googlebot,gptbot,oai-searchbot,claude-searchbot` (method `ua_diff`) — per-UA response fields plus challenge detection. <!-- reconcile-flags-v0.2 -->
+ - `render_diff` against the M4 result for the renderability check (primary content present in `raw_html_path`).
+ - When the required tier is unavailable (`robots.txt` unfetchable, no M4 render result, no UA diff run), status is `needs_api` or `manual_review` — **never** a false `pass`.
## Findings
- Findings conform to `schema/finding.schema.json`. `evidence.observed` quotes the page/robots line; `verification.reproduce` is the runnable command above; `expected_impact` is banded + confidence-tagged (no naked %). Examples:
- - `M14.citation_bots.blocked` — `Disallow: /` reaches `OAI-SearchBot`/`Claude-SearchBot`/`PerplexityBot` (status `fail`, severity 4, `fixable: auto`, axis `ai`, confidence `established`).
- - `M14.render.content_js_only` — primary content in `rendered_html_path` but absent from `raw_html_path`, invisible to non-JS AI crawlers (status `warn`, severity 4, `fixable: advisory`, axis `ai`, confidence `directional`).
+ Findings conform to `schema/finding.schema.json`. `evidence.observed` quotes the page, header, or robots line verbatim; `verification.reproduce` is one of the runnable commands above; `expected_impact` is banded + confidence-tagged (no naked %). **Severity policy**: 5 is reserved for catastrophic, eligibility-killing facts at site/template scope; 4 major · 3 moderate · 2 minor · 1 cosmetic · 0 informational — and only an `established` severity-5 `fail` in an active category can cap a score (`references/scoring-model.md`).
+ **Google AI-feature eligibility** — axis `ai`, confidence `established` except where noted; sources: Google *AI features and your website* and the robots-meta / `X-Robots-Tag` spec:
+ - `M14.ai_eligibility.not_indexable` — `noindex`/`none`, or Googlebot disallowed for this path (`fail`, severity **5** at site/template scope, **4** for a single page, `fixable: proposed`).
+ - `M14.ai_eligibility.nosnippet` — `nosnippet` in a meta tag or header (`fail`, severity **5** site/template, **4** single page, `fixable: proposed`).
+ - `M14.ai_eligibility.max_snippet_zero` — `max-snippet:0` (`fail`, severity **5** site/template, **4** single page, `fixable: proposed`).
+ - `M14.ai_eligibility.max_snippet_low` — a positive `max-snippet:N` below our **50-word** cutoff (`warn`, severity 2, `fixable: advisory`, confidence `directional` — the cutoff is ours, Google publishes no threshold).
+ - `M14.ai_eligibility.data_nosnippet_primary` — `data-nosnippet` wraps the H1 or the lead passage (`fail`, severity 4, `fixable: proposed`).
+ - `M14.ai_eligibility.data_nosnippet_partial` — `data-nosnippet` present but only around ancillary blocks (`pass`, severity 1).
+ - `M14.ai_eligibility.ok` — indexable and snippet-eligible, no suppressing directive (`pass`, severity 3).
+ - `M14.google_extended.blocked_info` — `Google-Extended` disallowed; informational, because it does not affect AI Overviews or AI Mode (`pass`, severity 1).
+
+ **Third-party crawler access** — axis `ai`:
+ - `M14.citation_bots.blocked` — a `Disallow` reaches `OAI-SearchBot` / `Claude-SearchBot` / `PerplexityBot` (or another retrieval bot) on the audited path (`fail`, severity 4, `fixable: auto`, confidence `established` — the vendors document the token and that they honor robots.txt; the citation loss itself is stated as `directional` in `rationale`).
+ - `M14.retrieval.allowed` — every retrieval/citation bot may fetch the audited path (`pass`, severity 4, confidence `directional`).
+ - `M14.render.content_js_only` — primary content, H1, or JSON-LD only in `rendered_html_path` (`warn`, severity 4, `fixable: advisory`, confidence `directional`).
+ - `M14.access.ai_bot_challenged` — an AI user-agent gets a challenge/interstitial or a near-empty body where the default UA gets the page (`warn`, severity 4, `fixable: advisory`, confidence `directional`).
+ - `M14.access.googlebot_blocked_at_edge` — the spoofed `Googlebot` UA is refused at the edge (`warn`, severity 2, `fixable: advisory`, confidence `speculative` — refusing an unverified spoof is legitimate defence; verify with reverse DNS before acting).
+
+ **Content-Signal** — axis `ai`, **never `established`** (no vendor documents compliance; the IETF draft expired 2026-04 and defines vocabulary only):
+ - `M14.content_signal.ai_input_no` — `ai-input=no` (`warn`, severity 3, `fixable: advisory`, `directional`): consent withheld for answer-engine input while the page stays fetchable.
+ - `M14.content_signal.search_no` — `search=no` (`warn`, severity 3, `fixable: advisory`, `directional`).
+ - `M14.content_signal.ai_train_no` — `ai-train=no` (`pass`, severity 1): a legitimate, low-cost preference that costs no citations.
+ - `M14.content_signal.malformed` — unknown key, non-`yes|no` value, or an unparseable pair (`warn`, severity 2, `fixable: auto`).
+ - `M14.content_signal.absent` — no `Content-Signal` line (`not_applicable`, severity 0 — absence is not a defect).
+
+ **Probes** (owned by the `geo` command, listed here because they carry the M14 prefix): `M14.probe.web_presence` and `M14.gsc_ai.impressions_present|zero_impressions|unavailable` are **severity 0**, live under `report.json.probes`, and never move or cap a score.
+
## Honesty
- - Blocking a **training** bot does NOT block the matching **search** bot — they are separate user-agents (`GPTBot` ≠ `OAI-SearchBot`, `ClaudeBot` ≠ `Claude-SearchBot`). Many "block AI" guides get this wrong.
- - `Bytespider` and some agents frequently ignore `robots.txt`; a robots rule is best-effort. Real enforcement needs an edge rule / WAF (advisory).
- - `robots.txt` controls **crawling**, not **indexing** — to keep a page out, use a `noindex` meta tag and don't also `Disallow` it.
- - llms.txt and the other discovery files are not part of this module and never move the M14 category — they are reported under M21 at weight 0.
+ Google's own statements bound every claim in this module:
+ - A page reaches AI Overviews / AI Mode by being **indexed and snippet-eligible** in Search. There is no AI-specific file, token, schema type, or rewriting step that adds eligibility.
+ - **`Google-Extended` does not affect AI Overviews or AI Mode.** It limits training and Gemini/Vertex grounding. Blocking it is a training choice, not an AI-visibility choice.
+ - **Google Search ignores `llms.txt`.** It is reported under M21 at weight 0 and never moves the M14 category.
+ - Search Console's generative-AI report exposes **impressions only**, and the Search Analytics API exposes no AI dimensions (checked 2026-08) — the tool never claims API-level AI citation data.
+
+ Further caveats:
+ - **Content-Signal is metadata, not enforcement.** No AI vendor documents compliance with it; every `M14.content_signal.*` finding is `directional` at most. A `Disallow` line does the work — never present a signal as a substitute, and never call an `ai-input=no` page "blocked" when it is still served.
+ - **UA-diff results are spoofed-UA results.** We send the token, not a verified crawler: a site may legitimately refuse an unverified `Googlebot` string, and a CDN may serve a challenge to any unknown client. Treat divergence as a lead to verify (reverse DNS, server logs, Search Console URL Inspection), never as proof of cloaking. That is why `M14.access.googlebot_blocked_at_edge` is `speculative`.
+ - Blocking a **training** bot does not block the matching **search** bot — `GPTBot` ≠ `OAI-SearchBot`, `ClaudeBot` ≠ `Claude-SearchBot`. Most "block AI" guides get this wrong and cost the site citations.
+ - `Bytespider` and some user-fetchers (Meta documents that `Meta-ExternalFetcher` may bypass robots.txt) do not reliably honor `robots.txt`; a robots rule is best-effort and real enforcement needs an edge rule (advisory).
+ - `robots.txt` controls **crawling**, not **indexing** — to keep a page out of the index use `noindex` and do *not* also `Disallow` it, or the crawler never sees the directive.