mdream: an HTML to Markdown converter tuned for LLM input
☁️ The fastest HTML to markdown convertor on GitHub. Optimized for LLMs and supports streaming.
At a glance
- What is it?
- mdream is a zero-dependency HTML to Markdown converter from harlan-zw, shipped as a Rust NAPI engine plus a WASM build, a pure JS engine, a crawler, a Vite plugin and a GitHub Action. The interesting part is not the conversion itself but the preset system that strips markup an LLM does not need, and the split between a native path and a portable one.
- Who is it for?
- Adopt mdream if you are feeding web pages or static HTML into a model and want the conversion step to be a thin, dependency-free pipe rather than a browser-grade DOM library. Skip it if you need CSS selector based extraction, or if you are already committed to Turndown plugins, since mdream's hook surface and preset model do not map onto that ecosystem.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The problem is not HTML to Markdown, it is HTML to Markdown for a context window
Converting HTML to Markdown is a solved problem in the sense that several libraries do it. What mdream targets is a narrower case: you have a page of HTML and you want to put it in a prompt. In that setting the failure mode of a general converter is not broken output, it is output that carries navigation, footers, script remnants and inline styling the model pays for in tokens and gains nothing from. The README frames the project around this directly, describing a converter that is "Optimized for LLMs" and claiming "Up to 2x fewer tokens than Turndown, node-html-markdown, and html-to-markdown" plus "70-99% fewer tokens than raw HTML". Those are the project's own benchmarks, not something verified here. The audience is anyone building a retrieval or ingestion pipeline over web content, plus people who want to publish a Markdown or llms.txt version of their own site. The second group matters because mdream ships a crawler and a Vite plugin for exactly that, so the project is doing two jobs under one name.
Presets are the actual product, and minimal is the one that changes output
The repository lists a `minimal` preset at `packages/mdream/src/preset/minimal.ts` and describes it as generating "Minimal GitHub Flavored Markdown: Frontmatter, Nested & HTML markup support". The default conversion path and the minimal path are different enough that the README uses `--preset minimal` in every CLI example, including the one-liner `curl -s https://en.wikipedia.org/wiki/Markdown | npx mdream --preset minimal`. That is a signal worth reading: the project's own documentation does not show the no-preset output as the recommended one. If you adopt mdream, the preset choice is the first thing to evaluate, because it decides how much structure survives. The README does not spell out, in the material available, exactly which elements the minimal preset drops, so the honest way to decide is to run both on a representative page and diff them. A converter that removes a table or a code fence to save tokens is not obviously a win for every pipeline.
Three engines, and the choice between them is a deployment decision
mdream is not one library. The package table lists `mdream` (Rust NAPI engine plus WASM for edge, with a CLI), `@mdream/js` (pure JS engine, described as having "Full hook access, zero native deps" and tree-shakable conversion via a `/core` entry point), and a Rust crate published on crates.io with its own CLI and streaming support. The README claims the Rust NAPI path is "Up to 37x faster than Turndown (Rust NAPI vs JS)" and "4.6x, 5x faster than htmd (Rust vs Rust)", and states a 1.8MB HTML document converts in "~5.2ms (Rust)". Treat those as author-published figures. The practical consequence is that you are choosing between speed and portability: the NAPI engine needs a platform-specific binary, while the JS package has "zero native deps" and is the one that survives a bundler or a serverless runtime that will not load a native addon. The WASM build is the middle option for edge environments. Size is given as 10kB gzip for the JS core and 60kB gzip with the Rust WASM engine, both zero-dependency.
Streaming is the feature that separates it from a one-shot converter
The README lists streaming twice, once as "Memory efficient streaming for large documents and real-time pipelines" and again in the Rust crate description as "streaming support". The CLI examples show this in practice: `curl -s https://en.wikipedia.org/wiki/Markdown | npx mdream --origin https://en.wikipedia.org --preset minimal | tee streaming.md` pipes an HTTP response body straight into the converter and writes the result through `tee`. Nothing in that pipeline buffers the whole page before conversion starts, which is the point. For a crawler ingesting thousands of pages, or a server that converts a fetched page before forwarding it to a model API, holding the full HTML string plus the full Markdown string in memory per request is the cost you avoid. The `--origin` flag is a small but load-bearing detail: without it, relative image and link paths in the source page stay relative and are useless once the Markdown leaves its original context. The README notes it "will fix relative image and link paths".
Getting it running: one command for a page, one package for a site
The shortest path is the npx invocation shown in the README: `curl -s https://en.wikipedia.org/wiki/Markdown | npx mdream --preset minimal`. Add `--origin https://en.wikipedia.org` when the page uses relative URLs. For a local file, the README's example pipes the file through the CLI and writes the result with `tee`, the same pattern as the URL case. Beyond the CLI, the project ships `@mdream/crawl` for "Site-wide crawler to generate llms.txt artifacts from entire websites", `@mdream/vite` to generate `.md` files for Vite sites, and `@mdream/action` to generate `.md` and `llms.txt` artifacts from static `.html` output in CI. There are also pre-built Docker images, described as a small `core` converter image and a `crawl` image that bundles Playwright Chrome. That second image is the tell that crawling JavaScript-rendered sites is a different problem from converting HTML you already have, and it costs you a browser in the container. For browser use there is a CDN path via unpkg or jsDelivr with no build step. The material here does not include the full option list or config schema, so treat the CLI flags shown as examples rather than the complete surface.
Where it is the wrong tool
mdream converts HTML. It does not select it. If your pipeline needs to pull one article body out of a page with a known CSS selector, or strip a cookie banner by class name, you are looking at a different layer of the stack, and mdream's preset-based approach is a coarser instrument than a selector-driven extractor. The presets are fixed profiles rather than per-site rules, and the README does not present a selector configuration in the material available. A second boundary is the native engine. A Rust NAPI binary is a build and distribution artifact, and the README itself routes around it by offering a pure JS package and a WASM build, which is an admission that the fast path does not run everywhere. If your deployment target is a constrained serverless runtime, assume you are on the JS or WASM path and that the headline speed numbers do not apply to you. Third, the token claims are comparative, not absolute: "70-99% fewer tokens than raw HTML" says nothing about whether the Markdown that remains is the part your model needed. If a page's value lives in a data table the minimal preset trims, fewer tokens is a worse result.
Turndown is the reference point, and the difference is scope, not syntax
Turndown is the obvious comparison and the one the README names first. Both take an HTML string and return Markdown. The difference is what surrounds the conversion. Turndown is a JavaScript library built around a rule and plugin model, and it runs in the browser as well as Node, which is why it became the default in editor and clipboard tooling. mdream is built the other way around: a small core with fixed presets, a native engine for throughput, a streaming interface, and a set of packages that turn conversion into site-wide artifact generation. If you have existing Turndown rules that encode how your CMS's markup should be handled, mdream does not give you an obvious place to put them, and the README's hook story is attached to `@mdream/js` specifically. If you have no such rules and you want a pipe, mdream's shape is closer to what you are doing. Neither is more correct. The choice is whether you want a programmable converter or a configurable one.
Licence and the cost of keeping up
mdream is MIT licensed, which permits commercial use and modification with the usual requirement to carry the licence notice. That is a permissive starting point and nothing in the material suggests a dual-licence or open-core split, though a project with a sponsor program and a hosted homepage at mdream.dev is worth checking against its own repository before you assume the whole surface is MIT. On maintenance: the release history shown runs v1.7.0 in August 2026, v1.7.1 in early September, and v1.7.2 a few days later, with the last push on the default branch on the same day as the newest release. That is a fast patch cadence, which cuts both ways. You get fixes quickly, and you also get churn in a dependency that sits in your ingestion path. Pinning a version and reading the release notes before bumping is the cheap insurance. The larger cost is the multi-package structure: a project that ships a Rust crate, a NAPI binding, a WASM build, a JS package, a crawler, a Vite plugin and a GitHub Action has more surfaces to keep in sync than a single-library converter, and upgrading means checking which of those you actually depend on.
Editorial conclusion
Adopt mdream if you are feeding web pages or static HTML into a model and want the conversion step to be a thin, dependency-free pipe rather than a browser-grade DOM library. Skip it if you need CSS selector based extraction, or if you are already committed to Turndown plugins, since mdream's hook surface and preset model do not map onto that ecosystem. Before committing, run the same page through `--preset minimal` and the default preset, compare the two outputs by hand, and check whether the Rust NAPI binary resolves on your target platform or whether you need the pure JS package instead.
Community notes