turndown
Turndown is a JavaScript converter that walks HTML and emits Markdown. It accepts an HTML string or an existing Element, Document, or DocumentFragment, applies built-in CommonMark-oriented rules, and lets you tune heading, list, fence, emphasis, and link styles. Custom rules can replace selected elements, keep them as raw HTML, or remove them with their contents. GitHub-flavored tables, strikethrough, and task-list behavior live in the separate turndown-plugin-gfm package. It is a one-way structural conversion utility, not an HTML sanitizer, browser scraper, Markdown renderer, or round-trip document format.
Turndown is still the straightforward choice for controllable, mostly conventional HTML-to-Markdown conversion. Do not mistake it for a sanitizer or a fidelity-preserving document converter, and budget for the GFM and TypeScript companion packages when needed.
Use it if
- You need predictable HTML-to-Markdown conversion in Node or a browser
- You want to add element-specific conversion rules without building a full syntax-tree pipeline
- You need to preserve selected unknown elements as inline HTML while converting the rest
- You want configurable CommonMark-style output and can add the GFM plugin for tables or strikethrough
- You need exact round trips: unsupported elements fall back to text, whitespace is normalized, and Markdown cannot represent every HTML attribute or layout
- You expect tables or strikethrough in the core package: the README puts those features in the separate turndown-plugin-gfm package
- You need sanitization of hostile HTML: the API converts whatever DOM it receives and does not document a security-cleaning policy
- You need first-party TypeScript declarations: npm metadata for 7.2.4 does not declare bundled types, so TypeScript projects commonly add @types/turndown
- You cannot tolerate aggressive escapes: the README says regex-based escaping can be aggressive and documents prototype override as the escape hatch
Setup reality
Version 7.2.4 requires Node 18 or newer and has one declared runtime dependency, @mixmark-io/domino, which supplies DOM behavior for HTML strings in Node. In a browser you can pass a live DOM node or use the distributed browser build, but converting a string and converting the browser's parsed DOM can differ around malformed markup because an HTML parser repairs it first. The default output is not the flavor many repositories expect: headings use setext style, bullet lists use *, code blocks are indented, and links are inline. Configure atx headings and fenced code explicitly if that is your house style. Core Turndown does not include GitHub-flavored tables, strikethrough, or task-list handling; install turndown-plugin-gfm and call use(gfm). The package does not advertise bundled TypeScript definitions, so strict TypeScript projects need @types/turndown or a local declaration. Treat conversion as lossy. Classes, ids, data attributes, layout containers, and unrecognized markup usually disappear while text survives. keep preserves chosen elements as raw HTML, while remove deletes both the element and its contents. Rule precedence is easy to misread: blank nodes win first, then added rules, built-in CommonMark rules, keep rules, remove rules, and the default fallback. That means keep('a') does not override the standard link rule; add an explicit rule if you need that. Input HTML is not made safe just because the output is Markdown. Sanitize untrusted HTML before conversion, and sanitize or constrain any later Markdown-to-HTML rendering too. Finally, snapshot-test real documents. Whitespace collapsing, nested lists, mixed inline markup, malformed tables, and the documented aggressive escaping are where apparently simple migrations change content.
Patterns
Convert an HTML stringconvert-html-string
const TurndownService = require('turndown');
const service = new TurndownService();
const markdown = service.turndown('<h1>Hello</h1><p>Welcome.</p>');The conversion is structural and lossy. HTML attributes and layout containers generally do not survive unless a rule preserves them.
Convert an existing browser nodeconvert-dom-node
import TurndownService from 'turndown';
const article = document.querySelector('article');
const markdown = new TurndownService().turndown(article);The input may be an Element, Document, or DocumentFragment. Check for a null query result before calling turndown.
Emit repository-friendly Markdownconfigure-output-style
const service = new TurndownService({
headingStyle: 'atx',
bulletListMarker: '-',
codeBlockStyle: 'fenced',
fence: '~~~',
emDelimiter: '*',
});
const markdown = service.turndown(html);Defaults use setext headings, * bullets, indented code blocks, and _ emphasis, so set team conventions explicitly.
Add GitHub-flavored Markdown rulesenable-gfm
const TurndownService = require('turndown');
const { gfm } = require('turndown-plugin-gfm');
const service = new TurndownService();
service.use(gfm);
const markdown = service.turndown(html);turndown-plugin-gfm is a separate install. Core Turndown does not convert GFM tables and strikethrough by itself.
Convert highlighted text with a custom ruleadd-custom-rule
service.addRule('highlight', {
filter: 'mark',
replacement(content) {
return '==' + content + '==';
},
});Added rules take precedence over built-in CommonMark, keep, remove, and default rules, but the special blank rule still wins.
Match elements using attributesfilter-by-attribute
service.addRule('mention', {
filter(node) {
return node.nodeName === 'A' && node.hasAttribute('data-user');
},
replacement(content, node) {
return '@' + node.getAttribute('data-user');
},
});Function filters receive DOM nodes. Validate attribute content if generated Markdown is consumed by another trusted system.
Preserve selected tags as HTMLkeep-raw-html
service.keep(['sub', 'sup']);
const markdown = service.turndown('<p>H<sub>2</sub>O and x<sup>2</sup></p>');Built-in CommonMark and added rules outrank keep filters. Keeping a normally handled tag requires an explicit added rule.
Drop unwanted elements and contentsremove-elements
service.remove(['script', 'style', 'nav']);
const markdown = service.turndown(html);remove deletes the element and all of its contents. It is not a substitute for HTML sanitization because higher-priority rules and later rendering still matter.
Emit referenced linksuse-reference-links
const service = new TurndownService({
linkStyle: 'referenced',
linkReferenceStyle: 'collapsed',
});
const markdown = service.turndown(html);Reference definitions are appended to the output. Snapshot repeated and nested links because reference labels depend on conversion order.
Mark unrecognized blocks explicitlycustomize-unknown-elements
const service = new TurndownService({
defaultReplacement(content, node) {
const block = /^(DIV|SECTION|ASIDE)$/.test(node.nodeName);
return block ? '\n\n' + content + '\n\n' : content;
},
});The default rule only runs after added, built-in, keep, and remove rules. It is useful for diagnostics but cannot restore attributes already discarded.
Package house rules as a plugincompose-plugin
function editorialRules(service) {
service.remove(['script', 'style']);
service.addRule('line-break', {
filter: 'br',
replacement: () => ' \n',
});
}
service.use(editorialRules);A plugin is a function called with the service. Pass an array to use when ordering several plugin bundles matters.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| node-html-markdown | npm | You want a newer converter focused on speed, TypeScript support, and batch Node workloads |
| rehype-remark | npm | You already use unified syntax trees and need plugins to inspect or transform structure between HTML and Markdown |
| html-to-md | npm | You want a smaller direct converter and its narrower option set matches your documents |