Skip to main content

crawlkit_engine/
lib.rs

1//! # crawlkit-core
2//!
3//! Core library for crawlkit — a high-performance Rust web crawler for SEO analysis.
4//!
5//! ## Overview
6//!
7//! This crate provides the foundational types, HTTP fetching, HTML parsing,
8//! SEO analyzers, crawl queue, storage, and observability primitives used by
9//! the crawlkit CLI and API server.
10//!
11//! ## Features
12//!
13//! - **28 SEO analyzers** covering meta tags, content quality, security, accessibility
14//! - **Async HTTP/2** fetching with retry, redirect tracking, rate limiting
15//! - **HTML parsing** with link, heading, image, and structured data extraction
16//! - **SQLite storage** with WAL mode and batch operations
17//! - **Observability** with atomic metrics and OpenTelemetry support
18//! - **Plugin system** with WASM sandboxing for third-party extensions
19//! - **Encryption at rest** with AES-256-GCM
20//!
21//! ## Quick Start
22//!
23//! ```rust,no_run
24//! use crawlkit_engine::{CrawlConfig, HttpClient, HtmlParser};
25//! use crawlkit_engine::analyzers::AnalyzerRegistry;
26//!
27//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
28//! let config = CrawlConfig::default();
29//! let client = HttpClient::from_crawl_config(&config)?;
30//! let registry = AnalyzerRegistry::new(&config);
31//!
32//! let url = url::Url::parse("https://example.com")?;
33//! let result = client.fetch(&url).await?;
34//! let parsed = HtmlParser::parse(&result.body, &url)?;
35//! # Ok(())
36//! # }
37//! ```
38#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used, clippy::panic))]
39
40use chrono::{DateTime, Utc};
41use serde::{Deserialize, Serialize};
42use std::time::Duration;
43use thiserror::Error;
44use url::Url;
45
46/// Advanced crawl features such as JavaScript rendering and WASM analysis.
47///
48/// Provides [`AlertManager`](advanced_features::AlertManager) for monitoring
49/// crawl metrics and triggering notifications on threshold breaches.
50pub mod advanced_features;
51/// AI-powered page content analyzers for answer boxes, citations, and crawler accessibility.
52///
53/// These analyzers detect AI-specific SEO opportunities and issues, such as
54/// whether AI crawlers can access the site and whether content is structured
55/// for AI extraction.
56pub mod ai_analyzers;
57/// Registry of known AI bot user-agents and crawler identification.
58///
59/// Contains a static list of AI crawlers (GPTBot, ClaudeBot, etc.) with metadata
60/// for robots.txt analysis and AI accessibility scoring.
61pub mod ai_bots;
62/// SEO analysis engine with pluggable analyzers (title, meta, links, etc.).
63///
64/// Defines the [`Analyzer`](analyzers::Analyzer) trait and provides 28+ built-in analyzers
65/// covering HTTP status, redirects, canonical URLs, meta tags, headings, links,
66/// images, structured data, security, accessibility, and more.
67pub mod analyzers;
68/// Audit trail logging for crawl operations and configuration changes.
69///
70/// Provides a tamper-evident append-only log with SHA-256 chaining for
71/// compliance and security auditing.
72pub mod audit;
73/// Adapters for third-party backlink data sources (Ahrefs, GSC, Majestic).
74///
75/// Defines the [`BacklinkAdapter`](backlink_adapters::BacklinkAdapter) trait for
76/// integrating external backlink data into crawl analysis.
77pub mod backlink_adapters;
78/// Backlink analysis, scoring, and reporting.
79///
80/// Computes PageRank-like scores from internal link graphs and produces
81/// per-page backlink reports and site-wide summaries.
82pub mod backlinks;
83/// Backpressure controller to bound in-flight work and prevent memory blowouts.
84///
85/// Uses tokio semaphores and bounded channels to limit concurrent tasks,
86/// ensuring the crawler stays within resource budgets.
87pub mod backpressure;
88/// Circuit breaker for failing HTTP endpoints to avoid cascading failures.
89///
90/// Per-domain circuit breakers track consecutive failures and automatically
91/// stop requests to failing domains until they recover.
92pub mod circuit_breaker;
93/// Diff-based comparison of two crawl results.
94///
95/// Detects added/removed pages, status changes, title changes, content
96/// changes, and Core Web Vitals regressions between crawls.
97pub mod compare;
98/// Deterministic replay controller for reproducible crawl runs.
99///
100/// Seed-based PRNG ensures that given the same input and configuration,
101/// the crawler produces identical output for testing and auditing.
102pub mod determinism;
103/// DNS resolution cache and prefetching.
104///
105/// Concurrent DNS cache with configurable TTL and background prefetching
106/// to reduce DNS lookup latency during high-throughput crawling.
107pub mod dns;
108/// TLS and encryption configuration for HTTPS requests.
109///
110/// Provides AES-256-GCM encryption at rest for sensitive crawl data,
111/// with key management via files, environment variables, or system keyrings.
112pub mod encryption;
113/// Enterprise feature gating and licensing utilities.
114pub mod enterprise;
115/// Export of crawl data to JSON, CSV, HTML, and Markdown formats.
116///
117/// Configurable column selection and formatting for CSV export,
118/// with streaming support for large datasets.
119pub mod export;
120/// Feature flag system for toggling capabilities at runtime.
121///
122/// Flags are immutable per-crawl session once set. Supports TOML
123/// configuration and programmatic access via [`SharedFeatureFlags`](feature_flags::SharedFeatureFlags).
124pub mod feature_flags;
125/// HTTP client with retry, redirect following, and rate limiting.
126///
127/// Provides [`HttpClient`](http::HttpClient) with exponential backoff retry,
128/// manual redirect tracking, user-agent rotation, and streaming responses.
129pub mod http;
130/// Decision engine for determining whether a page requires JavaScript rendering.
131///
132/// Detects SPA frameworks (Next.js, Nuxt, SvelteKit, Angular) via HTML hints
133/// and URL patterns to decide when to invoke Playwright.
134pub mod js_render_decision;
135
136/// Metrics collection and observability hooks.
137///
138/// Provides atomic [`Metrics`](observability::Metrics) for tracking pages crawled,
139/// bytes fetched, timing, and circuit breaker events with zero-allocation hot paths.
140pub mod observability;
141/// Playwright-based headless browser integration for JS-rendered pages.
142///
143/// Renders JavaScript-heavy SPAs via Playwright CLI subprocess with
144/// browser context isolation, resource limits, and console/network capture.
145pub mod playwright;
146/// Plugin system for extending the crawler with custom analyzers.
147///
148/// WASM-based plugins are sandboxed via wasmtime with a well-defined
149/// ABI (`crawlkit_plugin_init`, `crawlkit_plugin_analyze`, `crawlkit_plugin_alloc/free`).
150pub mod plugin;
151
152pub use plugin::{PluginError, PluginManifest, PluginMetadata, PluginRegistry, WasmPlugin};
153/// Priority URL queue with depth and scope filtering.
154///
155/// Binary heap-based priority queue with deduplication, domain tracking,
156/// and configurable scope control (allowed/blocked domains and paths).
157pub mod queue;
158/// Per-domain rate limiting to respect politeness constraints.
159///
160/// Token-bucket rate limiter with per-domain and global buckets,
161/// supporting crawl-delay from robots.txt and concurrency limiting.
162pub mod ratelimit;
163/// Runtime resource monitoring and limit enforcement.
164///
165/// Tracks memory, CPU, disk, and page counts against configurable limits,
166/// providing early termination when budgets are exceeded.
167pub mod resource_monitor;
168/// robots.txt parsing, caching, and compliance checking.
169pub mod robots;
170/// Real User Metrics (CrUX, GA) integration for performance data.
171///
172/// Fetches Core Web Vitals from Chrome UX Report API and Google Analytics,
173/// merging lab and field data for comprehensive performance analysis.
174pub mod rum;
175/// Sitemap.xml parsing and URL discovery.
176pub mod sitemap;
177/// SQLite-backed persistent storage for crawl results and issues.
178///
179/// WAL-mode SQLite with LRU page cache, batch insert operations,
180/// and memory usage tracking. Supports pages, links, issues, images,
181/// structured data, and CrUX metrics.
182pub mod storage;
183/// WASM-based analyzers for advanced code and performance analysis.
184///
185/// Static pattern analysis, runtime performance analysis, and
186/// Playwright-powered rendering analysis for WebAssembly content.
187pub mod wasm_analyzers;
188
189pub use ai_analyzers::{
190    AiAnswerBoxAnalyzer, AiCitationEligibilityAnalyzer, AiContentStructureAnalyzer,
191    AiCrawlerAccessibilityAnalyzer,
192};
193pub use ai_bots::{AiBot, AiBotRegistry};
194pub use analyzers::{
195    AccessibilityAnalyzer, AnalysisContext, Analyzer, AnalyzerRegistry, CanonicalUrlValidator,
196    ContentQualityAnalyzer, EcommerceSignalsAnalyzer, EnhancedReadabilityAnalyzer, EntityAnalyzer,
197    Finding, HeadingHierarchyAnalyzer, HreflangValidator, HttpStatusAnalyzer, ImageAnalyzer,
198    ImageInfo, InternationalSeoAnalyzer, KeywordAnalyzer, LinkAnalyzer, LinkInfo, MetaTagAnalyzer,
199    MobileFriendlinessChecker, RedirectChainAnalyzer, RobotsRule, RobotsTxtAnalyzer,
200    SecurityHeaderAnalyzer, SitemapAnalyzer, SitemapEntry, SocialMediaAnalyzer, SslCertificateInfo,
201    SslCertificateValidator, StructuredDataValidator, WordCountAnalyzer,
202};
203pub use audit::{AuditEvent, AuditEventType, AuditTrail};
204pub use backlink_adapters::{
205    AdapterError, AhrefsAdapter, BacklinkAdapter, BacklinkAdapterRegistry, ExternalBacklink,
206    GscAdapter, MajesticAdapter,
207};
208pub use backlinks::{Backlink, BacklinkAnalyzer, BacklinkReport, BacklinkSummary, PageScore};
209pub use backpressure::{BackpressureController, BackpressureError, BoundedPipeline};
210pub use circuit_breaker::{
211    CircuitBreaker, CircuitBreakerConfig, CircuitBreakerRegistry, CircuitState,
212};
213pub use determinism::DeterminismController;
214pub use dns::{DnsCache, DnsError, DnsPrefetcher};
215pub use encryption::{EncryptionConfig, EncryptionError, EncryptionManager};
216pub use feature_flags::{
217    FeatureFlags, SharedFeatureFlags, FLAG_AI_ANALYZERS, FLAG_JS_RENDERING, FLAG_WASM_ANALYZERS,
218};
219pub use http::{FetchStreamReader, HttpClient, HttpClientConfig};
220pub use js_render_decision::{JsRenderDecision, JsRenderDecisionEngine, SpaIndicators};
221
222pub use observability::{Metrics, MetricsSnapshot, SharedMetrics};
223pub use playwright::{
224    BrowserContext, BrowserType, ConsoleMessage, NetworkRequest, PlaywrightConfig,
225    PlaywrightDetector, PlaywrightError, PlaywrightRenderer, RenderedPage,
226    WasmError as PlaywrightWasmError,
227};
228pub use resource_monitor::{ResourceLimits, ResourceMonitor, ResourceUsage};
229pub use robots::RobotsTxtCache;
230pub use rum::{
231    CruxAdapter, CruxData, FieldMetrics, GoogleAnalyticsAdapter, LabMetrics, MergedMetrics,
232    MetricDeltas, RumDataPoint, RumError,
233};
234pub use sitemap::SitemapCache;
235pub use storage::{
236    CacheStats, CrawlStats, Issue, IssueCategory, IssueFilter, Severity, StorageError,
237};
238pub use wasm_analyzers::{WasmPatternAnalyzer, WasmPerformanceAnalyzer, WasmRuntimeAnalyzer};
239
240/// HTML meta tag extraction (title, description, OG, Twitter Cards, hreflang).
241///
242/// Provides [`MetaTags`](meta::MetaTags) with helper methods for checking
243/// `noindex`/`nofollow` directives and measuring tag lengths.
244pub mod meta;
245/// HTML parser that extracts links, headings, images, forms, and structured data.
246///
247/// [`HtmlParser::parse`] produces a [`ParsedPage`] with all SEO-relevant data
248/// extracted from raw HTML, including accessibility landmarks and social metadata.
249pub mod parser;
250
251pub use meta::{HreflangTag, MetaTags, OpenGraphTags, TwitterTags};
252pub use parser::{
253    ExtractedForm, ExtractedImage, ExtractedInput, ExtractedLink, Heading, HtmlParser, ParseError,
254    ParsedPage, ScriptInfo, StructuredData, StyleInfo,
255};
256
257mod duration_ms {
258    use serde::{Deserialize, Deserializer, Serialize, Serializer};
259    use std::time::Duration;
260
261    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
262    where
263        S: Serializer,
264    {
265        duration.as_millis().serialize(serializer)
266    }
267
268    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
269    where
270        D: Deserializer<'de>,
271    {
272        let ms = u64::deserialize(deserializer)?;
273        Ok(Duration::from_millis(ms))
274    }
275}
276
277mod opt_duration_ms {
278    use serde::{Deserialize, Deserializer, Serialize, Serializer};
279    use std::time::Duration;
280
281    pub fn serialize<S>(duration: &Option<Duration>, serializer: S) -> Result<S::Ok, S::Error>
282    where
283        S: Serializer,
284    {
285        duration.map(|d| d.as_millis()).serialize(serializer)
286    }
287
288    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
289    where
290        D: Deserializer<'de>,
291    {
292        let ms = Option::<u64>::deserialize(deserializer)?;
293        Ok(ms.map(Duration::from_millis))
294    }
295}
296
297/// Errors that can occur during crawl operations.
298///
299/// This is the primary error type for the crawlkit crate. It wraps
300/// lower-level errors from URL parsing, HTTP requests, robots.txt,
301/// and storage operations.
302///
303/// # Examples
304///
305/// ```rust
306/// use crawlkit_engine::CrawlError;
307///
308/// let err = CrawlError::TooManyRedirects(20);
309/// assert!(err.to_string().contains("20"));
310/// ```
311#[derive(Debug, Error)]
312pub enum CrawlError {
313    /// The URL could not be parsed.
314    #[error("invalid URL: {0}")]
315    InvalidUrl(#[from] url::ParseError),
316
317    /// The HTTP request failed.
318    #[error("request failed: {0}")]
319    RequestFailed(#[from] reqwest::Error),
320
321    /// The URL exceeded the maximum redirect limit.
322    #[error("too many redirects ({0})")]
323    TooManyRedirects(usize),
324
325    /// The URL is excluded by robots.txt disallow rules.
326    #[error("blocked by robots.txt: {0}")]
327    BlockedByRobotsTxt(String),
328
329    /// The URL is outside the allowed domain scope.
330    #[error("out of scope: {0}")]
331    OutOfScope(String),
332
333    /// A storage/database error occurred.
334    #[error("storage error: {0}")]
335    Storage(String),
336
337    /// All retry attempts were exhausted.
338    #[error("max retries exceeded after {0} attempts")]
339    MaxRetriesExceeded(usize),
340}
341
342/// Configuration for a crawl session.
343///
344/// Controls the starting URL, crawl limits, politeness settings, and
345/// URL filtering patterns. Implements `Default` with sensible values
346/// for most use cases.
347///
348/// # Examples
349///
350/// ```rust
351/// use crawlkit_engine::CrawlConfig;
352/// use std::time::Duration;
353///
354/// let config = CrawlConfig {
355///     max_pages: 500,
356///     request_delay: Duration::from_millis(200),
357///     concurrency: 8,
358///     ..Default::default()
359/// };
360/// assert_eq!(config.max_pages, 500);
361/// ```
362#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct CrawlConfig {
364    /// The starting URL to crawl.
365    pub start_url: Url,
366
367    /// Maximum number of pages to crawl.
368    pub max_pages: usize,
369
370    /// Maximum crawl duration. `None` means no time limit.
371    #[serde(default, with = "opt_duration_ms")]
372    pub max_time: Option<Duration>,
373
374    /// Maximum crawl depth from the starting URL. `None` means no depth limit.
375    pub max_depth: Option<usize>,
376
377    /// Delay between requests to the same domain.
378    #[serde(with = "duration_ms")]
379    pub request_delay: Duration,
380
381    /// Number of concurrent fetchers per domain.
382    pub concurrency: usize,
383
384    /// HTTP request timeout.
385    #[serde(with = "duration_ms")]
386    pub request_timeout: Duration,
387
388    /// User-Agent string to send with requests.
389    pub user_agent: String,
390
391    /// Maximum number of redirects to follow.
392    pub max_redirects: usize,
393
394    /// Whether to respect robots.txt directives.
395    pub respect_robots_txt: bool,
396
397    /// Allowed URL patterns (glob-style).
398    pub allowed_patterns: Vec<String>,
399
400    /// Disallowed URL patterns (glob-style).
401    pub disallowed_patterns: Vec<String>,
402}
403
404impl Default for CrawlConfig {
405    fn default() -> Self {
406        Self {
407            start_url: Url::parse("https://example.com")
408                .unwrap_or_else(|_| unreachable!("static URL string is always valid")),
409            max_pages: 100,
410            max_time: None,
411            max_depth: None,
412            request_delay: Duration::from_millis(500),
413            concurrency: 4,
414            request_timeout: Duration::from_secs(30),
415            user_agent: format!("crawlkit/{}", env!("CARGO_PKG_VERSION")),
416            max_redirects: 20,
417            respect_robots_txt: true,
418            allowed_patterns: Vec::new(),
419            disallowed_patterns: Vec::new(),
420        }
421    }
422}
423
424/// Represents a single URL discovered during crawling.
425///
426/// Tracks the original URL, its canonical form, the referring page,
427/// crawl depth, and discovery timestamp. Used by the crawl queue
428/// and storage layers.
429#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct UrlEntry {
431    /// The original URL as discovered.
432    pub url: Url,
433
434    /// The canonical URL after normalization.
435    pub canonical_url: Url,
436
437    /// The page that linked to this URL.
438    pub referrer: Option<Url>,
439
440    /// The depth from the starting URL (0 = start).
441    pub depth: usize,
442
443    /// When this URL was discovered.
444    pub discovered_at: DateTime<Utc>,
445}
446
447/// The result of fetching a URL.
448///
449/// Contains the final URL (after redirects), HTTP status, response headers,
450/// body content, timing, and size information. This is the primary output
451/// of [`HttpClient::fetch`](http::HttpClient::fetch).
452#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct FetchResult {
454    /// The final URL after following redirects.
455    pub final_url: Url,
456
457    /// HTTP status code.
458    pub status_code: u16,
459
460    /// Response headers.
461    pub headers: Vec<(String, String)>,
462
463    /// The response body as a UTF-8 string.
464    pub body: String,
465
466    /// Time taken for the request.
467    #[serde(with = "duration_ms")]
468    pub response_time: Duration,
469
470    /// Size of the response body in bytes.
471    pub body_size: usize,
472
473    /// When the request was made.
474    pub fetched_at: DateTime<Utc>,
475}
476
477/// A single hop in a redirect chain.
478///
479/// Records the source and destination URLs along with the HTTP status code
480/// (301, 302, 307, 308) for each redirect. Multiple hops form a
481/// [`RedirectChainAnalyzer`](analyzers::RedirectChainAnalyzer) input.
482#[derive(Debug, Clone, Serialize, Deserialize)]
483pub struct RedirectHop {
484    /// The URL that redirected.
485    pub from: Url,
486
487    /// The URL redirected to.
488    pub to: Url,
489
490    /// The HTTP status code (301, 302, 307, 308).
491    pub status_code: u16,
492}
493
494#[cfg(test)]
495#[allow(clippy::unwrap_used)]
496mod tests {
497    use super::*;
498
499    #[test]
500    fn test_crawl_config_default() {
501        let config = CrawlConfig::default();
502        assert_eq!(config.max_pages, 100);
503        assert_eq!(config.concurrency, 4);
504        assert_eq!(config.max_redirects, 20);
505        assert!(config.respect_robots_txt);
506    }
507
508    #[test]
509    fn test_crawl_config_serialization() {
510        let config = CrawlConfig::default();
511        let json = serde_json::to_string(&config).unwrap();
512        let deserialized: CrawlConfig = serde_json::from_str(&json).unwrap();
513        assert_eq!(config.max_pages, deserialized.max_pages);
514        assert_eq!(config.request_delay, deserialized.request_delay);
515        assert_eq!(config.request_timeout, deserialized.request_timeout);
516    }
517
518    #[test]
519    fn test_url_entry_serialization() {
520        let entry = UrlEntry {
521            url: Url::parse("https://example.com").unwrap(),
522            canonical_url: Url::parse("https://example.com").unwrap(),
523            referrer: None,
524            depth: 0,
525            discovered_at: Utc::now(),
526        };
527
528        let json = serde_json::to_string(&entry).unwrap();
529        let deserialized: UrlEntry = serde_json::from_str(&json).unwrap();
530        assert_eq!(entry.url, deserialized.url);
531        assert_eq!(entry.depth, deserialized.depth);
532    }
533
534    #[test]
535    fn test_redirect_hops() {
536        let hops = [
537            RedirectHop {
538                from: Url::parse("https://example.com/old").unwrap(),
539                to: Url::parse("https://example.com/mid").unwrap(),
540                status_code: 301,
541            },
542            RedirectHop {
543                from: Url::parse("https://example.com/mid").unwrap(),
544                to: Url::parse("https://example.com/new").unwrap(),
545                status_code: 302,
546            },
547        ];
548
549        assert_eq!(hops.len(), 2);
550        assert_eq!(hops[0].status_code, 301);
551        assert_eq!(hops[1].status_code, 302);
552    }
553
554    #[test]
555    fn test_fetch_result_serialization() {
556        let result = FetchResult {
557            final_url: Url::parse("https://example.com").unwrap(),
558            status_code: 200,
559            headers: vec![("content-type".into(), "text/html".into())],
560            body: "<html></html>".into(),
561            response_time: Duration::from_millis(123),
562            body_size: 14,
563            fetched_at: Utc::now(),
564        };
565
566        let json = serde_json::to_string(&result).unwrap();
567        assert!(json.contains("123"));
568        let deserialized: FetchResult = serde_json::from_str(&json).unwrap();
569        assert_eq!(deserialized.response_time, Duration::from_millis(123));
570    }
571}