Library / SDK
mixmark-io/turndown avatar
mixmark-io/turndown

Turndown: an HTML to Markdown converter in JavaScript

🛏 An HTML to Markdown converter written in JavaScript

11,444 stars993 forksHTMLMIT

At a glance

What is it?
Turndown is an MIT-licensed JavaScript library that turns HTML into Markdown, in Node.js or the browser. Its rule system is what makes it flexible, and its default output style is what most often surprises new users.
Who is it for?
Adopt Turndown if you need to convert HTML into Markdown inside a JavaScript or Node.js pipeline and you want to control the output through rules rather than post-processing strings. Do not adopt it if you need round-trip fidelity between HTML and Markdown, or if you are converting documents whose structure depends on CSS, since Turndown reads the DOM and not the rendered page.
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 last received commits 27 days ago.
What is it written in?
Mainly HTML, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What Turndown converts, and for whom

Turndown takes HTML and returns Markdown. The README describes it as a way to "Convert HTML into Markdown with JavaScript", and the package.json description is the same idea in one line: "A library that converts HTML to Markdown". It runs in Node.js and in the browser, which matters because the two environments rarely share a converter. A Node service that ingests scraped pages, a static site generator that imports legacy HTML, and a browser tool that copies a selection as Markdown are all the same problem, and Turndown is one answer to all three.

The audience is JavaScript developers, not end users. There is no CLI in the README and no server component. You import the library, instantiate a service, and call a method. The repository lists a homepage with an online demo, so the fastest way to judge output quality is to paste a sample document into that demo before writing any code.

How the rule engine turns DOM nodes into Markdown

Turndown does not run a regex pass over an HTML string. It parses the input into a DOM and walks it, and the conversion behaviour comes from rules. The README states that Turndown accepts DOM nodes directly, including element nodes, document nodes and document fragment nodes, which is the clearest sign that the DOM is the working representation.

A rule is a plain object with two properties. The filter decides which nodes the rule applies to, and the replacement function returns the Markdown string for that node. Filters can be a tag name, an array of tag names, or a function that receives the node and the service options. The README gives the paragraph rule as an example, where the filter is the string p and the replacement returns the content surrounded by two newlines. Because the replacement receives the already-converted content of the children, rules compose upward rather than competing for the whole document.

Three special rules sit underneath the rest: blankReplacement, keepReplacement and defaultReplacement. They handle blank elements, elements marked for keeping, and elements no other rule matched. That last one is where unexpected output usually comes from, because an element nobody wrote a rule for still has to produce something.

In Node.js the DOM comes from the @mixmark-io/domino dependency, listed in package.json. In the browser, the package.json browser field maps the CJS and ES entry points to browser builds and disables domino, so the native DOM is used instead. That split is why the same code behaves slightly differently in the two environments, and why the published package ships separate UMD files for Node.js and for the browser.

Installing Turndown and converting your first document

The README gives npm as the installation route. The package requires Node.js 18 or newer and npm 9 or newer, according to the engines field in package.json.

bash
npm install turndown

In Node.js you require the package, construct a service, and call turndown with either an HTML string or a DOM node. The README's example converts a single heading:

js
// For Node.js
var TurndownService = require('turndown')

var turndownService = new TurndownService()
var markdown = turndownService.turndown('<h1>Hello world!</h1>')

With default options, headingStyle is setext, so that h1 comes back as a line of text underlined with equals signs rather than as a leading hash. The README shows how options are passed at construction time:

js
var turndownService = new TurndownService({ option: 'value' })

For a first real use, run the converter over an existing fragment and inspect the output before wiring it into anything. The README's DOM example passes an element from the document, which is the pattern you would use in a browser tool that converts a content region:

js
var markdown = turndownService.turndown(document.getElementById('content'))

In the browser without a bundler, the README points at a script tag loading the IIFE build from unpkg. For module bundlers, the package exposes lib/turndown.cjs.js, lib/turndown.es.js and UMD builds under lib/. To generate those files from source, the README says to clone the repository and run npm run build, which chains the cjs, es, umd and iife Rollup configurations in config/.

Where Turndown's defaults and design will bite you

The default option table is the first real limitation, because the defaults are not what most people mean by Markdown. headingStyle is setext, bulletListMarker is *, codeBlockStyle is indented, emDelimiter is _, and linkStyle is inlined. A team that expects ATX headings and fenced code blocks with backticks will look at the first output and assume something is broken. It is not; it is the documented default, and every one of those is an option you have to set.

The second limitation is the conversion model itself. Turndown reads the DOM, so anything that exists only as CSS does not survive. A div used as a visual heading, a table laid out with floats, an element whose meaning comes from a class name: none of that is visible to a rule that filters on tag names. You can write a filter function that inspects attributes, and the README shows a function filter that checks nodeName and an href attribute, but you are then encoding your own document conventions into the converter.

The third is that Markdown cannot represent everything HTML can. The keep method exists precisely for this: it renders selected elements as raw HTML instead of converting them. The README's example keeps del and ins elements, so they pass through as HTML tags. That is a deliberate escape hatch, and it means the output is Markdown with HTML islands, which not every downstream renderer will accept.

