j0k3r/php-readability
PHP library that extracts the main readable content and title from web pages. Based on an improved full-text-rss Readability fork, it offers namespaced code, simple tests, optional Tidy HTML cleanup, and PSR-3 logging for debugging parsing issues.
Architecture fit. This package is a pure, self-contained PHP library: it takes an HTML string plus a base URL, returns a DOMElement for title and content, and nothing else. That is a clean seam for a Laravel application — no service container assumptions, no HTTP client opinion, no storage opinion. It slots in as a single stateless-in-practice step inside a content pipeline you already own (fetch → extract → sanitize → persist → render). The Psr\Log\LoggerInterface injection point means extraction attempts can be traced through the same Monolog channel as the rest of the app, which is the difference between a debuggable extraction feature and a black box.
The Apache-2.0 license is the friendliest possible outcome for a commercial product: permissive, explicit patent grant, no copyleft contamination of your application code. The project is a re-namespaced fork of a fork (fivefilters → full-text-rss/Dither → j0k3r), which matters less legally than it does epistemically: the algorithm lineage is Mozilla Readability, synced at 2.0.0, and the maintainers have been explicitly backporting Mozilla behavior. Algorithmic parity with the browser's reader mode is a real, defensible product property.
Integration feasibility. Low. Composer install, one class, roughly six lines of glue. PHP >= 7.4 with CI coverage through 8.4 fits any modern Laravel release. The only non-PHP dependency is the optional Tidy extension, which is a Dockerfile line item, not an architecture decision. The 2.0.9 change making loadHtml() publicly callable independently of init() is quietly important for TPMs: it enables a two-phase workflow where you load/validate the document once and then retry extraction with different options, instead of paying full parse cost per attempt.
Technical risk.
| Risk | Severity | Note |
|---|---|---|
| Maintainer concentration | High | Effectively one primary maintainer plus two regular contributors. 184 stars, 0 dependents — the ecosystem has not stress-tested this at scale. |
| Code comprehensibility | Medium-High | The README says the code "is still really hard to understand / read." That is an honest vendor admission and predicts expensive debugging. |
| Encoding correctness | Medium | Multiple releases (1.2.13, 2.0.7, 1.3.1) are charset-decoding and tidy_repair_string encoding fixes. Non-UTF-8 and mis-declared pages will be your top bug class. |
| Extraction completeness | Medium | init() returns false on failure by design. There is no confidence score, no partial result, no diagnostic payload beyond logs. |
| Output safety | High if mishandled | It strips <script>/<style> via clean(), but it is a readability extractor, not a sanitizer. Extracted HTML retains attributes and markup. It must be followed by a real sanitizer (HTMLPurifier, symfony/html-sanitizer, or mews/purifier) before it touches a browser. |
| Resource exhaustion | Medium | Full-document DOM construction on attacker- or user-supplied HTML. No size cap, no timeout, no depth guard. You must enforce these upstream. |
Key questions for the team.
init() returns false — raw HTML, truncated text, or a "view original" link? The UX contract matters more than the library.Stack fit. Native Laravel fit, with three thin wrappers you should build rather than skip:
ReadabilityExtractor service (singleton, but instantiated per document — the object is stateful across loadHtml/init) behind an interface, so swapping to a Node sidecar running mozilla/readability later is a config change, not a refactor.ExtractArticleContent) rather than inline execution. Extraction is CPU-bound and unpredictable; it does not belong in a request/response cycle.ContentExtraction value object returning title, sanitized HTML, plain text, word count, and a status enum — never let DOMElement leak into your domain layer.Suggested pipeline: HTTP fetch (Laravel HTTP client, strict timeouts) → charset normalization → size guard (reject > ~5 MB) → Readability → sanitizer → persistence → cache.
Migration path. Assume you are replacing one of: (a) a paid extraction API (Mercury/Postlight, Diffbot), (b) a Node microservice, or (c) nothing — regex or naive strip_tags. In all three cases the migration is additive, not destructive: run the new extractor in shadow mode against a sample of live URLs, store both outputs, and diff. Only cut over when the success rate and output fidelity are demonstrably at or above the incumbent. Cost savings from killing a per-call API is the most likely business case and is easy to quantify.
Compatibility. PHP 7.4–8.4 verified by CI. psr/log 1.x/2.x/3.x all accepted, which means no conflict with Monolog 2 or 3. Parser is selectable (libxml default vs masterminds/html5); note that the HTML5 parser moved from the abandoned electrolinux/php-html5lib to masterminds/html5 in 1.2.5, so older lockfiles may need attention. Two live major lines (1.x and 2.x) receive parallel fixes — pin to ^2.0 and do not straddle.
Sequencing.
Maintenance. Low code churn, but the dependency is a quality dependency, not a stability dependency: you are relying on someone else keeping the algorithm current as publishers change their markup. Release cadence is healthy (2.0.x maintained in parallel with 1.x, Dependabot enabled, PHPStan, php-cs-fixer, Rector, code coverage published), which is a good maintenance signal. The counterweight is the bus factor and 0 dependents — you are an early adopter, not a follower. Budget roughly one engineer-day per quarter for dependency bumps and extraction-quality triage, plus a standing contingency: fork to an internal namespace if upstream stalls. That fork is cheap precisely because the surface area is one class plus a handful of DOM helpers.
Support. Support burden will not come from crashes; it will come from "this article came out wrong" tickets. Mitigations: always log the source URL and a hash of the input HTML with the extraction result; keep the "original page" link permanently; expose a per-URL internal debug command that re-runs extraction with a Monolog DEBUG handler attached. Without these, every bad extraction becomes an unbounded investigation.
Scaling. Embarrassingly parallel and horizontally trivial — no shared state, no external I/O. Cost is CPU and memory per document: DOM construction dominates, and pathological pages (huge comment threads, deeply nested tables) can spike memory well past the input size. Size-cap at ingress and give queue workers generous memory_limit with a per-job guard. The real scaling lever is caching: extracted content is a pure function of (HTML, URL, options), so cache on a content hash and you convert a compute problem into a lookup. At meaningful volume, extraction cost becomes noise relative to the fetch.
Failure modes, ranked by likelihood.
init() returns false, job succeeds, user sees a blank reader view. Treat false as a typed failure, not a boolean, and have an explicit fallback UI.meta[charset]-after-title pages.Ramp-up. A competent Laravel engineer ships a working integration in half a day from the README alone. Understanding why an extraction failed takes considerably longer, because the internals are dense and undocumented beyond the usage snippet. Plan for one engineer to own this end-to-end — including the regression corpus and the fork contingency — rather than distributing it across a team where nobody accumulates the debugging intuition. The package's genuine strength is that its API is so small that the ramp-up cost is concentrated and bounded; its genuine weakness is that when it misbehaves, the code itself offers little help, so your observability has to compensate.
How can I help you explore Laravel packages today?