Skip to content

feat(seo): open the crawler policy to every operator - #10474

Merged
MarkusNeusinger merged 2 commits into
mainfrom
feat/open-robots-policy-to-all-llms
Aug 18, 2026
Merged

feat(seo): open the crawler policy to every operator#10474
MarkusNeusinger merged 2 commits into
mainfrom
feat/open-robots-policy-to-all-llms

Conversation

@MarkusNeusinger

Copy link
Copy Markdown
Owner

The change

Retrieval, citation, search indexing and model training are now permitted for every operator, replacing the retrieval-yes / training-no split from #9633.

Before After
Retrieval agents allowed allowed
Training crawlers (GPTBot, CCBot, Amazonbot, meta-externalagent) declined allowed
Google-Extended declined allowed
Applebot-Extended declined allowed
ClaudeBot allowed allowed
Bytespider declined declined (bandwidth, not policy)
Content-Signal ai-train=no + Art. 4 reservation ai-train=yes, reservation removed

Why

The split was never coherent here. The catalogue is MIT-licensed and published to be reused — every page ships its runnable source, /llms.txt describes the site for agents, an MCP server exists so assistants can query it directly. Declining a training crawler protected nothing the licence had not already granted, and cost reach.

It also had a consequence nobody intended. Google-Extended is a single token governing Gemini grounding and training together; no finer control exists, verified against Google's crawler documentation. Declining it as though it were training-only kept anyplot out of Gemini's answers entirely. Applebot-Extended really is training-only (retrieval rides on Applebot), so declining that was correct before and is simply no longer wanted.

It corrected an asymmetry that favoured one vendor. ClaudeBot — Anthropic's training crawler, per Anthropic's own documentation — sat in the allow group while its exact counterpart GPTBot was declined. Nothing recorded why. Opening both resolves it in the direction the licence already points.

The Article 4 reservation

The old file declared Content-Signal: ai-train=no as an express reservation of rights under Article 4 of EU Directive 2019/790. It is removed rather than left standing three lines from a licence that grants exactly what it reserves — a reservation contradicting your own licence is worse than none.

Structural simplification

With the policy uniform, the named allow-groups became redundant: they resolve identically to User-agent: *. The file collapses to one declined group plus the wildcard, which removes the first-match ordering traps its own header spent twenty lines warning about.

Verified with Python's urllib.robotparser — a strict first-match parser — across eleven agent/path pairs:

GPTBot /box-basic              -> allowed      Bytespider /box-basic   -> blocked
ClaudeBot /box-basic           -> allowed      Googlebot /debug        -> blocked
Google-Extended /box-basic     -> allowed      GPTBot /interactive     -> blocked
Applebot-Extended /box-basic   -> allowed
CCBot, Amazonbot, MistralAI-Training, Googlebot -> allowed

⚠️ Does not take effect without a dashboard change

Cloudflare AI Crawl Control still 403s GPTBot, CCBot, Amazonbot, meta-externalagent and Google-CloudVertexBot at the edge, verified on the live zone 2026-08-18. A permission granted here that the edge blocks is a published lie. Unblock everything except Bytespider in the zone dashboard; docs/reference/seo.md records the measured state and the verification commands.

Also recorded there: Google-CloudVertexBot is blocked at the edge and appears in no repo-side policy — a decision that exists only as a dashboard toggle. And Cloudflare is no longer prepending a managed robots.txt block, unlike the 2026-07-25 measurement; the live file is byte-identical to this one.

🤖 Generated with Claude Code

Replaces the retrieval-yes / training-no split from #9633 with a single
open policy: retrieval, citation, indexing and training are permitted for
everyone.

The split was never coherent here. The catalogue is MIT-licensed and
published to be reused — every page ships its runnable source, /llms.txt
describes the site for agents, an MCP server exists so assistants can
query it directly. Declining a training crawler protected nothing the
licence had not already granted, and cost reach.

It also had a concrete consequence nobody intended. Google offers one
token, Google-Extended, governing Gemini grounding AND training together;
no finer control exists. Declining it as though it were training-only
kept anyplot out of Gemini's answers entirely, which is the opposite of
what a catalogue built for AI consumption wants. Applebot-Extended really
is training-only, so declining it was correct before and is simply no
longer wanted.

