spatie/crawler
A powerful PHP package for crawling websites, using Guzzle promises to fetch multiple URLs concurrently. It can render JavaScript via Chrome and Puppeteer, restrict crawls to internal links or depth, collect found URLs, and fake HTTP responses for testing crawl logic.
spatie/crawler collapses what would otherwise be a multi-week internal project (concurrent HTTP fetching, URL queueing, internal-link scoping, redirect handling, robots.txt, depth limits, throttling) into a Composer dependency. The decision becomes "do we build a crawler or wire up callbacks?" — a very different cost curve.redirectHistory(), wasRedirected()), orphan-page detection (foundUrls() vs. sitemap), internal linking depth analysis, and sitemap-driven discovery (parseSitemaps(), including the namespaced-sitemap fix in 9.4.2). These are shippable features, not research.fake() makes crawl logic testable in CI without network calls; depth(), limit(), concurrency(), and delay() make a smoke-crawl job cheap and deterministic. That means "crawl the staging site after deploy and fail the pipeline on 5xx or broken internal links" can be scheduled as a small, low-risk roadmap item rather than an infrastructure initiative.internalOnly() + foundUrls() gives a quick inventory comparison against the pre-migration URL set — a decision-grade artifact for "are we safe to 301 and decommission?"CloudflareRenderer), so the PM decision "do we support auditing SPAs?" has a known, costed path rather than an open question. It's opt-in via suggest, so the decision is reversible per environment.shouldStopCallback(), FinishReason (Completed, CrawlLimitReached, TimeLimitReached, Interrupted), and AdaptiveThrottle/FixedDelayThrottle mean you can decide on crawl budgets, graceful shutdown behavior, and politeness policy as product/config choices instead of bespoke engineering.Consider it when most of the following hold:
internalOnly(), includeSubdomains(), and matchWww() are the design center.onCrawled, onFailed, onWillCrawl, onFinished — rather than a managed, always-on crawling service.fake()) and a permissive license (MIT) with no per-page or per-crawl fees.Look elsewhere when:
ArrayCrawlQueue; persistence, sharding, and cross-worker coordination are yours to build.CrawlResponse, CrawlProfile as interface, startCrawling() → start(), Browsershot no longer required, UriInterface removed). Budget real migration effort if you're on v8.For executives (one paragraph): We can add site-crawling capability — broken-link reports, redirect cleanup, internal-link and content inventories, post-migration URL verification — using a mature, MIT-licensed Laravel package instead of a multi-week build. It's actively maintained (frequent releases, documented, changelogged), has strong brand recognition in the PHP ecosystem (~2.8k GitHub stars), and integrates with the stack we already run. Main costs are operational rather than licensing: crawl budgets, politeness/throttling, and optional headless-Chrome infrastructure if we choose to audit JavaScript-rendered pages. It is an engine, not a monitoring product — we should scope the first release to one or two concrete reports rather than promise a platform.
For engineering (the short version): Crawler::create($url)->internalOnly()->depth(3)->foundUrls() is the whole onboarding. Under the hood it's Guzzle concurrency with a clean observer model: onCrawled(), onFailed(), onWillCrawl(), onFinished(), plus a CrawlResponse object exposing status(), body(), dom(), header(), transferStats(), and redirect history. v9 added what we'd otherwise have to write: fake() for network-free tests, FinishReason for explicit termination semantics, shouldStopCallback() for graceful external stops, retry(), stream() for memory-bounded runs, and FixedDelayThrottle/AdaptiveThrottle for politeness. Scope helpers (allowCrawl/neverCrawl, alwaysCrawl), request config (basicAuth(), token(), proxy(), cookies(), middleware()), and a JavaScriptRenderer interface mean it adapts to our auth and rendering constraints without forking. Two things to plan for: queue persistence is ours to implement if we need resumability, and the v9 upgrade is breaking — pin and migrate deliberately. Recommended first slice: a scheduled CLI job that crawls the production domain, flags non-2xx internal links and redirect chains, and writes results to our own storage.
How can I help you explore Laravel packages today?