Tighten robots.txt to cut duplicate bot crawl - #3654
Tighten robots.txt to cut duplicate bot crawl#3654ChristopherChudzicki wants to merge 3 commits into
Conversation
OpenAPI ChangesNo changes detected Unexpected changes? Ensure your branch is up-to-date with |
There was a problem hiding this comment.
Pull request overview
This PR updates the Next.js robots.ts metadata route to reduce duplicate, origin-reaching crawler traffic by restricting crawlable URL variants (especially ?resource= drawer overlays) while preserving indexing via canonical /search?resource= URLs and the sitemap.
Changes:
- Replace the single default
robots.txtrule set with multiple user-agent groups (default crawl rules, link-preview bots exemption, AI-training crawlers blocked). - Add an explicit invariant note tying canonical drawer URL query-param ordering to the
robots.txtallow rule. - Add a unit test that pins both
MITOL_NOINDEXbranches and the exact generated rule groups.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| frontends/main/src/common/urls.ts | Documents the invariant that resource must remain the first query param in canonical drawer URLs. |
| frontends/main/src/app/robots.ts | Rewrites robots rules into multiple groups to block duplicate drawer overlays, internal search result pages, and _rsc payloads while allowing canonical drawer URLs. |
| frontends/main/src/app/robots.test.ts | Adds unit tests to pin the emitted robots metadata for indexing and no-indexing modes. |
Every ?resource= drawer URL canonicalizes to /search?resource=<id>&resource_title=<slug>, and the resources sitemap enumerates every resource at exactly that URL — so crawling drawer overlays on /c/, /news, home, and faceted search is pure duplicate load (Applebot alone fans out over ~2.5M such URLs/week). Allow only the canonical drawer form and the bare /search landing, and disallow resource-carrying URLs site-wide, internal search results, and Next.js _rsc prefetch payloads (never part of rendered or indexed content). Also: - add /enrollmentcode/, /organization/, and /website_content/drafts to the app-only disallows - exempt link-preview fetchers (Facebook/Twitter/Slack/etc.) via their own group so og: cards keep working on drawer URLs - block AI-training crawlers (GPTBot, CCBot, meta-externalagent, Bytespider, ClaudeBot, Amazonbot) and opt out of training use of Googlebot/Applebot crawl data (Google-Extended, Applebot-Extended) The Allow rules win by longest-match precedence (RFC 9309) and are emitted first for naive first-match parsers. They are literal prefix matches, so `resource` must remain the first query param in canonical drawer URLs — documented on resourceDrawerSearch, whose output is already pinned by urls.test.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Allow rule for canonical drawer URLs is a literal prefix match, so it silently stops covering the sitemap/canonical URLs if resourceDrawerSearch ever changes its path or param order. Evaluate the emitted rules against the real builder output with a minimal RFC 9309 longest-match evaluator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
add845b to
675b735
Compare
Drop the $ end-anchor: RFC 9309 does standardize $, but disallowing /search? instead of /search keeps the bare landing page crawlable even under legacy parsers that treat $ literally, and needs no anchor at all. Restore MITOL_NOINDEX by deletion when it was originally unset, since assigning undefined to process.env stores the string "undefined". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| // (a non-compliant crawler can ignore it), so if this doesn't reduce its | ||
| // request volume, blocking it at the gateway/WAF layer is the follow-up. | ||
| { | ||
| userAgent: "meta-externalads/1.1", |
There was a problem hiding this comment.
Changed below to meta-externalads per https://www.rfc-editor.org/rfc/rfc9309.html#section-2.2.1
Crawlers set their own name, which is called a product token, to find relevant groups. The product token MUST contain only uppercase and lowercase letters ("a-z" and "A-Z"), underscores ("_"), and hyphens ("-").
What are the relevant tickets?
N/A
Description (What does it do?)
Rewrites the robots.txt rules (
frontends/main/src/app/robots.ts) to stop crawlers from fetching URL variants that duplicate content they can already reach through canonical URLs, and blocks AI-training crawlers.Every
?resource=drawer URL canonicalizes to/search?resource=<id>&resource_title=<slug>, and the resources sitemap enumerates every resource at exactly that URL — so crawler fetches of drawer overlays on/c/,/news, home, and faceted search are pure duplicate load. A 7-day Fastly log analysis found this crawl is the dominant source of origin-reaching traffic: bot resource-URL crawl is roughly half of all origin-reaching requests (Applebot alone fans out over ~2.5M/c/+/newsdrawer URLs/week), and because each per-resource URL is fetched about once, the CDN can't absorb it. These are full page renders, so this is most of our real origin compute.The new rules: allow only the canonical drawer form (
/search?resource=) and the bare/searchlanding page; disallow resource-carrying URLs site-wide, internal search result pages (/search?q=/facets, per Google's own guidance), and_rscrouter-prefetch payloads (never part of rendered or indexed content); extend the app-only disallows (/enrollmentcode/,/organization/,/website_content/drafts); exempt link-preview fetchers (Facebook/Twitter/Slack/etc.) via their own group so og: cards keep working on shared drawer URLs; and block AI-training crawlers (GPTBot, CCBot, meta-externalagent, Bytespider, ClaudeBot, Amazonbot) plus the Google-Extended/Applebot-Extended training opt-out tokens, which cost no search visibility.Resource indexing is preserved: search engines keep the sitemap-enumerated canonical drawer URLs, and the drawer variants they lose all declare canonicals pointing at the form that stays crawlable.
How can this be tested?
Unit test pins both
MITOL_NOINDEXbranches and the exact rule groups:yarn test frontends/main/src/app/robots.test.ts.For manual validation, run the app with
MITOL_NOINDEX=false, fetch/robots.txt, and check sample URLs against the rules —/search?resource=123and/searchallowed;/search?q=python,/c/topic/physics/?resource=5, and/?resource=5disallowed. Google Search Console's robots.txt tester (or any RFC 9309 checker) confirms the longest-match precedence:Allow: /search?resource=(17 chars) beatsDisallow: /*?resource=(12) andDisallow: /search(7).Additional Context
The
Allow: /search?resource=rule is a literal prefix match, so it depends onresourcestaying the first query param in canonical drawer URLs — documented onresourceDrawerSearchincommon/urls.ts, whose output was already pinned byurls.test.ts. Rule order within the group is Allow-first as a courtesy to naive first-match parsers; compliant crawlers use longest-match, where order is irrelevant.This only affects robots-compliant crawlers. The disguised scraper traffic we've seen (rotating fake-browser UAs from a Tencent-cloud netblock) never fetches robots.txt and needs a Fastly rate-limit/ASN block, tracked separately.
meta-externaladsintentionally stays under the default group: on our logs it only fetches the UAI+B2C pages we advertise on Meta, so it keeps access to everything except drawer-overlay and_rscURLs.