Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Graby Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture fit

  • Graby is a framework-agnostic PHP library, not a Laravel package: no service provider, no facade, no config publishing. It fits cleanly behind an application-owned port, but any Laravel ergonomics must be built by us (~1 day of glue).
  • Its job is narrow and well-defined: fetch a URL, extract readable article HTML plus title, language, date, authors, lead image, and summary. That maps well to a discrete, queueable domain service (e.g. "article ingestion") rather than to inline request-path logic.
  • Decoupled HTTP via HTTPlug is genuinely useful for us: we can inject the Guzzle client we already configure with proxies, timeouts, and middleware, rather than inheriting a hardcoded client.
  • The 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

  • Straightforward mechanically: 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.
  • Two hard prerequisites gate us: PHP >= 8.2 and the 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.
  • No official Laravel support surface means version drift is ours to absorb. Because the library has no framework coupling, Laravel upgrades carry near-zero blast radius — the inverse trade-off is that nothing is tested against our stack by the maintainer.

Technical risk

  • Stability of the dependency target (highest). The v3 line is documented as 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.
  • Licensing provenance. The package declares MIT, but it is a fork of Full-Text RSS v3.3, which has historically carried copyleft terms. This needs a legal read before we distribute or embed it in a product; I would not assume the MIT badge resolves it.
  • SSRF exposure. Any server-side URL fetcher is an attack surface. 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.
  • Silent failure. 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.
  • Extraction quality is best-effort. No JS execution, so client-rendered pages and many paywalled/modern SPA sites will yield nothing useful. Accuracy depends on heuristic readability plus per-site config, which decays as third-party markup changes.
  • Community signal is modest. 391 stars, 0 listed dependents, effectively one maintainer. That is acceptable for a commodity utility but argues for abstraction and an exit path rather than deep coupling.
  • Content handling. With 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

  1. What is the legal position on MIT-vs-GPLv3 upstream heritage for our intended distribution model (SaaS vs shipped product)?
  2. Which v3 commit do we pin, and who reviews upstream diffs on a schedule?
  3. Is JS-rendered content in scope? If yes, this package alone is insufficient and we need a headless browser tier alongside it.
  4. What is the acceptable extraction quality bar, on what golden set of URLs, measured how (title accuracy, body recall, image presence)?
  5. Do we need to store raw extracted HTML, or only derived fields? That decision drives storage, retention, and copyright exposure.
  6. Who owns the per-site config tuning backlog, and is that an engineering or content-ops function?
  7. Is there an existing Full-Text RSS sidecar or third-party extraction API in the estate that this would replace or duplicate?

Integration Approach

Stack fit

  • Laravel 11/12 on PHP 8.2+ is a direct match; Guzzle 7 is what Laravel already ships, so php-http/guzzle7-adapter reuses known ground.
  • Bind 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.
  • Wrap the call in an application-owned 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.
  • Register 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

  • If we are currently shelling out to a Full-Text RSS instance or calling a hosted extraction API, introduce the interface first and implement both backends behind it. Dark-launch the Graby backend on a shadow percentage, diff outputs against the incumbent, and cut over on measured parity rather than on a date.
  • If this is greenfield, still build the interface — the migration then happens on day one and costs nothing.
  • Retire the sidecar last: run it read-only for a defined soak period so rollback is a config toggle, not a redeploy.

Compatibility

  • PHP: requires 8.2 minimum — fine for current Laravel, but check any legacy services sharing the monorepo/image.
  • Guzzle: v3 dropped Guzzle 5/6; if any adjacent service pins an older Guzzle, dependency conflicts will surface at install time. Resolve in the spike, not in the sprint.
  • Extensions: add tidy to the runtime image and CI matrix; verify parity between local, CI, and production images before feature work begins.
  • Monolog: the optional handler is a normal PSR-3 handler and coexists with Laravel's logging stack; no conflict, but pin the level.
  • No framework lock-in means Laravel upgrades are decoupled — treat that as a reason to invest in the wrapper, not a reason to skip it.

Sequencing

  1. Spike (3–5 days): throwaway artisan command, real Docker image with tidy, 20–30 representative URLs, capture quality against a hand-scored baseline. Go/no-go on quality.
  2. Legal/compliance gate: licence provenance and content-retention position confirmed in parallel with the spike.
  3. Foundation: pin commit hash, composer.json constraint, service provider, config file, ArticleExtractor contract, logging channel.
  4. Guardrails: egress controls and SSRF deny-list at the HTTP layer; per-host rate limiting; hard timeouts; response size caps.
  5. Execution model: queued job with timeout, limited retries, backoff, and idempotency keyed on URL + content hash. Never call this synchronously in a web request.
  6. Quality harness: golden-set regression run in CI so upstream bumps are evaluated, not gambled on.
  7. Rollout: feature flag, shadow traffic, cutover, then decommission the incumbent path.

Operational Impact

Maintenance

  • Expect low-volume but non-zero upkeep: dependency pin reviews, occasional upstream bumps, and a steady trickle of per-site config adjustments as target sites change. Budget roughly a few days per quarter, front-loaded.
  • The dev-master pin is the main recurring cost. Mitigation: pin to a commit SHA, subscribe to releases, and maintain a fork we can patch — the licence permits it and the codebase is small enough to reason about.
  • Site-config heuristics are the perishable asset. Decide up front whether we tune them in-house or accept upstream's cadence; either way, someone must own the regression suite.

Support

  • No vendor, no SLA. Support is the maintainer's issue tracker plus a Gitter channel, and effectively one person. Escalation plan must be "we fix it in our fork," which requires at least one engineer able to read the extraction pipeline.
  • Internal runbook needed for the three common tickets: "content came back empty," "content came back wrong," "request timed out." Each has a distinct diagnostic path.
  • Extraction quality complaints will arrive as content bugs, not infrastructure bugs. Route them to a triage queue with a URL, timestamp, and stored raw response — without the raw HTML, these are unreproducible.

Scaling

  • Workload is CPU- and memory-bound (DOM parse, Tidy, readability passes), not I/O-bound in the way typical API calls are. Scale with queue workers, not with more web containers.
  • Concurrency must be capped per origin host. Fanning out aggressively at a single domain is how we earn 429s and IP blocks, which then look like extraction failures.
  • Cache aggressively: key on normalised URL, store derived fields with a TTL, and treat the raw HTML as short-lived (hours to days) rather than permanent. Re-extraction on cache miss is cheap; unbounded HTML retention is not.
  • Large pages can spike memory. Set a response size ceiling and a per-job memory limit; a single pathological page should fail one job, not a worker.
  • Cost is dominated by worker hours, which is predictable and easy to model from the golden-set timing run.

Failure modes

  • Silent empty extraction — the dominant risk. Status 200 with sentinel content. Detect via explicit checks on status, body length, and the configured error_message/error_message_title strings, and emit a metric rather than an exception the queue swallows.
  • Upstream blocking — 403/429/robots-driven rejection. Distinguish from parse failure in metrics; retries are usually wrong here and worsen the block.
  • Redirect loops and slow origins — bounded by max_redirect and client timeout; without both, a single URL can hold a worker indefinitely.
  • Non-HTML content types — PDFs, media, archives. content_type_exc handles this gracefully if configured; otherwise expect empty bodies and confused consumers.
  • JS-only pages and paywalls — structurally unextractable. Should surface as a distinct "not extractable" outcome, not as a generic error, so downstream logic can fall back to metadata-only records.
  • Extension drift — a base image rebuild without tidy turns every call into a failure with a misleading error. Assert extension presence at boot.
  • Sanitisation gap — extracted HTML stored and later rendered without our own escaping. Treat all extracted markup as hostile regardless of xss_filter.

Ramp-up

  • Calling the API correctly: under a day for a mid-level Laravel engineer.
  • Production-grade integration (contract, provider, config, queue job, guardrails, metrics): roughly one sprint.
  • Quality tuning and the golden-set harness: a second sprint, and it needs a named owner rather than a shared assumption.
  • Overall this is a low-ceremony dependency with a moderate operating discipline attached. The library is fine; the risk sits in the unpinned v3 branch, the licence question, and the SSRF surface — all three are ours to close, and all three are closable. I would fund the spike and the legal check before committing to a build.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
sanjid29/laravel-starter-core
vanda/core
mimon/kafka-logger
webup/laravel-helium
hadiabedzadeh/laravel-journey-builder
shadowbane/laravel-gelf-logger
devloopsnet/bagisto-mastercard
sunxyw/laravel-admin
voxyfy/anadolupay
emild/laravel-metable
timedoor/baskito
larva/tencentcloud-sdk-php
jund-fauz/filament-kompact
atannex/panels
sahanranasingha/sri-lanka-holidays
alan2k2/cna-framework
moztopia/access
itorgov/laravel-forge-bot
backstage/laravel-ok
mr-swapan/applinks