The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Doc Scraper listing page.
doc-scraper)A configurable, concurrent, and resumable web crawler written in Go. Specifically designed to scrape technical documentation websites, extract core content, convert it cleanly to Markdown format suitable for ingestion by Large Language Models (LLMs), and save the results locally.

This project provides a powerful command-line tool to crawl documentation sites based on settings defined in a config.yaml file. It navigates the site structure, extracts content from specified HTML sections using CSS selectors, and converts it into clean Markdown files.
The main objective of this tool is to automate the often tedious process of gathering and cleaning web-based documentation for use with Large Language Models. By converting structured web content into clean Markdown, it aims to provide a dataset that is:
| Feature | Description |
|---|---|
| Configurable Crawling | Uses YAML for global and site-specific settings |
| Scope Control | Limits crawling by domain, path prefix, and disallowed path patterns (regex) |
| Content Extraction | Extracts main content using CSS selectors |
| HTML-to-Markdown | Converts extracted HTML to clean GitHub-Flavored Markdown (tables, task lists, strikethrough) |
| Image Handling | Opt-in downloading and local rewriting of image links with domain and size filtering (disabled by default; doc-scraper is text-first) |
| Link Rewriting | Rewrites internal links to relative paths for local structure |
| JSONL Output | Optional one-record-per-page JSONL with a trailing crawl-summary record, for RAG ingestion |
| Concurrency | Configurable worker pools and semaphore-based request limits (global and per-host) |
| Rate Limiting | Configurable per-host delays with jitter |
| Robots.txt & Sitemaps | Respects robots.txt and processes discovered sitemaps |
| State Persistence | Uses BadgerDB for state; supports resuming crawls via crawl --resume |
| Graceful Shutdown | Handles SIGINT/SIGTERM with proper cleanup |
| HTTP Retries | Exponential backoff with jitter for transient errors |
| Observability | Structured logging (log/slog); optional pprof endpoint (build with -tags pprof) |
| Modular Code | Organized into packages for clarity and maintainability |
| CLI Utilities | Built-in config validate and config list commands for configuration management |
| MCP Server Mode | Expose as Model Context Protocol server for Claude Code/Cursor integration |
| Full-Text Search | Offline BM25 search over crawled docs (SQLite FTS5) via the search_docs MCP tool |
| Auto Content Detection | Automatic framework detection (Docusaurus, MkDocs, Sphinx, GitBook, ReadTheDocs) with readability fallback |
| Parallel Site Crawling | Crawl multiple sites concurrently with shared resource management |
| Watch Mode | Scheduled periodic re-crawling with state persistence |
Option 1: Direct Installation (Recommended)
Install the latest version directly from GitHub:
This installs the doc-scraper binary to your GOPATH/bin directory (usually ~/go/bin or %USERPROFILE%\go\bin). Make sure this directory is in your PATH.
Option 2: Clone and Build
Clone the repository:
Install Dependencies:
Build the Binary:
This creates an executable named doc-scraper in the project root.
Create a minimal config.yaml in the project root:
Run the crawl:
The Markdown, plus pages.jsonl, llms.txt, and llms-full.txt, lands under ./crawled_docs/rust_cli_book/ (output is organized by site key). A small book like this finishes in a few seconds; large sites can take minutes, so start with a low max_depth to gauge size before removing the bound.
config.yaml)A config.yaml file is required to run the crawler. Create this file in the project root or specify its path using the -config flag.
When configuring for LLM documentation processing, pay special attention to these settings:
sites.<your_site_key>.content_selector: Define precisely to capture only relevant textsites.<your_site_key>.allowed_domain / allowed_path_prefix: Define scope accuratelyskip_images: Images are not downloaded by default (text-first). Set to false globally or per-site to download and localize images for offline consumption| Option | Type | Description | Default |
|---|---|---|---|
default_user_agent | String | Default User-Agent header for requests | "" (Go default) |
default_delay_per_host | Duration | Time to wait between requests to the same host | 0s (no delay) |
num_workers | Integer | Number of concurrent crawl workers | 4 |
num_image_workers | Integer | Number of concurrent image download workers | same as num_workers |
max_requests | Integer | Maximum concurrent requests (global) | 10 |
max_requests_per_host | Integer | Maximum concurrent requests per host | 2 |
output_base_dir | String | Base directory for crawled content | "./crawled_docs" |
state_dir | String | Directory for BadgerDB state data | "./crawler_state" |
max_retries | Integer | Maximum retry attempts for HTTP requests. To disable retries, set this to 0 together with a non-zero initial_retry_delay; max_retries: 0 on its own is treated as unset and falls back to the default | 3 |
initial_retry_delay | Duration | Initial delay for retry backoff | 1s |
max_retry_delay | Duration | Maximum delay for retry backoff | 30s |
global_crawl_timeout | Duration | Overall timeout for the entire crawl | 0s (no timeout) |
per_page_timeout | Duration | Timeout for processing a single page | 0s (no timeout) |
skip_images | Boolean | Whether to skip downloading images. Image downloading is opt-in | true (skip) |
max_image_size_bytes | Integer | Maximum allowed image size (applies only when images are downloaded) | 0 (unlimited) |
max_page_size_bytes | Integer | Maximum HTML page body size | 52428800 (50 MiB) |
enable_jsonl_output | Boolean | Enable JSONL page output (one record per page plus a trailing crawl_meta record) for RAG pipelines | false |
jsonl_output_filename | String | Filename for JSONL output | "pages.jsonl" |
enable_incremental | Boolean | Enable incremental crawling globally | false |
crawl_history_retention | Integer | Number of past crawls per site kept in the SQLite history index (powers get_freshness/diff_crawl) | 10 |
http_client_settings | Object | HTTP client configuration | (see below) |
sites | Map | Site-specific configurations | (required) |
HTTP Client Settings:
(Global; cannot be overridden per site. Pool, dialer, and TLS timings are baked into pkg/fetch with sane defaults and are not exposed as config knobs.)
timeout: Overall request timeout (default 45s)max_idle_conns_per_host: Idle connections per host (default 2)allow_private_networks: Disables the SSRF guard that blocks dials to loopback / private / link-local / CGNAT / multicast addresses. Default false. Set to true only if you intentionally crawl internal documentation servers reachable via private IPs.Site-Specific Configuration Options:
start_urls: Array of starting URLs for crawling (Required)allowed_domain: Restrict crawling to this domain (Required)allowed_path_prefix: Restrict crawling to URLs under this path prefix (Optional; defaults to /, the whole domain). Setting it is strongly recommended to bound scopecontent_selector: CSS selector for main content extraction, or "auto" for automatic detection (Required)max_depth: Exclusive upper bound on crawl depth from start URLs. Start pages are depth 0, so 1 crawls only the start pages, 2 adds their directly-linked pages, and so on. 0 = unlimited. URLs discovered from a sitemap.xml are seeded at depth 1 (one hop from the site root), so they are still bounded by max_depth: max_depth: 1 stays start-only and skips sitemap expansiondelay_per_host: Override global delay setting for this sitedisallowed_path_patterns: Array of regex patterns for URLs to skiplink_extraction_selectors: Array of CSS selectors for additional link extraction areasrespect_nofollow: Boolean. Whether to respect rel="nofollow" linksuser_agent: String. Override global user agent for this siteskip_images: Override the global image setting for this site. Images are skipped unless this (or the global skip_images) is set to falsemax_image_size_bytes: Integer. Override global max image size for this siteallowed_image_domains: Array of domains from which to download imagesdisallowed_image_domains: Array of domains to block image downloads fromenable_jsonl_output: true or false. Override global JSONL output enablement for this sitejsonl_output_filename: String. Override global JSONL output filename for this siteExecute the compiled binary from the project root directory:
| Command | Description |
|---|---|
crawl | Start a crawl (add --resume to continue an interrupted one) |
add | Probe a docs site and draft a config entry for it: detects the framework, proposes crawl scope from the sitemap, previews one extracted page, and writes only after confirmation |
config validate | Validate configuration file without crawling |
config list | List available site keys from config |
mcp-server | Start MCP server for AI tool integration |
search | Ranked full-text search over the crawled corpus (BM25, stemming, section anchors) |
watch | Watch sites and re-crawl on schedule |
version | Show version information |
run | Read a JSON task spec from stdin and dispatch a crawl or watch (for orchestration/automation) |
crawl:
| Flag | Description | Default |
|---|---|---|
-config <path> | Path to config file | config.yaml |
-site <key> | Site key from config (single site) | - |
-sites <keys> | Comma-separated site keys for parallel crawling | - |
--all-sites | Crawl all configured sites in parallel | false |
--resume | Resume an interrupted crawl from existing state | false |
-loglevel <level> | Log level (debug, info, warn, error) | info |
-json | Emit logs as JSON (one record per line) instead of text | false |
-pprof <addr> | pprof server address. Only effective in builds with -tags pprof; default builds log a warning and ignore the flag | "" (disabled) |
-incremental | Enable incremental crawling (skip unchanged pages) | false |
-full | Force full crawl (ignore incremental settings) | false |
Note: One of -site, -sites, or --all-sites is required.
add:
Probes the site with a handful of polite requests (the page, robots.txt, llms.txt, the sitemap), then shows what it found before anything is written: the detected framework and content selector (validated against the fetched page), a crawl scope clustered from the sitemap with page counts as evidence, sibling version/locale trees proposed as exclusions, and a markdown preview of the extracted page with code-block fidelity numbers. The entry is appended to your config only after you confirm; the rest of the file is preserved byte-for-byte, comments included.
| Flag | Description | Default |
|---|---|---|
-config <path> | Path to config file (created if missing) | config.yaml |
-site <key> | Site key to use instead of the derived one | - |
-selector <css> | Content CSS selector, skipping auto-detection | - |
-depth <n> | Override the proposed max_depth | - |
-yes | Write without prompting | false |
-dry-run | Draft only, never write (exit code 2) | false |
-json | Emit the draft as JSON on stdout (human text goes to stderr) | false |
Exit codes: 0 written, 1 error, 2 drafted but not written. For agents and scripts: add -dry-run -json <url> inspects, then add -yes <url> commits; with no terminal attached the command fails fast instead of waiting on stdin. Sites whose robots.txt disallows crawling the given path are refused, and robots rules that restrict AI crawlers are surfaced as a warning.
config validate:
| Flag | Description | Default |
|---|---|---|
-config <path> | Path to config file | config.yaml |
-site <key> | Site key to validate (optional, validates all if empty) | - |
-json | Emit a single JSON object instead of human-readable text | false |
config list:
| Flag | Description | Default |
|---|---|---|
-config <path> | Path to config file | config.yaml |
-json | Emit a single JSON object instead of human-readable text | false |
mcp-server: (stdio transport only; the SSE transport was removed in v2.x)
| Flag | Description | Default |
|---|---|---|
-config <path> | Path to config file | config.yaml |
-loglevel <level> | Log level (debug, info, warn, error) | info |
watch:
| Flag | Description | Default |
|---|---|---|
-config <path> | Path to config file | config.yaml |
-site <key> | Site key to watch (single site) | - |
-sites <keys> | Comma-separated site keys to watch | - |
--all-sites | Watch all configured sites | false |
-interval <duration> | Crawl interval (e.g., 1h, 24h, 7d) | 24h |
-loglevel <level> | Log level (debug, info, warn, error) | info |
-json | Emit logs as JSON (one record per line) instead of text | false |
Note: One of -site, -sites, or --all-sites is required.
Basic Crawl:
Resume a Large Crawl:
Validate Configuration:
List Available Sites:
High Performance Crawl with Profiling:
Debug Mode for Troubleshooting:
Parallel Crawl of Multiple Sites:
Crawl All Configured Sites:
Start MCP Server for Claude Desktop:
crawl -incremental (which implies --resume, and is also what watch mode uses) re-fetches every previously-crawled page and re-checks it for changes:
Because there is no conditional-request support yet, incremental mode still performs the HTTP fetch for each known page; the savings come from skipping the downstream processing of unchanged pages.
Crawled content is saved under the output_base_dir defined in the config, organized by site key and preserving the site structure. Keying by site key (rather than domain) keeps two site configs that target the same domain in separate trees:
When JSONL output is enabled, the crawler also emits llms.txt and llms-full.txt following the llmstxt.org convention. llms.txt is a markdown manifest (H1 + summary blockquote + ## Pages list of every crawled page with title and URL). llms-full.txt concatenates the full markdown content of every page, with section separators. Both files are regenerated on every crawl from the JSONL source of truth, so resumed crawls produce a complete updated manifest.
Each generated Markdown file begins with a YAML frontmatter block carrying page metadata, followed by the converted content:
---) with title, url (source URL), crawled_at (RFC3339 timestamp), content_hash (SHA-256 of the content, matching the JSONL record), and depthExample:
When enabled, the crawler writes one JSON object per line to a JSONL file. This format is designed for ingestion into RAG pipelines and downstream indexers.
Enable it:
The file mixes two record kinds, distinguished by the record_type field:
page records, one per crawled page.crawl_meta record as the final line, holding the crawl-level summary. Resuming rewrites the file to drop any leftover crawl_meta record before appending a fresh one at close, so a closed file always contains exactly one crawl_meta record.page record fields (from PageJSONL):
| Field | Description |
|---|---|
record_type | Always "page" |
url | Final absolute URL of the page |
title | Page title |
content | Full markdown content |
headings | Array of headings extracted from the page |
links | Array of links found in the content |
images | Array of image URLs found in the content |
content_hash | SHA-256 hash of the content (used for incremental crawling) |
crawled_at | Timestamp of when the page was crawled |
depth | Crawl depth from the start URL |
crawl_meta record fields (from CrawlMetaJSONL):
| Field | Description |
|---|---|
record_type | Always "crawl_meta" |
site_key | Site key from the config |
allowed_domain | The crawled domain |
crawl_started_at | Crawl start timestamp |
crawl_ended_at | Crawl end timestamp |
total_pages | Number of pages recorded in this crawl |
The output file is written to each site's output directory. Both the enable flag and filename can be overridden per site.
When you set content_selector: "auto" for a site, the crawler automatically detects the documentation framework and applies the appropriate content selector.
Detection recognizes 30+ documentation generators and hosted platforms, checked in three tiers of decreasing trust: the <meta name="generator"> tag, structural DOM signatures (attributes, ids, classes), and asset path patterns. Covered families include Docusaurus, VitePress, VuePress, Starlight/Astro, Nextra, Fumadocs, Mintlify, GitBook, MkDocs (Material, ReadTheDocs theme, and plain), Sphinx (furo, pydata, book, RTD, and classic themes), Antora, Docsy, hugo-book, Geekdoc, just-the-docs, mdBook, rustdoc, pkg.go.dev, Javadoc, Doxygen, TypeDoc, Writerside, ReadMe.com, Intercom, and Docus.
Every detected selector is validated against the live page before it is trusted: if it matches nothing or captures too little text, the crawler falls back instead of extracting empty content. Client-rendered shells (Docsify, Swagger UI, Redoc, Scalar, Document360, and generic empty-body SPAs) are recognized and reported as needing JavaScript rendering rather than silently producing an empty crawl.
If no known framework is detected (or the detected selectors do not match the page), the crawler uses Mozilla's Readability algorithm to extract the main content. This works well on classic server-rendered docs, but can drop code blocks on some modern sites, so doc-scraper add's preview reports code-block fidelity before you commit a config.
Crawl multiple documentation sites concurrently with shared resource management. The orchestrator coordinates multiple crawlers while respecting global rate limits and semaphores.
When running parallel crawls, the following resources are shared across all site crawlers:
Each site still maintains its own:
After all sites complete, the orchestrator outputs a summary:
Unknown or misspelled site keys are rejected before the crawl starts, so they never appear as a FAILED row in this summary. For example, crawl -sites pytorch_docs,typo_key exits immediately (non-zero) with:
The FAILED rows in the summary are for sites that exist in the config but errored during the crawl itself.
Watch mode enables scheduled periodic re-crawling of documentation sites. The scheduler tracks the last run time for each site and automatically triggers crawls when the configured interval has elapsed.
The interval supports standard Go duration format plus day units:
30m - 30 minutes1h - 1 hour24h - 24 hours7d - 7 days1d12h - 1 day and 12 hoursWatch mode persists state to <state_dir>/watch_state.json, tracking:
This allows the scheduler to resume correctly after restarts, only running sites when their interval has elapsed.
Watch mode handles SIGINT/SIGTERM gracefully: it stops the scheduler and cancels any in-progress crawl, letting the crawler flush its BadgerDB state and partial output first, so the interrupted crawl resumes cleanly on the next run.
The run command reads a single JSON object from stdin and dispatches the equivalent crawl or watch. It is meant for orchestration agents that would rather build a JSON payload than assemble shell flags. Unknown fields are rejected so typos surface immediately; logs go to stderr and the exit code matches the equivalent flag-driven subcommand.
Examples:
The crawler can run as a Model Context Protocol (MCP) server, enabling integration with AI assistants like Claude Code and Cursor.
| Tool | Description |
|---|---|
describe_server | Orientation manifest: server identity + sites + recent jobs in one call (call this first) |
list_sites | List all configured sites from config file |
get_page | Fetch a single URL live over the network and return content as markdown |
crawl_site | Start a background crawl for a site (returns job ID) |
get_job_status | Check the status of a background crawl job |
cancel_crawl | Cancel a running or pending crawl job by job ID |
list_pages | Enumerate crawled pages for a site (paginated, metadata only) |
read_page | Return a crawled page's markdown from the stored output, without network access |
search_docs | Full-text search across crawled docs (BM25, stemming, snippets), without network access |
get_freshness | Report how stale a site's latest crawl is, from the crawl-history index |
diff_crawl | Report pages added, removed, or changed since a given timestamp |
The MCP server uses the stdio transport, compatible with Claude Desktop, Claude Code, and Cursor.
Add to your Claude Code configuration (claude_code_config.json):
List available sites:
Fetch a single page:
Start a background crawl:
Check crawl progress:
Enumerate crawled pages:
Cancel a running crawl:
Contributions are welcome! Please feel free to open an issue to discuss bugs, suggest features, or propose changes.
Pull Request Process:
git checkout -b feature/amazing-feature)git commit -m 'Add some amazing feature')git push origin feature/amazing-feature)Please ensure code adheres to Go best practices and includes appropriate documentation.
doc-scraper collects nothing: no telemetry, no analytics, no accounts. All output and state stays on your machine, and the only network requests it makes are the crawls and fetches you explicitly ask for. Full policy: PRIVACY.md.
This project is licensed under the Apache-2.0 License.