Skip to main content
doc0blade47/fumadocsdocumentation home
Sign in

Markdown AST Plugins

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

Fumadocs processes documentation through an extensible pipeline of remark and rehype plugins that transform abstract syntax trees to support features like admonitions, code tabs, and table of contents generation. These plugins integrate directly with MDX Loader Pipeline, Source Loader, and Content Collections Adapter to standardize markdown compilation across build tools and runtimes.

Sources: packages/core/src/mdx-plugins/index.ts:1-16, packages/core/src/content/mdx/preset-bundler.ts:23-92, packages/content-collections/src/configuration.ts:58-124

Admonition parsing and directive transformations

Directive-Based Admonition Containers

The remarkDirectiveAdmonition plugin processes containerDirective nodes within the MDX Loader Pipeline to support standard callout syntaxes, particularly for migrations from Docusaurus. It inspects tree nodes using unist-util-visit, checking whether a node name matches any entry in the configured types mapping. When a match is found, the plugin transforms the AST node into an mdxJsxFlowElement representing a callout container.

export function remarkDirectiveAdmonition({
  tags: {
    CalloutContainer = 'CalloutContainer',
    CalloutTitle = 'CalloutTitle',
    CalloutDescription = 'CalloutDescription',
  } = {},
  types = {
    note: 'info',
    tip: 'info',
    info: 'info',
    warn: 'warning',
    warning: 'warning',
    danger: 'error',
    success: 'success',
  },
}: RemarkDirectiveAdmonitionOptions = {}): Transformer<Root, Root> {

Sources: packages/core/src/mdx-plugins/remark-directive-admonition.ts:29-44

The mapping between input directive names and target callout types defaults to specific standard identifiers.

Directive NameTarget Callout Type
noteinfo
tipinfo
infoinfo
warnwarning
warningwarning
dangererror
successsuccess

Sources: packages/core/src/mdx-plugins/remark-directive-admonition.ts:35-43

During transformation, any directive attributes present on the container are extracted into mdxJsxAttribute objects, alongside the primary type attribute. Children of the container are partitioned: paragraphs flagged with directiveLabel are routed into a title element named via CalloutTitle, while all remaining content blocks form the children of a description element named via CalloutDescription.

const titleNodes: PhrasingContent[] = [];
const descriptionNodes: (BlockContent | DefinitionContent)[] = [];
 
for (const item of node.children) {
  if (item.type === 'paragraph' && item.data?.directiveLabel) {
    titleNodes.push(...item.children);
  } else {
    descriptionNodes.push(item);
  }
}
 
const children: MdxJsxFlowElement[] = [];
 
if (titleNodes.length > 0) {
  children.push({
    type: 'mdxJsxFlowElement',
    name: CalloutTitle,
    attributes: [],
    children: titleNodes as BlockContent[],
  });
}
 
if (descriptionNodes.length > 0) {
  children.push({
    type: 'mdxJsxFlowElement',
    name: CalloutDescription,
    attributes: [],
    children: descriptionNodes,
  });
}
 

Sources: packages/core/src/mdx-plugins/remark-directive-admonition.ts:65-95

Note

remarkDirectiveAdmonition requires remark-directive to be configured beforehand in the unified pipeline to parse container directive syntax correctly. Sources: packages/core/src/mdx-plugins/remark-directive-admonition.ts:26-28

Legacy Admonition Syntax Rewriting

The legacy remarkAdmonition plugin provides backward compatibility for custom admonition tags (defaulting to :::) by scanning paragraph nodes, flattening their contents, and searching for matching type keys. While deprecated in favor of directive-based parsing, it maps alternative type names and extracts bracketed title meta strings into JSX attributes for standard Callout elements.

export function remarkAdmonition(options: RemarkAdmonitionOptions = {}): Transformer<Root, Root> {
  const tag = options.tag ?? ':::';
  // compatible with Docusaurus
  const typeMap = options.typeMap ?? {
    info: 'info',
    warn: 'warn',
 
    note: 'info',
    tip: 'info',
    warning: 'warn',
    danger: 'error',
  };
 

Sources: packages/core/src/mdx-plugins/remark-admonition.ts:22-34

Warning

Using nested admonitions with the legacy remarkAdmonition parser sets an hasIntercept flag that recursively invokes replaceNodes on inner child slices. Sources: packages/core/src/mdx-plugins/remark-admonition.ts:40-41, packages/core/src/mdx-plugins/remark-admonition.ts:80|80

Heading slugs and table generation

Heading Slug Assignment and TOC Extraction

The remarkHeading plugin processes markdown trees using unist-util-visit to locate heading nodes and assign unique URL-safe identifiers via github-slugger. By default, it supports custom heading IDs through a regular expression that inspects the final text node of a heading for a bracketed slug pattern ([#slug]). When a custom ID is matched, the plugin assigns it to heading.data.hProperties.id and strips the trailing tag from the node value. If no custom ID is present, it flattens the node content and passes it to the configured slug function.

const regex = /\s*\[#(?<slug>[^]+?)]\s*$/;

Sources: packages/core/src/mdx-plugins/remark-heading.ts:16-16, packages/core/src/mdx-plugins/remark-heading.ts:63-95

When generateToc is enabled, the plugin constructs an array of TOCItemType entries storing the title text, hash URL, and heading depth, attaching the resulting collection directly to file.data.toc.

if (generateToc) {
  toc.push({
    title: flattened ?? flattenNode(heading),
    url: `#${props.id}`,
    depth: heading.depth,
  });
}

Sources: packages/core/src/mdx-plugins/remark-heading.ts:86-92, packages/core/src/mdx-plugins/remark-heading.ts:97-97

Note

remarkHeading initializes a fresh Slugger instance and resets it on each transformer execution to guarantee deterministic ID generation across builds. Sources: packages/core/src/mdx-plugins/remark-heading.ts:53-62

Structured Data Extraction

The remarkStructure plugin parses MDAST trees to build searchable structured data containing headings and content blocks. It scans nodes matching specified types, defaulting to paragraphs, blockquotes, table cells, headings, and leaf MDX flow elements.

export const remarkStructureDefaultOptions = {
  types: ['heading', 'paragraph', 'blockquote', 'tableCell', 'mdxJsxFlowElement'],
  mdxTypes(node) {
    return !node.children || node.children.length === 0;
  },
  exportAs: false,
} satisfies StructureOptions;

Sources: packages/core/src/mdx-plugins/remark-structure.ts:94-100

If a heading node lacks an hProperties.id, remarkStructure emits a warning and skips it, requiring remarkHeading to run beforehand in the pipeline.

if (typeof id !== 'string') {
  console.warn(
    '[remark-structure] hProperties.id is missing in heading node, it is required to generate heading data. You can add remark-heading prior to remark-structure to generate heading IDs.',
  );
  return 'skip';
}

Sources: packages/core/src/mdx-plugins/remark-structure.ts:173-178

Collected blocks associate their content with the most recently encountered heading ID, storing the structured output in file.data.structuredData or serializing it as an MDX export when exportAs is configured.

file.data.structuredData = data;
if (exportAs) {
  tree.children.unshift(
    toMdxExport(typeof exportAs === 'string' ? exportAs : 'structuredData', data),
  );
}

Sources: packages/core/src/mdx-plugins/remark-structure.ts:204-209

Rehype Table of Contents Generation

The rehypeToc plugin inspects HTML element trees for heading tags (h1 through h6) to build a table of contents directly from HAST nodes. It recognizes control tags appended to heading text to alter behavior: [!toc] suppresses the heading from the table of contents and strips the tag, while [toc] includes the heading but removes the tag from the rendered output.

const TocOnlyTag = '[toc]';
const NoTocTag = '[!toc]';
const HeadingTags = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']);

Sources: packages/core/src/mdx-plugins/rehype-toc.ts:51-53, packages/core/src/mdx-plugins/rehype-toc.ts:77-91

Configuration PropertyTypeDefault ValuePurpose
exportTocboolean | { as: 'data' } | { as: 'esm', name: string }trueDetermines whether the table of contents is exported as an ESM variable or stored in virtual file data.

Sources: packages/core/src/mdx-plugins/rehype-toc.ts:8-30, packages/core/src/mdx-plugins/rehype-toc.ts:55-61

When exportToc is configured as an ESM export (defaulting to the name toc), the plugin converts HAST heading nodes into ESTree JSX fragments using hast-util-to-estree.

if (exportToc.as === 'esm') {
  const esmItems: {
    title: JSXElement;
    url: string;
    depth: number;
    _step?: number;
  }[] = [];
  for (const item of items) {
    const root = toEstree(item.title, {
      elementAttributeNameCase: 'react',
      stylePropertyNameCase: 'dom',
    }).body[0];
 
    if (root.type === 'ExpressionStatement' && root.expression.type === 'JSXElement') {
      esmItems.push({
        ...item,
        title: root.expression,
      });
    }

Sources: packages/core/src/mdx-plugins/rehype-toc.ts:110-128

Warning

Headings without a valid string identifier in their properties are skipped by rehypeToc and omitted from the generated table of contents. Sources: packages/core/src/mdx-plugins/rehype-toc.ts:73-74

Sequential step list conversion

Step Detection and Parsing

The remarkSteps plugin transforms consecutive step headings and their accompanying content into structured MDX JSX flow elements. It accepts an options object to customize CSS class names for both the outer steps container and individual step items.

Configuration PropertyTypeDefault ValuePurpose
stepsstring'fd-steps'Class name assigned to the outer wrapper container div element.
stepstring'fd-step'Class name assigned to each individual step item container div element.

Sources: packages/core/src/mdx-plugins/remark-steps.ts:7-21, packages/core/src/mdx-plugins/remark-steps.ts:29-32

To identify step headings, the plugin evaluates heading text against a regular expression pattern matching numeric prefixes like 1. Hello World, or checks for the trailing tag [step] via handleTag. When a valid step pattern matches, the plugin strips the marker from the text node and injects a data-fd-step property containing the incremental step index onto the heading's data properties.

const StepRegex = /^(\d+)\.\s(.+)$/;
const StepTag = '[step]';

Sources: packages/core/src/mdx-plugins/remark-steps.ts:23-24, packages/core/src/mdx-plugins/remark-steps.ts:70-90, packages/core/src/mdx-plugins/remark-steps.ts:124-132

Warning

If a heading has a mismatched depth or fails step parsing while a sequence is actively collecting, the plugin immediately terminates the current step group via onEnd(). Sources: packages/core/src/mdx-plugins/remark-steps.ts:118-127

Code tabs and command generation

Package Manager Conversion and Tabs Generation

Fumadocs provides remark plugins to rewrite code blocks into multi-tab package manager interfaces. The core pipeline includes remarkNpm and remarkCodeTab, alongside legacy support from remarkInstall.

Plugin NameTarget Language / MarkerOutput Element
remarkNpmpackage-install, npmCodeBlockTabs / custom triggers
remarkInstallpackage-installTabs, Tab
remarkCodeTabCode blocks with tab attributeCodeBlockTabs or Tabs / Tab

Sources: packages/core/src/mdx-plugins/remark-npm.ts:60-73, packages/doc-gen/src/remark-install.ts:60, packages/core/src/mdx-plugins/remark-code-tab.ts:255-259

The remarkNpm plugin visits code blocks in the Markdown AST and detects nodes where the language is package-install or npm. For package-install blocks, if the text does not start with npm or npx, it automatically prepends npm install. It then maps over configured package managers to translate commands and generate code block tabs.

export function remarkNpm({
  persist = false,
  packageManagers = [
    { command: (cmd) => cmd, name: 'npm' },
    { command: (cmd) => convertLines(cmd, 'pnpm'), name: 'pnpm' },
    { command: (cmd) => convertLines(cmd, 'yarn'), name: 'yarn' },
    { command: (cmd) => convertLines(cmd, 'bun'), name: 'bun' },
  ],
}: RemarkNpmOptions = {}): Transformer<Root, Root> {

Sources: packages/core/src/mdx-plugins/remark-npm.ts:46-54

Note

remarkInstall from @fumadocs/doc-gen is deprecated in favor of remarkNpm from fumadocs-core/mdx-plugins, which serves as a drop-in replacement. Sources: packages/doc-gen/src/remark-install.ts:45

Custom Code Tabs and Attribute Parsing

The remarkCodeTab plugin processes adjacent code blocks decorated with meta attributes like tab and tab-group. It supports two layout handlers defined in Types: CodeBlockTabs and Tabs.

Option PropertyTypeDefault ValuePurpose
Tabs'CodeBlockTabs' | 'Tabs''CodeBlockTabs'Determines the component name used for rendering grouped tab containers.
parseMdxbooleanfalseEnables parsing of embedded MDX inside tab trigger values.

Sources: packages/core/src/mdx-plugins/remark-code-tab.ts:11-21, packages/core/src/mdx-plugins/remark-code-tab.ts:206

During visitation, remarkCodeTab uses parseCodeBlockAttributes to extract tab and tab-group metadata from code block strings, assigns them to node data properties, and strips them from the raw meta string so syntax highlighters do not receive unknown flags.

for (; end < node.children.length; end++) {
  const child = node.children[end];
  if (child.type !== 'code' || !child.meta) {
    close();
    continue;
  }
 
  const meta = parseCodeBlockAttributes(child.meta, ['tab', 'tab-group']);
  if (!meta.attributes.tab) {
    close();
    continue;
  }
 
  if (start === -1) start = end;
  child.meta = meta.rest;
  child.data ??= {};
  child.data.tab = meta.attributes.tab;
  if (meta.attributes['tab-group']) {
    child.data.tabGroup = meta.attributes['tab-group'];
  }
}

Sources: packages/core/src/mdx-plugins/remark-code-tab.ts:248-268

Warning

When processing Tabs in simple mode with parseMdx enabled, the plugin checks whether the existing node attributes contain an items property before attempting to parse tab names as MDX. Sources: packages/core/src/mdx-plugins/remark-code-tab.ts:220-224

Mermaid diagram code block conversion

Remark Transformer and AST Replacement

The remarkMdxMermaid plugin transforms fenced code blocks containing diagram specifications into dynamic MDX JSX flow elements. It exports an options interface that accepts a custom language identifier.

Option PropertyTypeDefault ValuePurpose
langstring'mermaid'Specifies the target code block language identifier to match during AST visitation.

Sources: packages/core/src/mdx-plugins/remark-mdx-mermaid.ts:21-26

The plugin returns a unified transformer function that traverses the markdown abstract syntax tree using unist-util-visit. It targets nodes of type code, validates their positional indices and parent references, and compares their language property against the configured target language.

export function remarkMdxMermaid(options: RemarkMdxMermaidOptions = {}): Transformer<Root, Root> {
  const { lang = 'mermaid' } = options;
 
  return (tree) => {
    visit(tree, 'code', (node, idx, parent) => {
      if (node.lang !== lang || !node.value || typeof idx !== 'number' || !parent) return;
 
      parent.children[idx] = toMDX(node.value);
    });
  };

Sources: packages/core/src/mdx-plugins/remark-mdx-mermaid.ts:31-40

Caution

The visitor function skips code block nodes where idx is not a number or where parent is undefined, preventing out-of-bounds mutations on root or unattached AST nodes. Sources: packages/core/src/mdx-plugins/remark-mdx-mermaid.ts:36

MDX JSX Node Generation

When a matching code block is identified, the plugin delegates conversion to the toMDX helper function. This function constructs an MdxJsxFlowElement node configured to render the <Mermaid /> component.

function toMDX(code: string): MdxJsxFlowElement {
  return {
    type: 'mdxJsxFlowElement',
    name: 'Mermaid',
    attributes: [
      {
        type: 'mdxJsxAttribute',
        name: 'chart',
        value: code.trim(),
      },
    ],
    children: [],
  };
}

Sources: packages/core/src/mdx-plugins/remark-mdx-mermaid.ts:6-19

Note

The source code inside the matched code block is trimmed via code.trim() before being assigned to the chart attribute of the generated JSX element, removing leading and trailing whitespace from the diagram definition. Sources: packages/core/src/mdx-plugins/remark-mdx-mermaid.ts:14

LLM exports and postprocessing pipeline

Remark LLM Stringification and Placeholder Transformation

The remarkLLMs plugin processes markdown trees to generate clean plain-text representations and exportable string constants suitable for large language model ingestion. It accepts an options interface extending stringifier configurations with export name, heading identifier inclusion, element filtering, and placeholder conversion settings.

Option PropertyTypeDefault ValuePurpose
asstring'_markdown'Defines the variable name for the exported markdown string.
headingIdsbooleantrueAppends explicit heading anchors ([#id]) to output headings when present.
filterElementStringifyOptions['filterElement']Custom functionFilters elements; excludes mdxjsEsm by default while retaining others.
mdxAsPlaceholderstring[]undefinedTarget MDX component tag names to stringify as runtime placeholders.
_databooleanfalseStores the generated markdown string inside file.data.markdown.

Sources: packages/core/src/mdx-plugins/remark-llms.ts:9-55

The internal stringifier customizes node serialization by invoking placeholder for matching mdxJsxFlowElement and mdxJsxTextElement nodes listed in mdxAsPlaceholder.

stringify(node, parent, state, info, ctx) {
  if (mdxAsPlaceholder) {
    switch (node.type) {
      case 'mdxJsxFlowElement':
      case 'mdxJsxTextElement':
        if (node.name && mdxAsPlaceholder.includes(node.name))
          return placeholder(node, parent, state, info);
    }
  }
 
  return rest.stringify?.(node, parent, state, info, ctx);
},

Sources: packages/core/src/mdx-plugins/remark-llms.ts:80-91

Note

The placeholder helper extracts valid non-expression attributes, formats element phrasing children, and serializes the resulting payload into a null-delimited JSON string format conforming to PlaceholderData. Sources: packages/core/src/mdx-plugins/remark-llms.ts:112-129

Postprocessing Pipeline and Frontmatter Extraction

The remarkPostprocess transformer orchestrates document metadata generation, reference extraction, and optional LLM string export. It inspects configuration options to determine which values to extract and export into the module AST.

Option PropertyTypeDefault ValuePurpose
_format'md' | 'mdx'RequiredSpecifies the underlying document format.
valueToExportstring[][]Additional property keys from file.data to export as named variables.
includeProcessedMarkdownboolean | LLMsOptionsfalseEnables remarkLLMs processing and exports the resulting string.
extractLinkReferencesbooleanfalseCollects link href values and exports them via extractedReferences.
includeMDASTboolean | { removePosition?: boolean }falseSerializes and exports the document MDAST via _mdast.

Sources: packages/mdx/src/loaders/mdx/remark-postprocess.ts:14-40

Warning

If frontmatter.title is missing, the postprocessor traverses the AST to find the first level-1 heading and flattens its text nodes to auto-populate the document title. Sources: packages/mdx/src/loaders/mdx/remark-postprocess.ts:56-64

Exported declarations are injected into the AST by converting JavaScript values into estree nodes using valueToEstree, wrapped in an mdxjsEsm node via getMdastExport, and prepended to the tree's children array.

function getMdastExport(name: string, value: unknown): RootContent {
  return {
    type: 'mdxjsEsm',
    value: '',
    data: {
      estree: {
        type: 'Program',
        sourceType: 'module',
        body: [
          {
            type: 'ExportNamedDeclaration',
            attributes: [],
            specifiers: [],
            source: null,
            declaration: {
              type: 'VariableDeclaration',
              kind: 'let',
              declarations: [
                {
                  type: 'VariableDeclarator',
                  id: {
                    type: 'Identifier',
                    name,
                  },
                  init: valueToEstree(value),
                },
              ],
            },
          },
        ],
      },
    },
  };
}

Sources: packages/mdx/src/loaders/mdx/remark-postprocess.ts:128-161

Plugin presets and runtime aggregation

Pipeline Composition and Resolution

Fumadocs provides unified configuration presets that compose default remark and rehype plugin pipelines across different build environments and runtimes. The core package exports bundler and runtime preset factories (mdxPreset) alongside an MDX configuration preset (applyMdxPreset) that handles environments such as bundler or runtime.

Preset FunctionPackageEnvironment ContextDefault Output Format
mdxPresetfumadocs-core/content/mdx/preset-bundlerBundler build tools (Next.js, Vite, Webpack)Unmodified / default
mdxPresetfumadocs-core/content/mdx/preset-runtimeLocal or runtime executionfunction-body
applyMdxPresetfumadocs/config/presetConfigurable (bundler or runtime)function-body (for runtime)

Sources: packages/core/src/content/mdx/preset-bundler.ts:26-92, packages/core/src/content/mdx/preset-runtime.ts:23-83, packages/mdx/src/config/preset.ts:50-131

The plugin resolution layer accepts user-defined arrays or functions, conditionally appending default remark plugins like remarkGfm, remarkHeading, remarkImage, remarkCodeTab, remarkNpm, and remarkStructure, as well as rehype plugins like rehypeCode and rehypeToc.

const remarkPlugins = await resolvePlugins(
  (v) => [
    import('remark-gfm').then((mod) => mod.default),
    remarkHeadingOptions !== false &&
      import('@/mdx-plugins/remark-heading').then((mod) => [
        mod.remarkHeading,
        {
          generateToc: false,
          ...remarkHeadingOptions,
        },
      ]),
    _include ?? false,
    remarkImageOptions !== false &&
      import('@/mdx-plugins/remark-image').then((mod) => [
        mod.remarkImage,
        {
          ...remarkImageOptions,
          useImport: remarkImageOptions?.useImport ?? true,
        },
      ]),
    remarkCodeTabOptions !== false &&
      import('@/mdx-plugins/remark-code-tab').then((mod) => [
        mod.remarkCodeTab,
        remarkCodeTabOptions,
      ]),
    remarkNpmOptions !== false &&
      import('@/mdx-plugins/remark-npm').then((mod) => [mod.remarkNpm, remarkNpmOptions]),
    ...v,
    remarkStructureOptions !== false &&
      import('@/mdx-plugins/remark-structure').then((mod) => [
        mod.remarkStructure,
        {
          exportAs: 'structuredData',
          ...remarkStructureOptions,
        } satisfies Plugins.StructureOptions,
      ]),
  ],
  mdxOptions.remarkPlugins,
);
 
const rehypePlugins = await resolvePlugins(
  (v) => [
    rehypeCodeOptions !== false &&
      import('@/mdx-plugins/rehype-code').then((mod) => [mod.rehypeCode, rehypeCodeOptions]),
    ...v,
    import('@/mdx-plugins/rehype-toc').then((mod) => mod.rehypeToc),
  ],
  mdxOptions.rehypePlugins,
);

Sources: packages/core/src/content/mdx/preset-bundler.ts:38-86

Tip

Setting any preset option to false (such as remarkImageOptions: false or rehypeCodeOptions: false) explicitly disables that specific plugin from the unified pipeline. Sources: packages/core/src/content/mdx/preset-bundler.ts:15-20, packages/core/src/content/mdx/preset-bundler.ts:41-73

Alternative Runtimes and Vault Compilers

For lightweight markdown rendering and preview components, createMarkdownCompiler and createMarkdownRenderer instantiate standalone unified processors using remark, remarkRehype, and user-supplied plugin lists without full MDX compilation baggage.

const processor = remark()
  .use(remarkPlugins)
  .use(remarkRehype, remarkRehypeOptions)
  .use(rehypePlugins);

Sources: packages/preview/src/lib/md.ts:37-40, packages/core/src/content/md.ts:35-38

Similarly, the Obsidian integration builds a localized unified processor combining vault-specific remark plugins (remarkWikilinks, remarkConvert, remarkObsidianComment, and remarkBlockId) to transform vault files into compatible AST structures.

export function getRemarkPlugins(storage: VaultStorage, resolver: VaultResolver): PluggableList {
  return [
    [remarkWikilinks, { resolver, storage }],
    [remarkConvert, { resolver, storage }],
    remarkObsidianComment,
    remarkBlockId,
  ];

Sources: packages/obsidian/src/remark/index.ts:9-15, packages/obsidian/src/mdx/index.ts:24-24

Was this page helpful?