# Web Crawler Orchestrator Orchestrates web crawling with intelligent backend selection. Primary backend is the OurSEO async crawler (rate-limited, resumable, metadata-rich). Falls back to Firecrawl MCP for JS-rendered sites or WebFetch for simple pages. ## Trigger Keywords "crawl URLs", "fetch documents", "scrape pages", "download references" ## Intelligent Crawler Selection ```bash # Get crawler recommendation for a URL uv run python scripts/crawl_mgr.py select-crawler --url "https://docs.anthropic.com" ``` | Crawler | Best For | Auto-Selected When | |---------|----------|-------------------| | **SEO aiohttp** (default) | Documentation sites, sitemaps | docs/developer/api domains, has sitemap | | **Firecrawl MCP** | Dynamic sites, SPAs | React/Vue/Angular, JS-rendered | | **WebFetch** | Quick single pages | Fallback, simple pages | | **Node.js** | Small docs sites | ≤50 pages, static content | | **Scrapy** | Enterprise crawls | >200 pages, multi-domain | ## Workflow ### Step 1: Analyze Target Site Run `select-crawler` to determine site characteristics and get a recommendation. ### Step 2a: Execute Crawl — SEO aiohttp (Recommended) The SEO crawler adapter provides rate limiting, progress tracking, resume support, and full metadata extraction. ```bash # Crawl specific URLs uv run python scripts/seo_crawler_adapter.py crawl \ --urls "https://docs.example.com/page1,https://docs.example.com/page2" \ --output ~/Documents/reference-library/topic-slug/raw/ # Crawl from XML sitemap (auto-discovers URLs) uv run python scripts/seo_crawler_adapter.py crawl-sitemap \ --sitemap https://docs.example.com/sitemap.xml \ --output ~/Documents/reference-library/topic-slug/raw/ \ --max-pages 50 # Discover URLs from root page (follows internal links) uv run python scripts/seo_crawler_adapter.py discover \ --url https://docs.example.com \ --output ~/Documents/reference-library/topic-slug/raw/ \ --max-depth 2 --max-pages 50 --same-domain # Crawl from reference-curator manifest uv run python scripts/seo_crawler_adapter.py crawl-manifest \ --manifest ./manifest.json \ --output ~/Documents/reference-library/topic-slug/raw/ # Check crawl progress uv run python scripts/seo_crawler_adapter.py status ``` **SEO crawler capabilities:** - Token-bucket rate limiting (via `BaseAsyncClient`) - Semaphore-controlled concurrency (default: 5 concurrent) - Tenacity retry with exponential backoff - Full `PageMetadata` extraction: title, headings, links, schema, OG - Progress tracking with ETA and resume files - Sitemap XML parsing (including sitemap indexes) - Internal link discovery (breadth-first, depth-limited) - Direct `.raw.md` output with enriched YAML frontmatter ### Step 2b: Execute Crawl — Firecrawl MCP (for JS-rendered sites) Use Firecrawl MCP tools directly when JS rendering is required: ``` firecrawl_map(url, limit=100) # Discover URLs firecrawl_scrape(url, formats=["markdown"], only_main_content=true) firecrawl_crawl(url, max_depth=2, limit=50) ``` ### Step 2c: Execute Crawl — WebFetch (fallback) For quick single-page fetches when no crawl infrastructure is needed: ``` WebFetch(url, prompt="Return the COMPLETE page content as clean markdown.") ``` ### Step 3: Store Crawl Results ```bash # Store crawled files and create result manifest uv run python scripts/crawl_mgr.py store-result \ --raw-dir ~/Documents/reference-library/raw/ \ --crawler seo-aiohttp \ --source-id 1 \ --output crawl_result.json # List recent crawls uv run python scripts/crawl_mgr.py list-crawls --status completed ``` ### Step 4: Write Raw Files (when `--save-raw` or `--no-distill`) When the pipeline is invoked with `--save-raw` (or `--no-distill`, which implies it), write each crawled page's markdown verbatim to the raw output directory. **Note:** The SEO crawler adapter (`seo_crawler_adapter.py`) writes `.raw.md` files directly with enriched YAML frontmatter — this step is automatic when using seo-aiohttp. **File naming:** `{NN}-{page-slug}.raw.md` — zero-padded index matching crawl order. **Output path:** - `--save-raw`: `{output}/{topic-slug}/raw/` - `--no-distill`: `{output}/{topic-slug}/` (raw files are primary) **YAML frontmatter (SEO crawler enriched):** ```yaml --- source_url: {original_url} crawled_at: {ISO 8601 timestamp} crawler: seo-aiohttp content_length: {character count} raw: true status_code: {HTTP status} title: {page title} word_count: {word count} internal_links: {count} external_links: {count} h1: {h1 text} schema_types: [{types found}] response_time_ms: {ms} --- ``` **After all pages are written**, a combined raw bundle (`{topic-slug}-raw-complete.md`) is auto-generated by concatenating all `.raw.md` files with `---` separators. ## Rate Limiting All crawlers respect these limits: - 20 requests/minute (SEO crawler: configurable via `BaseAsyncClient`) - 3-5 concurrent requests (SEO crawler: semaphore-controlled) - Exponential backoff on 429/5xx (SEO crawler: tenacity) - Use `--delay` flag for polite crawling (recommended: 2-3s for Google properties) ## Error Handling | Error | Action | |-------|--------| | Timeout | Retry with exponential backoff (max 3 attempts) | | Rate limit (429) | Token-bucket + exponential backoff | | Not found (404) | Log and skip | ## ⚠️ GOTCHA: IP Block / CAPTCHA Detection & Wayback Fallback **Problem:** Large-volume crawls on sites like `support.google.com` trigger IP-level bot detection. Google redirects to `google.com/sorry/` with a CAPTCHA. Once triggered: - Direct `requests`/`curl` returns 429 indefinitely (not a temporary rate limit) - Playwright headless/headful also gets blocked (Google detects automation) - The block persists even after the user solves the CAPTCHA in their browser (cookie-based, not IP-based lift) - `WebFetch` and `read_webpage` MCP tools also fail (same IP or server-side block) **Detection:** Check for these signals in crawl responses: 1. HTTP 302 redirect to `google.com/sorry/index?continue=...` 2. Response body contains `solveSimpleChallenge` or `unusual traffic` 3. Persistent 429 after retries with exponential backoff (>3 consecutive 429s) **Solution: Wayback Machine Detour** When IP block / CAPTCHA is detected, automatically fall back to Wayback Machine: ```bash # Instead of: https://support.google.com/analytics/answer/9304153 # Fetch from: https://web.archive.org/web/2024/https://support.google.com/analytics/answer/9304153 ``` The SEO crawler adapter supports this with the `--wayback-fallback` flag: ```bash uv run python scripts/seo_crawler_adapter.py crawl \ --urls "@urls.txt" \ --output ~/Documents/reference-library/topic/raw/ \ --delay 1.5 \ --wayback-fallback ``` **How it works:** 1. First attempts direct fetch for each URL 2. On 429 or CAPTCHA redirect, flags `captcha_detected` 3. After 3 consecutive CAPTCHA blocks, switches ALL remaining URLs to Wayback prefix 4. Wayback URLs use `https://web.archive.org/web/2024/{original_url}` 5. Frontmatter records `source: wayback` for traceability **Limitations:** - Wayback snapshots may be weeks/months old (acceptable for documentation/help pages) - Not all pages are archived (rare for major sites like Google, MDN, etc.) - Wayback has its own rate limits (gentler — 1.5s delay is sufficient) - Links in content point to Wayback URLs — the extractor strips these automatically **When to use `--delay` vs `--wayback-fallback`:** - Start with `--delay 3.0` for large Google crawls (preventive) - If 429s still occur, add `--wayback-fallback` (reactive) - For non-Google sites, `--delay 2.0` is usually sufficient | Access denied (403) | Log, mark as `failed` | | JS rendering needed | Switch to Firecrawl MCP | | Connection error | Retry with backoff, then skip | ## Scripts | Command | Purpose | |---------|---------| | `seo_crawler_adapter.py crawl` | Crawl URLs with SEO metadata extraction | | `seo_crawler_adapter.py crawl-sitemap` | Discover and crawl from XML sitemap | | `seo_crawler_adapter.py discover` | BFS link discovery from root URL | | `seo_crawler_adapter.py crawl-manifest` | Crawl from reference-curator manifest | | `seo_crawler_adapter.py status` | Check crawl progress | | `crawl_mgr.py select-crawler` | Recommend optimal crawler for a URL | | `crawl_mgr.py store-result` | Store crawl results and create manifest | | `crawl_mgr.py list-crawls` | List recent crawl records | ## Integration | From | To | |------|-----| | reference-discovery | URL manifest input | | SEO aiohttp / Firecrawl MCP | Raw crawled files | | → | content-repository (crawl manifest + raw files) | | → | raw/ directory (when --save-raw or --no-distill) | | quality-reviewer (deep_research) | Additional crawl requests |