Markdown Pipelines: remark vs markdown-it

For a documentation site, the Markdown pipeline is the most important piece of software after the generator itself. It decides what writers can express — callouts, tabs, footnotes, heading anchors, definition lists — how links are resolved, how code is highlighted and how fast thousands of pages render. Astro and Eleventy take different default routes. Astro uses the unified ecosystem: remark parses Markdown into a syntax tree, remark plugins transform it, it becomes an HTML tree that rehype plugins transform, and then it is serialized. Eleventy uses markdown-it: a fast tokenizer with render rules, extended by plugins that add or override token types.

This guide compares the two on the tasks documentation teams actually automate — heading anchors, custom containers, link rewriting, external link attributes, and table of contents extraction — and measures their speed on the same content. It is part of Astro vs Eleventy for Documentation Sites. The corpus is a 1,300-page documentation site with 1.1 million words and 9,100 code blocks, rendered by both generators.

Prerequisites

  • Familiarity with how each generator configures Markdown: markdown.remarkPlugins and markdown.rehypePlugins in Astro, amendLibrary('md', …) or setLibrary in Eleventy.
  • Node 20+ for the benchmark.
  • A list of the Markdown features your writers use — a quick grep of the content for :::, {%, <details> and HTML tags is a good start.

Two Models of Markdown Processing

The architectural difference explains most practical differences.

remark and rehype build complete trees. Every plugin receives the whole document as a tree (mdast for Markdown, hast for HTML) and can inspect or modify any node with a visitor. This makes structural transformations natural: "find every link whose URL starts with /docs/ and rewrite it", "collect every h2 and h3 into a table of contents", "wrap every table in a scrollable container". The cost is memory and time: each tree is built, walked by every plugin, and converted.

markdown-it produces a flat stream of tokens (heading_open, inline, heading_close, …) and renders each through a rule. Plugins add parsing rules for new syntax or replace render rules. Adding syntax — a :::warning container, ==highlight== marks, {#custom-id} attributes — is where markdown-it shines. Structural transformations are possible but clumsier, because the "tree" is implicit in open and close token pairs.

remark and rehype trees compared with markdown-it token streams Two panels. The unified pipeline in Astro parses Markdown to an mdast tree, runs remark plugins, converts to a hast HTML tree, runs rehype plugins and serializes; it suits structural transforms. markdown-it in Eleventy tokenizes to a flat token stream, runs core and inline rules from plugins, and renders tokens through render rules; it suits new syntax and speed. Two processing models remark + rehype (Astro) Markdown to mdast tree remark plugins visit nodes mdast to hast HTML tree rehype plugins visit elements serialize to HTML easy structural transforms markdown-it (Eleventy) Markdown to flat token stream plugins add parse rules render rules per token type direct string output easy new syntax and fast
Neither model is better in general; each makes a different class of plugin easy.

Task 1: Heading Anchors

Every documentation page needs stable IDs on headings so sections can be linked. Both ecosystems have mature plugins.

// Astro: astro.config.mjs
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
export default defineConfig({
  markdown: {
    rehypePlugins: [rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'append', properties: { ariaHidden: true, tabIndex: -1, className: ['anchor'] } }]]
  }
});
// Eleventy: eleventy.config.js
import markdownItAnchor from 'markdown-it-anchor';
eleventyConfig.amendLibrary('md', (md) => md.use(markdownItAnchor, {
  slugify: eleventyConfig.getFilter('slugify'),
  permalink: markdownItAnchor.permalink.linkAfterHeader({ style: 'visually-hidden', assistiveText: (t) => `Permalink to “${t}”` })
}));