The Content-Signal line moves to ai-train=yes and the Article 4
reservation is removed rather than left standing in contradiction to the
licence three lines above it. A reservation that contradicts your own
licence is worse than none.

Bytespider stays declined, on bandwidth rather than principle: documented
as ignoring robots.txt and crawling far harder than this catalogue can
justify serving. The group states intent; Cloudflare does the enforcing.

With the policy uniform, the named allow-groups became redundant — they
resolve identically to `User-agent: *` — so the file collapses to one
declined group and the wildcard, removing the ordering traps its own
header warned about. Verified with urllib.robotparser across eleven
agent/path pairs, including that /debug and /interactive stay declined.

This does NOT take effect until the Cloudflare dashboard is updated: the
edge still 403s GPTBot, CCBot, Amazonbot, meta-externalagent and
Google-CloudVertexBot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 18, 2026 19:01
@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates anyplot’s crawler/robots policy documentation and robots.txt to permit retrieval, citation, search indexing, and model training for all operators (with a bandwidth-based exception for Bytespider), aligning the repo-side policy with the intended licensing stance and simplifying the robots group structure.

Changes:

  • Simplifies app/public/robots.txt to a single declined group (Bytespider) plus the wildcard group with Content-Signal ... ai-train=yes.
  • Updates docs/reference/seo.md to record the new “everything open” decision and the Cloudflare edge/dashboard enforcement caveat.
  • Adds a detailed [Unreleased] changelog entry describing the policy change and the required Cloudflare dashboard alignment.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
docs/reference/seo.md Updates the documented AI crawler policy decision and Cloudflare enforcement notes.
CHANGELOG.md Adds an [Unreleased] entry describing the updated crawler policy and operational requirement.
app/public/robots.txt Implements the simplified “open by default” robots policy with a Bytespider exception and updated content signals.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/reference/seo.md
Comment thread CHANGELOG.md
… PR refs

Both from the Copilot review.

The "Robots.txt" section still described the four-group structure — named
allow group, declined training collectors, opt-out tokens, wildcard — and
quoted a verbatim block with `ai-train=no`, three screens above the
section declaring the policy open. A document contradicting itself is
worse than either version alone. It now describes the two groups that
exist and quotes the declined one. The backend subsection also still
showed the old blanket `Disallow: /`, from before /og/ was opened.

The changelog point turned out to be broader than the one bullet it was
raised against: this file's convention is to close an entry with its PR
reference, and twelve entries from this run of SEO work had none —
#10452, #10453, #10455, #10458, #10462, #10465, #10470 through #10474.
The already-merged ones are fixed here too, since they all live in the
same file and a half-referenced changelog is the harder thing to audit
at release time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@MarkusNeusinger

Copy link
Copy Markdown
Owner Author

Both applied, and the second was broader than the line it was raised against.

The self-contradiction was real and worse than flagged. The "Robots.txt" section still described four groups and quoted a verbatim block containing ai-train=no, three screens above the section declaring the policy open. It now describes the two groups that exist. While there I found the backend subsection still showing the old blanket Disallow: / from before /og/ was opened — same class, so fixed in the same pass.

The changelog convention was missing from twelve entries, not one. Checking the file properly showed this whole run of SEO work shipped without PR references: #10452, #10453, #10455, #10458, #10462, #10465, #10470#10474. The merged ones are fixed here too — they live in the same file, and a half-referenced changelog is the harder thing to audit at release time than a fully un-referenced one.

Copilot AI review requested due to automatic review settings August 18, 2026 19:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (5)

docs/reference/seo.md:336

  • This sentence says "Allow comes first" but doesn’t explain that it’s because /og/ is the specific exception and / is the broad rule. Making that explicit keeps it consistent with the ordering rationale described above.
`/og/` is the exception: every prerendered page references its preview image
there, so a blanket `Disallow` pointed crawlers at an image they were forbidden
to fetch. `Allow` comes first for the first-match parsers described above.

docs/reference/seo.md:322

  • The wording here implies a hard rule that Disallow: must always come before Allow:. A few lines later (API robots.txt) the file relies on the opposite ordering for a more-specific allow exception. Rephrase this bullet to describe the underlying rule (most-specific match first) to avoid contradictory guidance in the same document.

This issue also appears on line 334 of the same file.

