Article

Extending AstroJS Markdown Processing With Remark and Rehype Plugins

abstract-syntax-tree ast html markdown remark rehype plugin astrojs
Posted on Wednesday, August the 14th 2024
5 min read

AstroJS

AstroJS is a framework for static website building that has a lot of exciting features like

Another great aspect of AstroJS is that it supports extensions of Markdown processing via Rheype and Remark plugins.

Remark and Rehype

Both Remark and Rehype are frameworks to transform documents by first transforming them to abstract syntax trees (AST) and then applying pluggable functions to the AST before converting it to the target format.

Remark parses Markdown and MDX files and converts them to HTML. Rehype parses and transforms HTML and exports HTML. Both frameworks have a large ecosystem of community plugins available.

Remark and Rehype may be combined if we export HTML after transformation with Remark, for example by using the remark-rehype plugin. AstroJS automatically does this for use, so we do not need to worry about the transformation and may directly write plugins for Remark and Rehype.

Extending AstroJS With Remark and Rehype

A Remark or Rehype plugin is a higher-order function—a function returning another function in this case—that takes an options argument for configuration and then returns a function that receives the abstract syntax tree (AST) of the parsed Markdown or HTML document.

The abstract syntax tree for both tools looks something like this:

{
  type: 'root',
  children: [
    { type: ..., children: [Array], position: [Object] },
    // ...
  ],
  position: {
    start: { line: 1, column: 1, offset: 0 },
    end: { line: 44, column: 11, offset: 1201 }
  }
}

Plugins for both Remark and Rehype may be registered in the Markdown or MDX integrations in astro.config.mjs. Below, we is an example configuration of the MDX integration with a Remark plugin theRemarkPlugin:

import { defineConfig } from "astro/config";
import theRemarkPlugin from "src/remark/the-remark-plugin"

// https://astro.build/config
export default defineConfig({
  integrations: [
    mdx({
      remarkPlugins: [theRemarkPlugin]
    })
  ]
});

If we want to add a Remark or Rehype plugin to the Markdown processor, we have to add it to markdown.remarkPlugins respectively markdown.rehypePlugins of the configuration object passed to defineConfig instead.

Example: Enabling rehype-mermaid in AstroJS

Lets, assume we want to add rendered Mermaid diagrams to our page by transforming the source code in fenced code blocks like the following:

```mermaid
flowchart LR

A[Hard] -->|Text| B(Round)
B --> C{Decision}
C -->|One| D[Result 1]
C -->|Two| E[Result 2]
```

Thankfully, a Rehype plugin to generate Mermaid diagrams rehype-mermaid from HTML <pre>or <code> elements with Mermaid diagram source code already exists. Adding it to our Markdown or MDX processing pipeline is therefore as easy as installing it with NPM and adding it to the configuration as shown above.

The plugin’s documentation, however, states that it only converts <pre class="mermaid"> and <code class="language-mermaid"> elements to rendered diagrams in SVG format.

Unfortunately, if we add a fenced code block with language mermaid to our post, the element produced by remark is a <pre> element with a class list like astro-code github-dark. To get our rendered Mermaid diagram, we have to add the class mermaid to this element.

With the tooling provided for Rehype via the unist-related project, this is luckily rather straight forward. To help finding the nodes of the AST that we are concerned with and transforming them as need, we may use the visit function from unist-util-visit.

This function, may—for simple scenarios like ours—be defined with only two parameters

  1. ast (the AST’s root) and
  2. a visitor function that applies our changes.

Omitting some implementation details, our Rehype plugin addMermaidClass to add the mermaid class to Mermaid code blocks then may be written as follows:

import { visit, CONTINUE } from "unist-util-visit"
import type { Plugin } from 'unified';
import type { Root, Element } from 'hast';

/* ... */

const visitor = (node: any) => {
	/* ... */
}

const addMermaidClass: Plugin<void[], Root> = () =>
  (ast: Root) => visit(ast, visitor)


export default addMermaidClass

Let’s now take a look at the implementation of the visitor function.

const dataLanguageMermaid = "mermaid"
const typeElement = "element"
const tagNamePre = "pre"
const classMermaid = dataLanguageMermaid

const isPreElement = (node: any) => typeof node.type !== undefined && node.type === typeElement
    && node.tagName !== undefined && node.tagName === tagNamePre
    && node.properties !== undefined && node.properties.dataLanguage === dataLanguageMermaid

const visitor = (node: any) => {
  if(!isPreElement(node)) {
    return CONTINUE
  }

  const element = node as Element
  const properties = element.properties
  const className = properties.className as Array<string>
  properties.className = [...className, classMermaid]

  return CONTINUE
}

The important things to note are

Note that the visit function should return an object of type Action instead of the visited node. In this case, we return the [CONTINUE] 12 action that tells Remark to continue traversing the AST.

The last step is to import the plugin and add it to the configuration of the MDX integration before the rehypeMermaid plugin. My AstroJS at the time of writing looks like this.

import { defineConfig } from "astro/config";
import icon from "astro-icon";
import tailwind from "@astrojs/tailwind";
import mdx from "@astrojs/mdx";
import rehypeMermaid from 'rehype-mermaid'
import addMermaidClass from "src/rehype/add-mermaid-class"

// https://astro.build/config
export default defineConfig({
  integrations: [
    tailwind(),
    icon(),
    mdx({
      rehypePlugins: [addMermaidClass, rehypeMermaid]
    })
  ]
});

Adding the addMermaidClass plugin before the rehypeMermaid plugin is important since the array order defines the execution order of the plugins and we need to change the class name before passing the AST on to the rehypeMermaid plugin.

The rendered result of the Mermaid diagram is shown below:

Text
One
Two
Hard
Round
Decision
Result 1
Result 2
friedrichkurz.me

© 2025 Friedrich Kurz

Privacy Policy