The detail that matters in migrations: the two slug algorithms differ. github-slugger (used by rehype-slug and Astro's own heading IDs) and Eleventy's slugify filter produce different IDs for headings with punctuation, emoji or repeated text. When the documentation site's prototype moved from Eleventy to Astro, 212 of 14,800 heading IDs changed, which would have broken every external link to those sections. Pinning one slug function across both, and testing anchors with a link checker as in Checking Links in Pull Requests, prevented it.

Task 2: Callouts and Custom Containers

Writers want callouts — note, warning, tip — without writing HTML. In markdown-it, markdown-it-container adds ::: block syntax:

import container from 'markdown-it-container';
for (const kind of ['note', 'tip', 'warning']) {
  md.use(container, kind, {
    render: (tokens, i) => tokens[i].nesting === 1
      ? `<aside class="callout callout-${kind}" role="note"><p class="callout-title">${kind[0].toUpperCase() + kind.slice(1)}</p>\n`
      : '</aside>\n'
  });
}

In Astro, remark-directive parses the same :::note syntax into directive nodes, and a small remark plugin turns them into elements:

import { visit } from 'unist-util-visit';
export function remarkCallouts() {
  return (tree) => visit(tree, 'containerDirective', (node) => {
    if (!['note', 'tip', 'warning'].includes(node.name)) return;
    node.data = { hName: 'aside', hProperties: { className: ['callout', `callout-${node.name}`], role: 'note' } };
  });
}

Both are about fifteen lines. The markdown-it version writes HTML strings; the remark version sets properties on a tree node, which composes better with other plugins (a later rehype plugin can still see and modify the aside). For MDX content, Astro and Starlight offer components instead — see Shortcodes vs Components for Docs Authors.

Documentation content links to other pages by source path (../install.md) or by URL. Rewriting .md links to final URLs, adding rel="noopener" and an external marker to off-site links, and flagging links to draft pages are all structural transforms — easiest in rehype:

export function rehypeLinks({ site }) {
  return (tree, file) => visit(tree, 'element', (node) => {
    if (node.tagName !== 'a' || !node.properties.href) return;
    const href = String(node.properties.href);
    if (/^https?:\/\//.test(href) && !href.startsWith(site)) {
      node.properties.rel = ['noopener'];
      node.properties.className = [...(node.properties.className ?? []), 'external'];
    } else if (href.endsWith('.md') || href.includes('.md#')) {
      node.properties.href = href.replace(/(index)?\.md(#|$)/, '/$2').replace(/\/\/$/, '/');
    }
  });
}

The markdown-it equivalent overrides the link_open render rule and edits the token's attributes. It works, but reading the link text or the surrounding context from a render rule is awkward, and ordering between several link-related plugins is fragile.

How common documentation Markdown tasks map to each pipeline A matrix of five tasks. Heading anchors are easy in both with rehype-slug or markdown-it-anchor, but the slug algorithms differ. Custom containers are easy in both with remark-directive or markdown-it-container. Link rewriting is easy in rehype and awkward in markdown-it render rules. Table of contents extraction is easy in both. New inline syntax is harder in remark and easy in markdown-it. Common docs tasks in each pipeline Task remark + rehype markdown-it Heading anchors rehype-slug markdown-it-anchor Callout containers remark-directive markdown-it-container Rewriting links tree visitor render rule override Table of contents collect from hast markdown-it-toc-done-right New inline syntax micromark extension inline rule
For documentation sites the deciding tasks are usually link rewriting, which favours rehype, and custom syntax, which favours markdown-it.

Task 4: Table of Contents

Both generators expose headings for a page-level table of contents: Astro returns a headings array from render() for content collection entries, including depth, slug and text; in Eleventy, markdown-it-toc-done-right renders a table of contents where a marker appears, or a small filter can extract headings from rendered HTML. Astro's built-in array is the more convenient of the two, because layouts can render the table of contents anywhere — a sticky sidebar, for instance — without a marker in the content.

Speed on the Same Content

Rendering the 1,300-page corpus with each pipeline, isolated from the rest of the build (no templates, no highlighting), with a typical plugin set for each (anchors, containers, link rewriting, footnotes, tables):

PipelineRender time (1,300 pages)Peak memory
markdown-it + 5 plugins1.4 s210 MB
remark + rehype + 6 plugins5.9 s640 MB
remark + rehype, with Shiki17.2 s910 MB
markdown-it, with Shiki12.6 s480 MB

Measured with hyperfine over a Node script that renders every file, median of five runs. The raw parser difference is large, but once code highlighting is included — as it is on any documentation site — highlighting dominates and the gap narrows to about 35%. Caching highlighted blocks, as in Syntax Highlighting with Shiki in Astro and Eleventy, matters more than the choice of parser.

Render time for markdown-it and remark with and without Shiki Paired bars comparing markdown-it and remark plus rehype. Without highlighting, markdown-it rendered the corpus in 1.4 seconds and remark plus rehype in 5.9 seconds. With Shiki highlighting, markdown-it took 12.6 seconds and remark plus rehype 17.2 seconds. Rendering 1,300 pages markdown-it remark + rehype Without highlighting 1.4 s 5.9 s With Shiki 12.6 s 17.2 s
Median of five hyperfine runs on an 8-vCPU runner, Markdown rendering only.

Switching Pipelines Inside a Generator

Neither generator locks you in. Eleventy's setLibrary('md', …) accepts any object with a render method, so a unified pipeline can replace markdown-it if the team wants rehype's link handling or to share plugins with an Astro site. Astro can render Markdown with markdown-it through a custom loader in the Content Layer, which some teams do for speed on very large collections. In both cases the price is losing the ecosystem the generator's documentation and integrations assume. On the documentation site, the team kept each generator's default and ported only three plugins, which was the cheaper path.

Measured Impact

Measuremarkdown-it (Eleventy)remark + rehype (Astro)
Custom plugin code for docs features96 lines118 lines
Heading IDs that differed between generators—212 of 14,800 (fixed by pinning slugger)
Render time, full corpus with Shiki12.6 s17.2 s
Link-rewriting bugs found in first month40
Writer-facing syntax differences:::note:::note (same via directives)

Pitfalls & Rollback

  • Different slug algorithms. Heading IDs change silently when switching pipelines; pin one and test anchors.
  • Plugin order. rehype plugins run in array order; autolink plugins must run after slug plugins.
  • HTML in Markdown. markdown-it's html: true and remark's allowDangerousHtml behave differently with raw HTML blocks; test pages that embed HTML.
  • Memory on huge sites. Full trees for tens of thousands of pages can exceed default Node heap on CI; raise --max-old-space-size or process in batches.
  • Rollback: pipelines are configuration; plugin changes can be reverted individually, and snapshot tests of built output show exactly what a plugin changed.

Conclusion

remark and rehype make structural transformations — link rewriting, table of contents, attribute changes — clean and composable. markdown-it makes new syntax easy and renders several times faster. For documentation sites the difference in writer experience is small once callouts and anchors are configured, and code highlighting dominates build time either way. Choose the generator for other reasons, keep its default pipeline, and pin the details — slug algorithm and plugin order — that change URLs.

FAQ

Which Markdown parser does each generator use?

Astro uses remark to parse Markdown into an mdast syntax tree and rehype to transform the resulting HTML tree, the unified ecosystem. Eleventy uses markdown-it by default, a token-stream parser with render rules. Hugo uses Goldmark, written in Go.

Is markdown-it faster than remark?

Yes, typically several times faster for plain Markdown, because it produces tokens and renders directly instead of building and transforming full syntax trees. On a 1,300-page corpus, markdown-it rendered in 1.4 seconds and remark plus rehype in 5.9 seconds. Plugins and highlighting usually dominate both.

Which is easier to extend?

remark and rehype plugins operate on well-defined syntax trees and are easier to write for transformations such as rewriting links or adding attributes. markdown-it plugins are better suited to adding new inline or block syntax, such as custom containers, but require understanding its tokenizer rules.

Can I use remark plugins in Eleventy?

Yes, by replacing the Markdown library with a unified pipeline through setLibrary, or by using a plugin that does so. The cost is build speed and losing compatibility with markdown-it plugins you already use.

Filed underComparisonsAstroEleventy