j0k3r/graby
Graby extracts article content from web pages. It's a fully tested, Composer-friendly fork of Full-Text RSS built on php-readability and site_config, with decoupled HTTP clients. Requires PHP 8.2+ plus Tidy and cURL.
Architecture fit
setContentAsPrefetched() and cleanupHtml() entry points mean we can reuse it for sanitisation/normalisation of HTML we already hold (e.g. from an email ingest or a partner feed), which broadens the value beyond crawling.Integration feasibility
composer require, construct Graby, call fetchContent(), read the typed result object. The README's worked examples for success and error payloads are sufficient to build against.tidy + curl extensions. tidy is not in the default PHP images most teams run; this is a Dockerfile/CI change, not a blocker, but it must be scheduled.Technical risk
dev-master only and is explicitly API-incompatible with the stable 2.x branch. Depending on a branch head is non-reproducible and exposes us to unreviewed upstream commits; we would need to pin a commit hash and treat upgrades as reviewed events.allowed_urls/blocked_urls are substring matching convenience settings, not network controls — they will not stop DNS rebinding or redirects into internal address space. Real mitigation lives in the HTTP client layer (egress proxy, link-local/RFC1918 deny, max_redirect), which we own.fetchContent() does not throw; it returns HTTP status and sentinel strings like [unable to retrieve full-text content]. Any caller that checks only for exceptions will happily persist empty content. This is a correctness trap, not just an ops nuisance.xss_filter enabled, htmLawed strips iframes and some embeds; with it disabled, we are storing untrusted third-party HTML. Either way our own output-escaping and sanitisation policy must remain authoritative.Key questions
Stack fit
php-http/guzzle7-adapter reuses known ground.Graby as a singleton in a dedicated service provider, with settings externalised to config/graby.php sourced from env (timeout, allowed/blocked lists, xss_filter, content_links, singlepage/multipage). Nothing else in the README's long config block should be hardcoded.ArticleExtractor interface with a GrabyExtractor implementation. This is the single most important design decision: it isolates the dev-master pin, gives us a seam for a second backend, and makes the golden-set harness trivially mockable.Graby\Monolog\Handler\GrabyHandler as a named Laravel log channel at info in production and debug only in local/staging — debug dumps full HTML at each step and is a data-retention hazard.Migration path
Compatibility
tidy to the runtime image and CI matrix; verify parity between local, CI, and production images before feature work begins.Sequencing
tidy, 20–30 representative URLs, capture quality against a hand-scored baseline. Go/no-go on quality.composer.json constraint, service provider, config file, ArticleExtractor contract, logging channel.Maintenance
Support
Scaling
Failure modes
error_message/error_message_title strings, and emit a metric rather than an exception the queue swallows.max_redirect and client timeout; without both, a single URL can hold a worker indefinitely.content_type_exc handles this gracefully if configured; otherwise expect empty bodies and confused consumers.tidy turns every call into a failure with a misleading error. Assert extension presence at boot.xss_filter.Ramp-up
How can I help you explore Laravel packages today?