Introduction

If you don’t know what Hugo and Mermaid are, then that’s not a problem, here’s a super quick introduction.

Hugo is a static website generator written in Go. It allows you to easily create websites using markdown and content files like images. At the very basic level, you write your articles using markdown and then Hugo converts it to HTML.

It also allows code blocks in your markdown files, so you can add code snippets. Besides code snippets, it also supports diagrams that bring us to Mermaid.

So what is Mermaid? Mermaid is a JavaScript library that allows you to generate diagrams such as pie charts using simple text-based syntax. Here is an example of a Mermaid code block for a pie chart:

```mermaid
pie title Pizza
    "Pizza I have not yet eaten": 75
    "Pizza I have eaten": 25
```

The above code block results in this nice diagram:

Diagram

Ain’t that easy? Usually, Mermaid diagrams are rendered in the user’s browser using JavaScript. But if you, like me, prefer to lift that burden from the client’s browser and instead render those diagrams upfront and deliver the ready-rendered charts and graphs, then what?

Fear not, for I too have navigated those troubled waters. Here comes Mermaid CLI with a bit of tinkering to the rescue!

Convert Mermaid diagrams to SVG image files

Mermaid CLI is a command-line tool that converts Mermaid definitions to svg/png/pdf files. Since Hugo doesn’t offer a built-in solution for hooking-up external tools such as Mermaid CLI, we have to find a way around this.

Here’s the idea:

  1. Have a script of some sort that recursively iterates over our markdown files and converts Mermaid code blocks to SVG files
  2. Make Hugo use these SVG files instead of the embedded Mermaid code
Diagram

For this to work we adhere to the following convention: Each blog post lives in its own subfolder with an index.md file like in the following example:

.
└── content
    └── posts
        ├── my-first-post
        |   └── index.md
        └── my-second-post
            └── index.md

Each index.md file can contain Mermaid code blocks. Then we can do something along the following lines:

#!/bin/bash

set -euo pipefail

MERMAID=minlag/mermaid-cli:10.8.0

docker pull ${MERMAID}

while IFS= read -r -d '' file
do
  DIR=$(dirname "$file")
  docker run --rm -v ./"${DIR}":/data ${MERMAID} -i index.md -o index.svg
done<   <(find content/ -name index.md -print0)

The above script, placed in the root folder of our Hugo project, scans the content folder for files named index.md. For each file it finds, it launches Mermaid CLI (in a Docker container) with the found file as input. It then creates separate index-<n>.svg files, one for each block of Mermaid code it finds, numbered consecutively starting with 1 (i.e. index-1.svg, index-2.svg, and so on. Yes, I know, programmers usually start counting at 0, but here it starts at 1).

We then might have something like this:

.
└── content
    └── posts
        ├── my-first-post
        |   ├── index.md
        |   ├── index-1.svg
        |   └── index-2.svg
        └── my-second-post
            ├── index.md
            ├── index-1.svg
            ├── index-2.svg
            └── index-3.svg

Now we only have to tell Mermaid to use the svg files. We do this by creating a so-called code block render hook.

We create the following file in our project:

.
└── layouts
    └── _default
         └── _markup
             └── render-codeblock-mermaid.html

… with the following content:

<img src="index-{{ add .Ordinal 1 }}.svg"/>

This replaces each Mermaid code block with an image tag with the src-attribute set to index-<n>.svg with n starting at 1. .Ordinalis the zero-based ordinal of the code block on the page.

There is a catch

The above solution works fine as long as there are no code blocks with other languages before a mermaid code block, in which case the numbering of the index-<n>.svg files will not match the numbering of the generated image tags. This is because on the one hand the mermaid cli tool only counts and numbers mermaid code blocks, and on the other hand the .Ordinal method in render-codeblock-mermaid.html considers all code blocks.

Example

Let’s consider the following example with two code blocks:

```csharp
public class Program
{
    public static void Main(string[] args)
    {
        System.Console.WriteLine("Hello, Mermaid!");
    }
}
```
```mermaid
flowchart LR
    Hello --> C-Sharp
```

The mermaid cli only considers the one mermaid code block and creates a file named index-1.svg accordingly. However, in the render-codeblock-mermaid.html hook, the .Ordinal method considers all code blocks, which results in the following image tag being inserted:

<img src="index-2.svg"/>

This is because the mermaid code block is the 2nd code block in the file. Bingo, we have a 404!

A workaround for this problem is to rename the generated index-<n>.svg files and insert the correct numbers, correct as in how the .Ordinal method counts. To do this, we grep for all opening code block fences in the index.md file, number them, then grep only for the mermaid code blocks and number them again. This way we get a mapping of the form wrong number -> right number.

grep -E '^```([a-z]+)' index.md | nl -w1 | grep mermaid | awk '{print $1}' | nl -w1 -s:

In our example above this will print out the following, meaning the generated file index-1.svg should really be named index-2.svg:

1:2

Let’s insert this into our script:

#!/bin/bash

set -euo pipefail

MERMAID=minlag/mermaid-cli:10.8.0

docker pull ${MERMAID}

while IFS= read -r -d '' file
do
  DIR=$(dirname "$file")
  docker run --rm -v ./"${DIR}":/data ${MERMAID} -i index.md -o index.svg

  # Fix numbering of output files considering all code blocks found in the .md file
  readarray -t number_map < <(grep -E '^```([a-z]+)' "${DIR}"/index.md | nl -w1 | grep mermaid | awk '{print $1}' | nl -w1 -s:)
  for value in "${number_map[@]}"
  do
    readarray -t numbers < <(echo "$value" | tr ":" "\n")
    if [[ "${numbers[0]}" != "${numbers[1]}" ]]; then
      mv -v "${DIR}"/index-"${numbers[0]}".svg "${DIR}"/index-"${numbers[1]}".svg
    fi
  done
done<   <(find content/ -name index.md -print0)

In our example above this will rename index-1.svg to index-2.svg and the image file will be found.

Conclusion

By using Hugo with Mermaid and the Mermaid CLI, you can integrate Mermaid diagrams into static websites without relying on client-side JavaScript. You can convert Mermaid code blocks to SVG images at build time and use a custom render code block to shift the burden of rendering diagrams from the client’s browser to the server. This approach makes your site load faster and gives users a smoother experience, while still letting you use the simple, elegant text-based syntax for generating diagrams that Mermaid is known for.

Resources