- Inside each group, `Disallow:` comes **before** `Allow: /` — same reason: with
  the broad allow first, a first-match parser (Python's `urllib.robotparser`,
  for one) hands out `/debug` and `/interactive`.

app/public/robots.txt:13

  • The repository’s documentation consistently uses US English (“license”). This file introduces the British spelling “licence”, which is inconsistent with the surrounding docs and style guidance.
# So this file grants what the licence already grants. Retrieval, citation,
# search indexing and model training are all permitted, for every operator,
# without the retrieval-yes / training-no split this file used to draw. That
# split was never coherent here: the licence permits reuse including training,
# so declining a training crawler protected nothing while costing reach.

docs/reference/seo.md:352

  • This section uses “licence”, but the rest of the docs use “license”. For consistency (and Google style), switch to US spelling.
licence had not already given away, while costing reach.

CHANGELOG.md:147

  • This changelog entry uses the British spelling “licence”, but other docs in the repo use “license”. Align spelling for consistency.
  declining a training crawler protected nothing the licence had not already granted, while costing
  reach. It also had an unintended consequence — Google's `Google-Extended` token governs Gemini
  **grounding and training together**, with no finer control, so declining it kept anyplot out of
  Gemini's answers entirely. The `Content-Signal` line moves to `ai-train=yes` and the Article 4
  reservation is gone rather than left contradicting the licence beside it. `Bytespider` remains

@MarkusNeusinger
MarkusNeusinger merged commit d37dfa1 into main Aug 18, 2026
10 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the feat/open-robots-policy-to-all-llms branch August 18, 2026 19:16
MarkusNeusinger added a commit that referenced this pull request Aug 18, 2026
Same convention fix as #10474, applied to this entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MarkusNeusinger added a commit that referenced this pull request Aug 18, 2026
Three findings from a fresh-context audit of today's twelve PRs. I had
spotted none of them.

## 1. The documentation contradicted itself

`docs/reference/seo.md`'s bot-map section still said `gptbot`,
`meta-externalagent` and `amazonbot` were *"declined in robots.txt"*,
and `app/nginx.conf` repeated the claim in a comment. Both were written
about an hour before #10474 opened the policy, and neither was
reconciled — so the page asserted the old policy three screens from the
section declaring the new one.

The measured edge-state table was stale the other way round: the
dashboard unblock it prescribed had since been carried out, so the table
described a state that no longer existed. It now records what is
actually blocked (`Bytespider`, `TikTok Spider`, and three agents whose
rule-compliance is unverified rather than disproven) and says plainly
that `bot-serving-check` tests the origin and will never catch edge
drift.

## 2. A database outage would reopen #10453

With no catalogue to check against, the bot routes answered `200` with a
fabricated, self-canonicalising page for **any** string — the precise
defect #10453 removed, surviving in degraded mode.

Degraded pages now carry `noindex`. I first tried returning `503`, which
is arguably more correct, and backed it out: it broke eleven tests that
use the no-DB path as a rendering harness. `noindex` keeps the behaviour
those tests depend on and removes the indexing risk, which is the part
that matters. The path is unreachable in production — but "unreachable"
here means one misconfiguration away from indexable.

## 3. 404s were counted as successful page reads

`bot_fetch` ran as a router dependency. A dependency executes **before**
the handler and cannot see the response, so every miss was recorded as a
read.

It has moved to a middleware and gained a `status` property. Recording
the miss is right — an assistant asking for a URL that no longer exists
is how a library migration announces itself — but recording it as a page
view is a lie. Filter on `status` before reading anything else;
documented in `docs/reference/plausible.md`.

## Also

A docstring pointed at `app/src/router.tsx`, which does not exist.
Routing lives in `app/src/routes/index.tsx`.

## Verification

- `pytest tests/unit` — 1640 passed, including: degraded hub and impl
pages assert `noindex`, a companion test asserts normal pages do
**not**, and a `bot_fetch` test pins `status: "404"` on a miss
- `ruff check` + `ruff format --check` — clean
- `grep` confirms no remaining reference to the superseded policy in
`seo.md`, `nginx.conf` or `robots.txt`

## Not in this PR

The audit's other findings are handled elsewhere: the ten-day-red
monitor in #10478, and the analytics that recorded nothing in #10477.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants