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

Html To Markdown Laravel Package

nickcernis/html-to-markdown

Converts HTML to Markdown for PHP 7.2+, letting you edit existing HTML content as Markdown, store HTML while editing in Markdown, or turn HTML email into plain text. Install via Composer.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture fit

league/html-to-markdown is a framework-agnostic, single-responsibility library: construct HtmlConverter, hand it an HTML string, get Markdown back. There is no ServiceProvider, no facade, no config publishing, no Laravel integration surface at all. That is a feature, not a defect — it means zero coupling to our application lifecycle and no upgrade treadmill tied to Laravel major versions. The trade-off is that the TPM should budget for a thin first-party adapter rather than assuming "composer require and go."

It fits cleanly anywhere we already hold a string of trusted-or-sanitized HTML: a queued job, a console command, an artisan import, a Nova/Filament action, an API transformer. It does not fit streaming or chunked conversion well — the implementation builds a full DOMDocument from the input, walks the tree, and converts from the deepest nodes outward. Peak memory is therefore a function of the largest single HTML document we will ever process, not the average. For typical CMS article bodies and email templates this is immaterial; for full-page scrapes of 5–20 MB HTML it is a sizing question we must answer before committing.

The extension model is the strongest architectural asset. HtmlConverter::getEnvironment() and the now-public Environment::addConverter() let us register custom ConverterInterface implementations for our own house HTML patterns (custom callouts, <figure>/<figcaption>, embedded media) and opt into TableConverter explicitly where GFM tables are wanted. Because Environment was made final in 4.0.0 and method visibility was tightened in the same release, the extension contract is now stable — but it also means we cannot subclass Environment for clever wiring. Composition only.

Integration feasibility

High. Installation is composer require league/html-to-markdown, requiring PHP 7.2+ and the xml, libxml, and dom extensions. Those extensions ship by default on most distributions but are commonly disabled on CentOS/RHEL-style images and in some hardened container bases — a five-minute check in our Dockerfile and CI image, but a check that has historically cost teams a half-day when skipped. Our Laravel version is irrelevant here; the only real compatibility axes are PHP runtime version and extension availability.

Integration effort for a competent Laravel engineer is genuinely small — a singleton binding, a config array, a value-object DTO for options, and a service class wrapping convert(). The substantive work is not wiring; it is policy definition: deciding which of the ~10 documented options we standardize on and enforcing that decision centrally rather than letting each call site pass its own array.

Technical risk

The dominant risk is security-adjacent and is called out explicitly in the README's own warning: by default the library preserves HTML tags without Markdown equivalents — <span>, <div>, <iframe>, <script> and friends pass through into the output. If any conversion path accepts end-user HTML, an unconfigured converter is a stored-XSS vector wherever that Markdown is later rendered with HTML enabled. Mitigation is layered and non-negotiable: set strip_tags and/or remove_nodes on our default converter instance, and run untrusted input through HTML Purifier before conversion. The library is a converter, not a sanitizer; the README says so, and we should encode that as an ADR so it survives team turnover.

Secondary risks, ranked:

  • Fidelity gaps. Nested lists and lists inside blockquotes are listed as open to-dos in the README — these are common shapes in CMS content and in HTML email. Any workflow that round-trips rich content through Markdown will visibly degrade on them. This is our single largest product-facing risk and warrants a fixture-based fidelity spike before we commit to a use case.
  • Markdown-dialect mismatch. Output targets original Markdown, not Markdown Extra or MultiMarkdown; tables require explicit opt-in; footnote-style references are not emitted. If our renderer is CommonMark/GFM, most output is fine, but hard_break, header_style, and table support must be aligned deliberately with whichever renderer consumes the result.
  • Supply chain / maintenance continuity. MIT license, thephpleague org, led by @colinodell — a well-known, long-tenured maintainer in the PHP ecosystem. Historically healthy, with a dense changelog and semantic versioning discipline (a clean 4.x → 5.x progression). Single-maintainer concentration is the residual concern, mitigated by the license: the code is forkable and small.
  • Metadata anomalies. The dataset reports Dependents: 0 alongside ~1.9k stars and a README badge for total downloads — clearly a measurement artifact, but it means we must not treat "dependents" as a proxy for production adoption. Similarly, the listed last-release date (2026-10-19) is future-dated and internally inconsistent with the [unreleased]: ...compare/5.1.1 changelog link, implying the feed is unreliable rather than that there is genuinely a future release. Verify the actual Packagist version and commit history during the spike; do not plan against a scraped date.

Key questions

  1. What is the authoritative use case — CMS content migration, HTML-email-to-text, LLM prompt normalization, or user-authored content? Each carries a materially different risk profile and fidelity bar.
  2. Does any conversion path touch user-supplied HTML? If yes, HTML Purifier plus strip_tags/remove_nodes becomes a launch gate, not a follow-up ticket.
  3. Which renderer consumes the output, and does it match the library's dialect assumptions (tables, hard breaks, header style, autolinks)?
  4. What is the maximum document size we must support, and does that fit comfortably in a worker's memory ceiling?
  5. Is one-way conversion sufficient, or do we need a round-trip guarantee? If round-trip, this package alone will not satisfy it.
  6. Who owns the adapter — platform team as shared infrastructure, or the consuming feature team?

Integration Approach

Stack fit

Excellent, with one caveat that drives the whole plan: it is not a Laravel package, so there is nothing to php artisan vendor:publish. We build the integration ourselves, and we should build it once — a MarkdownConversionService in our application layer, bound as a singleton, exposing an intent-revealing API (e.g. convertArticleBody(), convertEmailToText()) rather than leaking HtmlConverter into controllers. Consumers should never new HtmlConverter() directly; that is how option drift starts.

Configuration belongs in config/markdown.php keyed by profile — default, email, llm, strict — each mapping to a documented option set. Profiles make the security posture auditable in one file and let us tighten strip_tags globally without a scattered refactor.

Migration path

Greenfield, so no data migration. The migration work is organizational: replacing any ad-hoc regex-stripping or strip_tags() calls currently used for HTML-to-text with the new service. Inventory those first — regex-based HTML stripping is a common latent bug source and a good "found value" item for the rollout narrative.

If a fidelity blocker appears (nested lists being the likely candidate), the fallback path is a custom ConverterInterface for the offending node type registered on the Environment, which is a contained, incremental fix rather than a fork. That escape hatch is why the extension model matters more than the default output quality.

Compatibility

Confirm PHP runtime ≥ 7.2 (almost certainly satisfied) and verify dom, xml, libxml in the base image and in CI. Pin the major version constraint in composer.json and let Renovate/Dependabot surface minor bumps. Watch for the 4.x → 5.x boundary if we are starting on 4.x: 4.0.0 was the release that stabilized visibility and finalized Environment, so treat 4.x as the practical floor. Align the bold_style/italic_style/hard_break/header_style choices with our renderer at configuration time, not at debug time.

Sequencing

  1. Spike (2–3 days). Throwaway branch, dependency installed, representative corpus assembled — 20–30 real documents spanning CMS articles, HTML emails, tables, nested lists, blockquotes, code blocks, and inline media. Convert and manually diff. This is the go/no-go gate on fidelity.
  2. Security decision (parallel, ~1 day). ADR documenting the sanitization boundary: Purifier before, strip_tags/remove_nodes at, and where the output is rendered. Sign-off from whoever owns application security.
  3. Adapter build (~3–5 days). Service class, config profiles, singleton binding, custom converters only where the spike proved them necessary.
  4. Golden-file test suite (~2 days). Snapshot tests over the spike corpus so fidelity regressions surface on dependency bumps. This is the highest-leverage item in the plan and the one most often skipped.
  5. Pilot one consumer. Pick the lowest-blast-radius call site.
  6. Broaden. Additional call sites, profiles tuned per use case.

Total realistic elapsed time for a production-ready integration: 2–3 weeks for one engineer, most of it in steps 1 and 4.

Operational Impact

Maintenance

Low steady-state. Small, focused, semantically versioned dependency with an MIT license and a documented changelog — upgrading is usually a composer update and a green test run. The recurring cost is not the library; it is our adapter and golden-file suite, which becomes shared platform surface that needs an owner. Budget a few hours per quarter for dependency review plus the occasional fidelity regression triage. Because Environment is final and visibility was tightened in 4.0.0, we should not build clever subclass-based extensions — keep customization to ConverterInterface implementations, which are the sanctioned path.

Support

Developer-facing library, no end-user support surface. The realistic support burden is internal: "why did this document convert differently than expected?" The answer should always be a profile lookup or a fixture test, not an investigation — which is exactly what the config profiles and snapshot suite buy us. Document the fidelity limitations (nested lists, no Extra/MultiMarkdown, inline-only links and images) in the adapter's README so the answer exists before the question is asked.

Scaling

CPU-bound string processing over a DOM tree. Throughput scales horizontally with workers; there is no shared state and no I/O, so queue-based conversion scales near-linearly. Two caveats:

  • Memory per document is the constraint, not throughput. A DOMDocument of a large HTML page can run several times the source size in memory. Set an input size ceiling in the adapter (reject or truncate above, say, 2 MB for synchronous paths) rather than discovering the ceiling via OOM-killed workers.
  • Don't do it in the request cycle for anything user-facing and non-trivial. Dispatch to a queue job; conversion on save, not on read. The 3.x changelog's Linux-line-ending normalization also means output is byte-stable — good for caching converted results by content hash.

Failure modes

  • Malformed HTML. DOMDocument is lenient, but deeply broken input can produce surprising trees rather than a clean exception. Wrap convert() and log input hashes on unexpected output; never let a conversion failure block a user's save. Degrade to "keep the HTML" rather than losing content.
  • Silent XSS passthrough. The default-preserve behavior means a successful conversion can be the dangerous outcome. Covered by the config policy; the failure is silent, which is why it must be defaulted correctly rather than reviewed per call site.
  • Fidelity degradation on upgrade. A minor bump changes output for an edge-case node. Caught by golden files; without them, caught by users.
  • Missing PHP extensions in a new base image. Manifests instantly as Class 'DOMDocument' not found. Add an extension assertion to CI and to container health checks.
  • Maintainer abandonment. Low likelihood, but the mitigation is cheap and should be written down: the library is small, MIT, and forkable, and our adapter isolates it behind one interface — a swap is an engineering day, not a project.

Ramp-up

Effectively zero for a PHP developer. The API is one method; the README documents every option with a runnable example, and the changelog is unusually clear about behavioral changes across majors. The learning is not "how do I call this" — it is "which options do we standardize on and why," and that knowledge lives in our ADR and config, not in the upstream docs. Plan a short onboarding note for the adapter and treat the upstream README as the reference for everything below it.

Overall: a low-risk, well-scoped, appropriately boring dependency. The library itself is not where the risk lives; the risk lives in (a) the default tag-preservation behavior and (b) nested-list fidelity. Both are addressable in the first week, both are cheap to verify, and neither is a reason to look for an alternative. The opportunity score of 5.56 reads correctly: this is a solved problem with an established solution, so the opportunity is not in the package but in standardizing and sanitizing how we use it.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin