This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Adapted from Andrej Karpathy's four CLAUDE.md principles — read these before touching code. They target reasoning failures (wrong assumptions, over-engineering, scope creep, weak success criteria), not formatting.
-
Think Before Coding — Don't assume. Don't hide confusion. Surface tradeoffs.
- State assumptions explicitly; if uncertain, ask rather than guess.
- Present competing interpretations instead of silently picking one.
- Call out inconsistencies, confusion, and tradeoffs as you find them.
-
Simplicity First — Minimum code that solves the problem. Nothing speculative.
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or configurability that wasn't requested.
- No error handling for scenarios that cannot occur.
-
Surgical Changes — Touch only what you must. Clean up only your own mess.
- Match the surrounding style and conventions.
- Don't refactor unbroken adjacent code that's orthogonal to the task.
- Only remove what your change rendered obsolete.
-
Goal-Driven Execution — Define success criteria. Loop until verified.
- Turn tasks into verifiable goals: "add validation" → "write tests, then make them pass"; "fix the bug" → "reproduce it in a test, then fix"; "refactor X" → "ensure tests pass before and after".
- Iterate against those criteria instead of asking for constant clarification.
git config core.hooksPath .githooks # enable pre-commit hook (fmt + clippy + tests)cargo fmt # auto-format code (always run before build)
cargo build # debug build
cargo build --release # release build (~11s)
cargo test # unit tests + offline integration tests (~300 tests)
cargo test --test integration_crawl -- --ignored --test-threads=1 # network integration tests (crawls crawler.siteone.io)
cargo test scoring::ci_gate::tests::all_checks_pass # run a single test by name
cargo clippy -- -D warnings # lint (CI enforces zero warnings)
cargo fmt -- --check # format check
# Browser rendering (`--browser`, chromiumoxide/CDP) is a DEFAULT feature — `cargo build`,
# `cargo test`, `cargo clippy` all include it. Build/test the lean variant (no chromiumoxide):
cargo build --release --no-default-features # lean build, ~6 MB smaller, no browser
cargo test --no-default-features # tests without the browser feature
cargo clippy --no-default-features -- -D warnings # lint the lean variant./target/release/siteone-crawler --url=https://example.com --single-page
./target/release/siteone-crawler --url=https://example.com --output=json --http-cache-dir= # no cache
./target/release/siteone-crawler --html-to-markdown=page.html # convert local HTML to markdown (stdout)
./target/release/siteone-crawler --html-to-markdown=page.html --html-to-markdown-output=page.md # convert to file-
CLI Parsing (
Initiator→CoreOptions::parse_argv()): Parses 120+ CLI options, merges config file if present, validates. Exits with code 101 on error, code 2 on--help/--version. Non-crawl utility modes (--serve-markdown,--serve-offline,--html-to-markdown) exit early inmain.rsbefore creating the Manager. -
Analyzer Registration (
Initiator::register_analyzers()): Creates all 17 analyzer instances (Accessibility, BestPractice, BrowserConsole, Caching, ContentType, DNS, ExternalLinks, Fastest, Headers, Page404, Redirects, Security, SeoAndOpenGraph, SkippedUrls, Slowest, SourceDomains, SslTls) and registers them withAnalysisManager. Some analyzers receive config from CLI options (e.g.fastest_top_limit,max_heading_level);BrowserConsoleis active only in--browsermode. -
Manager Setup (
Manager::run()): CreatesStatus(result storage),Output(text/json/multi),HttpClient(with optional proxy, auth, cache),ContentProcessorManager(HTML, CSS, JS, XML, Astro, Next.js, Svelte processors), and theCrawlerinstance. The crawl loop fetches through anArc<dyn Fetcher>(src/engine/fetcher.rs): by default theHttpClient; with thebrowserfeature +--browser, aBrowserRendererthat renders each HTML page in Chromium and returns the sameHttpResponse(plusbrowser_diagnostics). Everything downstream is unchanged. -
Robots.txt Fetch (
Crawler::fetch_robots_txt()): Before crawling starts, fetches and parses/robots.txtfrom the initial domain. Respects--ignore-robots-txtoption. -
Crawl Loop (
Crawler::run()): Breadth-first concurrent URL processing:- URL queue (
DashMap) seeded with initial URL - Tokio tasks limited by
Semaphore(=--workerscount) + rate limiting (--max-reqs-per-sec) - Per-URL flow: check robots.txt → HTTP request → on error, store with negative status code → on success, run content processors → extract links from HTML → enqueue discovered URLs
- Content processors (
HtmlProcessor,CssProcessor, etc.) transform response bodies during crawl — used by offline/markdown exporters for URL rewriting - Each visited URL's response is stored in
Statusfor post-crawl analysis - Per-URL data collected: status code, headers, body, response time, content type, size, redirects
- URL queue (
-
Post-Crawl Analysis (
Manager::run_post_crawl()): Sequential pipeline after crawling ends:- Transfer skipped URLs from crawler to
Status - Run all registered analyzers (
AnalysisManager::run_analyzers()): each analyzer gets read access toStatus(all crawled data) and write access toOutput(adds tables/findings) - Add content processor stats table
- Transfer skipped URLs from crawler to
-
Exporters (
Manager::run_exporters()): Generate output files based on CLI options:SitemapExporter: XML/TXT sitemap filesOfflineWebsiteExporter: Static website copy with rewritten relative URLsMarkdownExporter: HTML→Markdown conversion with relative .md linksFileExporter: Save text/JSON output to fileHtmlReport: Self-contained HTML report (also used by Mailer and Upload)MailerExporter: Email HTML report via SMTPUploadExporter: Upload report to remote serverAnimationExporter(featurebrowser): Assemble per-page screenshots into a GIF/MP4 animation (GIF via the embeddedimagecrate, MP4 via external ffmpeg)
-
Scoring (
scorer::calculate_scores()): Computes quality scores (0–10) across 5 weighted categories (Performance 20%, SEO 20%, Security 25%, Accessibility 20%, Best Practices 15%). Deductions come from summary findings (criticals, warnings) and stats (404s, 5xx, slow responses). -
CI/CD Gate (
ci_gate::evaluate()): When--ciis active, checks scores and stats against configurable thresholds (--ci-min-score,--ci-max-404, etc.). Returns exit code 10 on failure. -
Summary & Output (
Output::add_summary(),Output::end()): Prints summary table with OK/Warning/Critical counts, finalizes output. Exit code: 0 = success, 3 = no pages crawled, 10 = CI gate failed.
Each analyzer implements the Analyzer trait (analysis/analyzer.rs). Analyzers are post-crawl only — they don't run during crawling. The AnalysisManager calls each analyzer's analyze(&Status, &mut Output) method after all URLs have been visited. Analyzers read crawled data from Status (visited URLs, response headers, bodies, skipped URLs) and produce SuperTable instances that get added to Output. Analyzers also add Item entries to the Summary (OK, Warning, Critical, Info findings) which feed into scoring.
Content processors implement ContentProcessor (content_processor/content_processor.rs) and run during crawl on each URL's response body. They serve two purposes: (1) transform content for offline/markdown export (rewrite URLs to relative paths), and (2) extract metadata (links, assets). Processors are type-specific: HtmlProcessor handles HTML, CssProcessor handles CSS url() references, etc. The ContentProcessorManager dispatches to the right processor based on content type.
The crawler uses tokio for async I/O with a semaphore-based worker pool (options.workers). Shared state uses:
Arc<DashMap<...>>for lock-free concurrent maps (URL queue, visited URLs, skipped URLs)Arc<Mutex<...>>for sequential-access state (Status, Output, AnalysisManager)Arc<AtomicBool/AtomicUsize>for simple flags and counters
Analyzer(analysis/analyzer.rs): Post-crawl analysis (SEO, security, headers, etc.). Each analyzer gets&Statusand&mut Output.Exporter(export/exporter.rs): Output generators (HTML report, offline website, markdown, sitemap, mailer, upload).Output(output/output.rs): Formatting backend. Implementations:TextOutput,JsonOutput,MultiOutput.ContentProcessor(content_processor/content_processor.rs): Per-URL content transformation during crawl (HTML, JS, CSS, XML processors).
CLI options are defined in options/core_options.rs via get_options() which returns an Options struct with typed option groups. Parsing flow: parse_argv() → merge config file → parse flags → CoreOptions::from_options() → apply_option_value() for each option. New CLI options require: adding the field to CoreOptions, a case in apply_option_value(), and an entry in the appropriate option group.
| Code | Meaning |
|---|---|
| 0 | Success (with --ci: all thresholds passed) |
| 1 | Runtime error |
| 2 | Help/version displayed |
| 3 | No pages successfully crawled (DNS failure, timeout, etc.) |
| 10 | CI/CD quality gate failed |
| 101 | Configuration error |
HttpResponse.body is Option<Vec<u8>> (not String) to preserve binary data for images, fonts, etc. Use body_text() for string content. Failed HTTP requests return Ok(HttpResponse) with negative status codes (-1 connection error, -2 timeout, -4 send error), not Err.
- Unit tests: In-file
#[cfg(test)] mod testsblocks (standard Rust convention) - Integration tests:
tests/integration_crawl.rswith shared helpers intests/common/mod.rs - Network-dependent integration tests are
#[ignore]— run explicitly with--ignored
The crawler has a built-in HTTP server (--serve-offline=<dir>) that can serve any local directory as a static website. This enables efficient local testing of edge cases without deploying a real site:
- Create a sample website directory, e.g.
./tmp/sample-website-xyz/ - Add HTML files and assets simulating the desired scenario (spaces in filenames, special characters, redirect chains, broken links, specific heading structures, etc.)
- Start the built-in server:
./target/release/siteone-crawler --serve-offline=./tmp/sample-website-xyz/ --serve-port=8888 - In another terminal, crawl the local site:
./target/release/siteone-crawler --url=http://127.0.0.1:8888/ - Verify the crawler handles the scenario correctly (output, offline export, analysis results)
This approach is useful for reproducing bug reports, testing regex edge cases (e.g. URLs with spaces, HTML entities, unusual attribute quoting), validating offline/markdown export for specific HTML structures, and any scenario that would be hard to find on a live website.
src/engine/crawler.rs(~1700 lines): Core crawl loop, URL queue management, HTML/content parsingsrc/options/core_options.rs(~2500 lines): All 120+ CLI options, parsing, validationsrc/export/utils/offline_url_converter.rs(~1400 lines): URL-to-file-path conversion for offline exportsrc/export/html_report/report.rs: HTML report generation with embedded templatesrc/scoring/scorer.rs: Quality score calculation from summary findingssrc/scoring/ci_gate.rs: CI/CD threshold evaluationsrc/engine/fetcher.rs:Fetchertrait — the single seam the crawl loop fetches through;HttpClient(direct HTTP) andBrowserRendererboth implement itsrc/browser/(featurebrowser):BrowserRenderer(renderer.rs), Chromium detection/download/launch (launcher.rs), CDP diagnostics collection (diagnostics.rs), screenshots + pre-capture animation settling (screenshot.rs), cookie-banner dismissal/hiding (cookie_consent.rs).diagnostics.rsdata types are always compiled soHttpResponsecan carry an inertOption<BrowserDiagnostics>src/export/animation_exporter.rs(featurebrowser): builds GIF/MP4 animations from per-page screenshots (GIF via the embeddedimagecrate, MP4 via external ffmpeg; frames streamed to disk for O(1) memory)src/analysis/browser_console_analyzer.rs: reports browser console/JS/network/security diagnostics (the browser-mode analyzer; active only in--browsermode)
Project uses edition = "2024" (Rust 1.85+) with rust-version = "1.94". Edition 2024 features used throughout: unsafe extern blocks, if let chaining (if let ... && ...), unsafe { std::env::set_var() }.
Never commit automatically. Commits are only allowed on explicit user request. Before every commit, always run git status, review the changes, and stage only the relevant files — never use git add -A or git add . blindly.
Use Conventional Commits: feat:, fix:, refactor:, perf:, docs:, style:, ci:, chore:, test:. Examples:
feat: add built-in HTTP server for markdown/offline exportsfix: correct non-ASCII text corruption in heading ID generationperf: eliminate heap allocation in content_type_for_extensionchore: bump version to 2.0.3
- Update version in
Cargo.toml(version = "X.Y.Z") - Update version in
src/version.rs(pub const CODE: &str = "X.Y.Z.YYYYMMDD";) - Run
cargo checkso thatCargo.lockis updated with the new version - Commit all three files (
Cargo.toml,src/version.rs,Cargo.lock):git commit -m "chore: bump version to X.Y.Z" - Tag and push:
git tag vX.Y.Z && git push && git push --tags
- Tables, column order, and formatting must stay consistent across versions. The HTML parser uses the
scrapercrate. - HTTP cache lives in
tmp/http-client-cache/by default. Delete it for fresh crawls or use--http-cache-dir=to disable. rustlsrequires explicitringCryptoProvider installation inmain.rs.