remark
remark is a programmable Markdown processor built on unified. The main package gives you a ready-made pipeline with the CommonMark parser and Markdown serializer already attached, so it can parse text into the mdast syntax tree, run plugins that inspect or change that tree, and write Markdown back out. Add-ons provide GitHub Flavored Markdown, frontmatter, linting, tables of contents, HTML conversion, and many other jobs. It is best understood as an AST toolchain for applications and content workflows, not as a one-call Markdown-to-HTML renderer.
Choose remark when Markdown is structured data that your program must inspect, enforce, or rewrite. For plain Markdown-to-HTML rendering, use the smaller path its own maintainers recommend and start with micromark.
Use it if
- You need to inspect or rewrite Markdown structurally instead of relying on regular expressions
- You want a plugin pipeline for linting, formatting, tables of contents, frontmatter, or custom content rules
- Your input and output are both Markdown and you want parsing plus serialization preconfigured
- You need CommonMark by default with optional GFM, MDX, or other syntax supplied explicitly by plugins
- You only need Markdown rendered to HTML: the README recommends micromark for that narrower job, while remark requires a remark-rehype and rehype-stringify pipeline
- Your project still uses CommonJS: remark 15 declares type module and exposes an ES module entry point, so require('remark') is not the supported loading model
- You expect GFM tables, task lists, autolinks, strikethrough, or YAML frontmatter with no setup: the README says CommonMark is the default and those extensions require remark-gfm or remark-frontmatter
- You process hostile Markdown but cannot isolate or cap it: the security section warns that deeply repeated constructs can cause crashes or slowdowns and recommends input limits plus a worker that can be stopped
- You do not need plugins and plan to manipulate syntax trees directly: the maintainers point to mdast-util-from-markdown and mdast-util-to-markdown as the smaller, more direct fit
Setup reality
Installation is only npm install remark, and TypeScript declarations ship in the package, but the useful setup nearly always includes more packages. Version 15 is ESM-only, so use import {remark} from 'remark' from an ES module; a CommonJS require call is the first common failure. The package already includes remark-parse and remark-stringify, which is convenient only when both sides are Markdown. HTML output needs remark-rehype and rehype-stringify, and untrusted HTML needs rehype-sanitize in the correct place before stringification. GitHub Flavored Markdown and frontmatter are not built in; install remark-gfm and remark-frontmatter separately. Tree traversal generally adds unist-util-visit. Linting adds remark-lint rules or presets and vfile-reporter, while the command line is a separate remark-cli package. Plugins can be synchronous or asynchronous, so process() returns a promise and should normally be awaited. parse() is synchronous and only builds the tree; stringify() only serializes a tree; run() or runSync() is the step that applies transforms when you separate stages manually. Plugin order matters because syntax extensions must be registered before parsing and sanitization must happen after converting mdast to hast but before HTML serialization. The README also warns that third-party plugins have independent quality and security profiles. For user-controlled input, cap document size, avoid expensive unchecked plugins, and move processing to a worker if you need a hard timeout. Maintained unified releases follow maintained Node versions; the remark 15 documentation specifically targets Node 16, and a future major can drop an end-of-life runtime.
Patterns
Parse and format Markdownformat-markdown
import {remark} from 'remark'
const file = await remark().process('# Hello, *Mars*!')
console.log(String(file))process() parses, runs plugins, and serializes. It is asynchronous even when every configured transform is synchronous.
Parse Markdown into mdastparse-syntax-tree
import {remark} from 'remark'
const tree = remark().parse('## Hello *Pluto*!')
console.log(tree.type, tree.children[0].type)parse() is synchronous and does not run transformer plugins. Call run() afterward if the tree must pass through transforms.
Serialize an mdast treeserialize-syntax-tree
import {remark} from 'remark'
const tree = {
type: 'root',
children: [
{type: 'heading', depth: 2, children: [{type: 'text', value: 'Status'}]}
]
}
console.log(remark().stringify(tree))The object must follow mdast node shapes. TypeScript users can import Root and other node types from mdast.
Choose Markdown serialization styleconfigure-output-style
import {remark} from 'remark'
const output = await remark()
.use({bullet: '-', emphasis: '_', fences: true})
.process('* one\n* two\n\n code')
console.log(String(output))Parser and compiler settings are passed through use(). Formatting normalizes the whole document, so review large rewrites before applying them in bulk.
Enable GitHub Flavored Markdownenable-gfm
import {remark} from 'remark'
import remarkGfm from 'remark-gfm'
const output = await remark()
.use(remarkGfm)
.process('| a | b |\n| - | - |\n| 1 | 2 |')
console.log(String(output))Install remark-gfm separately. Tables, task lists, autolinks, and strikethrough are not part of remark's default CommonMark syntax.
Recognize YAML frontmatterparse-frontmatter
import {remark} from 'remark'
import remarkFrontmatter from 'remark-frontmatter'
const tree = remark()
.use(remarkFrontmatter, ['yaml'])
.parse('---\ntitle: Mars\n---\n\n# Page')
console.log(tree.children[0])remark-frontmatter creates a yaml node but does not parse its value into an object. Add a YAML parser if you need fields.
Render Markdown to sanitized HTMLrender-safe-html
import {remark} from 'remark'
import remarkRehype from 'remark-rehype'
import rehypeSanitize from 'rehype-sanitize'
import rehypeStringify from 'rehype-stringify'
const file = await remark()
.use(remarkRehype)
.use(rehypeSanitize)
.use(rehypeStringify)
.process(userMarkdown)
console.log(String(file))All three add-ons are separate packages. Keep rehype-sanitize between the mdast-to-hast bridge and HTML stringification for untrusted content.
Write a plugin that changes headingswrite-transform-plugin
import {remark} from 'remark'
import {visit} from 'unist-util-visit'
function shiftHeadings() {
return (tree) => {
visit(tree, 'heading', (node) => {
node.depth = Math.min(6, node.depth + 1)
})
}
}
const file = await remark().use(shiftHeadings).process('# Title')A plugin is registered by passing the function, not by calling it. unist-util-visit is an additional dependency.
Collect links from Markdowncollect-links
import {remark} from 'remark'
import {visit} from 'unist-util-visit'
const tree = remark().parse(markdown)
const links = []
visit(tree, 'link', (node) => {
links.push({url: node.url, title: node.title})
})
console.log(links)This only visits inline link nodes. Definitions and image links use different mdast node types and need separate visits.
Lint Markdown with maintained presetslint-markdown
import {remark} from 'remark'
import consistent from 'remark-preset-lint-consistent'
import recommended from 'remark-preset-lint-recommended'
import {reporter} from 'vfile-reporter'
const file = await remark()
.use(consistent)
.use(recommended)
.process(markdown)
console.error(reporter(file))The presets and vfile-reporter are separate installs. Lint messages live on file.messages; process() does not throw merely because warnings exist.
Run an asynchronous pluginrun-async-transform
import {remark} from 'remark'
function loadTitle() {
return async (tree, file) => {
const title = await lookupTitle(file.path)
tree.children.unshift({
type: 'heading',
depth: 1,
children: [{type: 'text', value: title}]
})
}
}
const file = await remark().use(loadTitle).process({path: 'post.md', value: body})Use process() or run() for asynchronous transformers. runSync() throws when any plugin returns a promise.
Format project Markdown from npm scriptsformat-with-cli
npm install --save-dev remark-cli remark-toc
# package.json
# {"scripts": {"format:md": "remark . --output --use remark-toc"}}
npm run format:mdremark does not include the executable; install remark-cli separately. The --output flag rewrites matching files, so run it on a clean worktree first.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| micromark | npm | You primarily need standards-focused Markdown-to-HTML conversion and do not need a plugin-driven mdast workflow |
| markdown-it | npm | You want a direct HTML renderer with a familiar renderer-rule plugin API and broad CommonJS history |
| marked | npm | You want a fast, direct parser and renderer with less AST pipeline ceremony |
| unified | npm | You are assembling a mixed Markdown, HTML, or text pipeline and want to choose the parser and compiler yourself |