Finally, the ordering rules are strict and easy to get wrong. Keep filters are overridden by the standard CommonMark rules and by any added rules. Remove filters are overridden by keep filters, by the standard rules, and by added rules. If you call remove on an element that a built-in rule already handles, nothing happens, and the README says so directly: to change those, add a rule instead.

Extending Turndown with rules and the GFM plugin

The extension surface is the reason to choose Turndown over a one-shot converter. addRule takes a key and a rule object and returns the service, so calls chain. The README's strikethrough example filters del, s and strike and wraps the content in tildes. Writing that rule yourself is a few lines, and it is the same shape as every other rule in the library.

Plugins package rules for reuse. The README points at turndown-plugin-gfm and shows importing gfm, tables and strikethrough from it, then calling use with either the whole plugin or an array of individual ones. Tables are the practical case: Markdown tables are not part of the base conversion, so without the tables plugin a table will be flattened into something that no longer reads as a table.

The service also exposes keep and remove for the two blunt cases, and both accept the same filter forms as rules. keep takes precedence over remove, so a node matched by both is kept. Both methods can be called more than once, with later calls taking precedence over earlier ones. That is a usable configuration model, but it is also a source of confusion: precedence runs keep, then CommonMark rules, then added rules, and remove sits below all of them.

Alternatives and how their approach differs

The README itself names the closest alternative, in a sense: Turndown was formerly called to-markdown, and the project publishes a migration guide for the rename. That is history rather than an alternative, but it is worth knowing if you find older code importing to-markdown.

For a genuine comparison, look at what Turndown is not. It is a DOM-walking converter with a rule registry. Converters built on a streaming tokenizer instead of a DOM take HTML as a token stream and emit Markdown as they go, which avoids building a full tree and avoids depending on a DOM implementation. That matters in constrained runtimes where a DOM shim is unwelcome, and it is the main architectural difference to weigh. Turndown's dependency on @mixmark-io/domino in Node.js is the concrete cost of the DOM approach, and the browser field in package.json exists to remove that dependency where a native DOM is already present.

The other difference is output control. A rule registry lets you decide, per element, what Markdown comes out, and lets you keep elements as HTML. A converter with a fixed mapping gives you less to configure and less to get wrong. If you want the output to look like a specific house style, Turndown's options and rules are the mechanism. If you want the converter to make no decisions, they are overhead.

Maintenance, licence and the cost of upgrading

The repository is not archived, and the last push was on 2026-09-03. The most recent release listed is v7.2.4 on 2026-04-03, preceded by v7.2.3 the same day and v7.2.2 on 2025-10-24. The version in package.json is 7.2.4, matching the latest release. That is a stable 7.x line with infrequent releases rather than a fast-moving one, which is what you want from a converter whose output other tools depend on.

The licence is MIT, declared in package.json and present as a LICENSE file at the repository root. For most consumers that means the usual MIT obligations around including the licence text, but the specifics of your situation are for your own legal review, not something a README can settle.

Upgrade cost is low on the surface and higher underneath. The public API is small: a constructor, turndown, addRule, keep, remove and use. The risk sits in the option defaults and in the rules you wrote yourself. A change to a default, or to how a built-in rule handles an element, changes your output even though your code did not change. The practical guard is the test directory in the repository and your own fixtures: convert a fixed set of HTML samples before and after an upgrade and diff the Markdown. The build also matters if you vendor the library, since the README says the UMD files are generated at publish time and can be regenerated with npm run build.

Editorial conclusion

Adopt Turndown if you need to convert HTML into Markdown inside a JavaScript or Node.js pipeline and you want to control the output through rules rather than post-processing strings. Do not adopt it if you need round-trip fidelity between HTML and Markdown, or if you are converting documents whose structure depends on CSS, since Turndown reads the DOM and not the rendered page. Before committing, verify the option defaults against your own corpus: headingStyle defaults to setext, bulletListMarker to *, codeBlockStyle to indented and emDelimiter to _, so a first conversion will not look like the Markdown most teams write by hand.

Frequently asked questions

How do I install Turndown?

Install it from npm with npm install turndown. The package requires Node.js 18 or newer and npm 9 or newer according to its engines field. In the browser without a bundler, the README shows loading the IIFE build from unpkg with a script tag.

Does Turndown convert tables to Markdown?

Not by default. The README points at turndown-plugin-gfm and shows importing tables from it and registering it with turndownService.use, either alone or together with the strikethrough plugin.

Why does Turndown output setext headings and indented code blocks?

Those are the documented defaults: headingStyle is setext and codeBlockStyle is indented. Pass options to the constructor, for example headingStyle: 'atx' or codeBlockStyle: 'fenced', to get the other form.

Official sources

  1. License: MIT
  2. mixmark-io/turndown on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/mixmark-io-turndown.svg)](https://hysenlabs.com/projects/mixmark-io-turndown)