Skip to content

feat(seo): serve prerendered pages to AI crawlers, state the crawler policy in robots.txt#9898

Merged
MarkusNeusinger merged 7 commits into
mainfrom
feat/ai-crawler-policy
Jul 25, 2026
Merged

feat(seo): serve prerendered pages to AI crawlers, state the crawler policy in robots.txt#9898
MarkusNeusinger merged 7 commits into
mainfrom
feat/ai-crawler-policy

Conversation

@MarkusNeusinger

Copy link
Copy Markdown
Owner

Summary

  • nginx now maps AI agents onto the seo-proxy path (app/nginx.conf): ClaudeBot, Claude-User, Claude-SearchBot, GPTBot, OAI-SearchBot, ChatGPT-User, PerplexityBot, Perplexity-User. None of them execute JavaScript, so they were being served the empty SPA shell — the same failure mode the search engines had before they were added. The map decides what a crawler is served, never whether it may fetch.
  • app/public/robots.txt carries the whole policy itself — content signals (incl. ai-train=no, an express reservation of rights under Art. 4 of EU Directive 2019/790), the welcomed retrieval/citation agents, the declined training collectors. Previously the file was three lines plus a comment noting that Cloudflare contradicted it; now the policy survives with Cloudflare's managed block turned off, which is the configuration this repo wants.
  • docs/reference/seo.md documents the decision for Decision: robots.txt AI-crawler policy (Cloudflare-managed) #9633, the measured edge behaviour, and the exact Cloudflare steps + verification commands. Also corrects the bot inventory (27 → 37 patterns; google-inspectiontool/googleother were missing from the table) and adds llms.txt + the bot-serving check to the file map.
  • bot-serving-check.yml gained two checks (an AI UA on a spec page, llms.txt under an AI UA) and a matching timeout bump (10 → 14 min for now 7 checks). It targets the Cloud Run origin, so it verifies the nginx map regardless of the zone-level AI policy.

Measured before this change (2026-07-25, edge ZRH)

Request Result
ClaudeBot / Claude-User / Claude-SearchBot on anyplot.ai/ 403 Your request was blocked.
GPTBot / OAI-SearchBot / ChatGPT-User / PerplexityBot on anyplot.ai/ 403
Any of those on anyplot.ai/llms.txt 403
Googlebot 200
ClaudeBot on api.anyplot.ai/libraries 200 (subdomain not covered by the block)

So llms.txt and the MCP pitch were unreachable to exactly the agents they are written for, and user-directed fetches were blocked along with the scrapers.

This PR does not unblock anything — lifting the 403 is a Cloudflare dashboard action on zone anyplot.ai (AI Crawl Control), documented in docs/reference/seo.md. This PR makes the repo say what the policy is and makes the serving correct for the moment the edge lets the agents through.

Plan

N/A — follow-up to #9633 (maintainer decision requested there).

Test plan

  • app/nginx.conf parses cleanly: crossplane parse of the fragment wrapped in events{}/http{} with the include resolved → status: ok, errors: []
  • UA semantics verified against real crawler UA strings (nginx ~* = case-insensitive regex): all 8 new patterns match their agent, Googlebot/Twitterbot still match, and Chrome/Safari/Firefox UAs still take the human path
  • bot-serving-check.yml is valid YAML; the run block passes bash -n; 7 check calls match the updated timeout comment
  • yarn fm:check, yarn lint, yarn type-check, yarn test (609 tests) green
  • After the Cloudflare change: curl -A "…ClaudeBot…" https://anyplot.ai/scatter-basic | grep '<title>' returns the per-route title, and curl -A "…CCBot…" https://anyplot.ai/ still returns 403

…n robots.txt

Claude, ChatGPT and Perplexity fetch pages either on a user's explicit
request or to build a citation index, and none of them execute
JavaScript — so they were being handed the empty SPA shell, the same
failure mode the search engines had. Map them onto the seo-proxy path
and guard it in the daily bot-serving check (origin-level, so it holds
regardless of the Cloudflare edge policy).

robots.txt now carries the full policy itself — content signals incl.
the Art. 4 EU-DSM training reservation, the welcomed retrieval agents,
the declined training collectors — so it survives with Cloudflare's
managed block turned off. docs/reference/seo.md records the decision,
the measured edge behaviour (hard 403 for every AI UA, llms.txt
included) and the dashboard steps that lift it.

Refs #9633
Copilot AI review requested due to automatic review settings July 25, 2026 21:41

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 improves anyplot.ai’s SEO serving behavior for AI crawlers by routing additional AI-related user agents to the prerendered seo-proxy path, and by explicitly documenting/encoding the project’s AI crawler policy in robots.txt and the SEO reference docs. It also extends the daily synthetic “bot serving” workflow to cover an AI UA and llms.txt under an AI UA.

Changes:

  • Route additional AI crawler/user-directed UAs (Claude*, OpenAI*, Perplexity*) to prerendered pages via the nginx bot map.
  • Expand robots.txt into a full, explicit AI crawler policy (content signals + allowed vs declined groups) and document the rationale/Cloudflare steps in docs/reference/seo.md.
  • Extend the bot-serving-check workflow with AI-UA coverage and adjust timeout accordingly; add a changelog entry.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
docs/reference/seo.md Updates bot inventory/documentation and adds an explicit “AI crawler policy” section with verification steps.
CHANGELOG.md Adds an Unreleased entry describing the new AI crawler policy + serving behavior.
app/public/robots.txt Encodes the crawler policy (content signals + allow/deny groups for AI UAs).
app/nginx.conf Extends $is_bot mapping to include AI assistant/search UAs so they receive prerendered HTML.
.github/workflows/bot-serving-check.yml Adds AI-UA checks (spec page + llms.txt under AI UA) and increases the workflow timeout.

Comment thread app/public/robots.txt
@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

A crawler obeys the single robots.txt group that matches it most
specifically, so ClaudeBot & co. would never have seen the
ai-train=no reservation declared under User-agent: * (Copilot #9898).
Copilot AI review requested due to automatic review settings July 25, 2026 21:45

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 5 out of 5 changed files in this pull request and generated no new comments.

Spec-compliant crawlers pick the most specific matching group, but
simpler parsers take the first match — with User-agent: * on top those
would read Allow: / and never reach the declining groups (Copilot
#9898).
Copilot AI review requested due to automatic review settings July 25, 2026 21:47

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 5 out of 5 changed files in this pull request and generated no new comments.

With 'Allow: /' first, a first-match parser (Python's
urllib.robotparser, for one) hands out /debug and /interactive; the
longest-match parsers are order-independent, so leading with the
Disallow lines is correct for both.
Copilot AI review requested due to automatic review settings July 25, 2026 21:53

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 5 out of 5 changed files in this pull request and generated 1 comment.

Comment thread docs/reference/seo.md Outdated
The declining groups carried no Content-Signal, so the very agents the
ai-train=no reservation is aimed at would never have read it — a crawler
only reads the group that matches it. Also replaces the seo.md snippet,
which showed invented '…' lines and the old Allow-before-Disallow order,
with the first group verbatim (Copilot #9898).
Copilot AI review requested due to automatic review settings July 25, 2026 22:02

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 5 out of 5 changed files in this pull request and generated no new comments.

MarkusNeusinger added a commit that referenced this pull request Jul 25, 2026
…9899)

## Summary

- `pyproject.toml` pins `ruff>=0.15.21` with no upper bound. CI resolved
**0.16.0** today, and that version formats Python code blocks **inside
Markdown** — 20 tracked `.md` files immediately counted as unformatted
and `Run Linting` went red on every PR (first seen on #9898, which
touches none of those files).
- Fix: `*.md` joins ruff's exclude list. Ruff owns this repo's Python,
not its prose.
- Deliberately **not** fixed by reformatting the 20 files: nine are
`prompts/library/*.md`, whose snippets steer code generation, so a
quote-style rewrite there is a pipeline change wearing a formatting
costume. If the docs should be ruff-formatted, that is its own decision
with its own review.

## Plan

N/A — unblocks CI.

## Test plan

- [x] `uvx ruff@0.16.0 format --check .` → `148 files already formatted`
(was: `20 files would be reformatted`)
- [x] `uvx ruff@0.16.0 check .` → `All checks passed!`
- [x] Same two commands on `ruff@0.15.21` (the version CI resolved while
green) → identical results, so the change is not version-specific
- [ ] `Run Linting` green on this PR
Copilot AI review requested due to automatic review settings July 25, 2026 22:15

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 5 out of 5 changed files in this pull request and generated no new comments.

@MarkusNeusinger
MarkusNeusinger merged commit 52bafb4 into main Jul 25, 2026
10 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the feat/ai-crawler-policy branch July 25, 2026 22:21
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