Geoff Ruddock

Render LaTeX math expressions in Hugo with MathJax 3

This blog runs on Hugo, a publishing framework which processes markdown text files into static web assets which can be conveniently hosted on a server without a database. It is great for a number of reasons (speed, simplicity) but one area where I find it lacking is in its support for math typesetting.

The problem

Typically, you embed a javascript library such as MathJax or KaTeX by adding a line of HTML to your website template. While the page is loading in a visitor’s browser, the library processes text enclosed in dollar signs and, renders it as LaTeX and replaces the contents of the page.

The problem is that the initial page contents have already been processed by Hugo’s markdown engine before the page even loads. The markdown parser interprets underscores (_) as italics, and so it removes them and wraps the enclosed text in the appropriate HTML tags. However the underscore is frequently used in LaTeX for subscript. E.g. x_1 gets rendered to $ x_1 $. So if your page contains multiple underscores, your LaTeX code will be broken before the page even starts loading.

The (typical) solution

The best general approach seems to be this one:

  1. Configure MathJax to attempt to typeset within <code> blocks (which it skips by default)
  2. Add a class has-jax to your CSS which undoes whatever code-specific formatting your website uses.
  3. Add a pseudo-callback to MathJax which waits until typesetting is complete, then runs a piece of javascript to add the above class to all the parent element of all MathJax elements.

The page above includes all the necessary code snippets to implement this for MathJax 2.x. But MathJax 2 is a lot slower than MathJax 3 or KaTeX. I tried simply swapping out the src for the newer version, but this did not work, because it seem that MathJax 3 uses an entirely new syntax than 2.x.

MathJax v3 is a complete rewrite of MathJax from the ground up, and so its internal structure is quite different from that of version 2. That means MathJax v3 is not a drop-in replacement for MathJax v2, and upgrading to version 3 takes some adjustment to your web pages. 1

Adapted for MathJax 3

The code below is a modification of Doswa’s code which loads MathJax 3 instead of 2.x.

  1. Create a file in your theme directory layouts/partials/mathjax_support.html as the following:

    <script>
      MathJax = {
        tex: {
          inlineMath: [['$', '$'], ['\\(', '\\)']],
          displayMath: [['$$','$$'], ['\\[', '\\]']],
          processEscapes: true,
          processEnvironments: true
        },
        options: {
          skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre']
        }
      };
    
      window.addEventListener('load', (event) => {
          document.querySelectorAll("mjx-container").forEach(function(x){
            x.parentElement.classList += 'has-jax'})
        });
    
    </script>
    <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
    <script type="text/javascript" id="MathJax-script" async
      src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
    
  2. Next, open the file layouts/partials/header.html and add the following line just before the closing </head> tag:

    {{ if .Params.mathjax }}{{ partial "mathjax_support.html" . }}{{ end }}
    
  3. Then, add the following lines to your CSS file. You may need to tinker with the contents here depending on your theme, these are just the settings which worked for me.

    code.has-jax {
        -webkit-font-smoothing: antialiased;
        background: inherit !important;
        border: none !important;
        font-size: 100%;
    }
    
  4. Finally, add mathjax: true to the YAML frontmatter of any pages containing math markup. Alternatively, you could omit the outer {{ if .Params.mathjax }} … {{ end }} conditional above to load the library automatically on all pages. However given that this library is quite heavy (it’s consistently the asset that Google PageSpeed Insights complains the most about) and that only <20% of my blog posts contain math at all, this is worth the extra effort for me.

Other approaches I considered

Here are a few other solutions I looked into, but ultimately decided not to adopt as a final solution.

Manually escape all problematic characters

You could manually escape all underscore or backslash characters with an additional backslash. This works if you rarely use LaTeX and just need a specific expression to render correctly, but it will get quickly annoying if your posts include multiple math expressions. Besides breaking rendering of LaTeX in your markdown editor, it also makes the raw code difficult to read.

Use MMark markdown processing engine

Hugo lets you specify which processing engine to use to convert markdown during the build process. There is one engine—MMark—which handles LaTeX well and so makes the above modifications entirely unnecessary. This was the approach previously officially recommended in Hugo documentation.

However according to the current docs, MMark is deprecated and will be removed in a future release. It may work for a while still, but it doesn’t make sense for me to adopt a solution that is already deprecated.

Goldmark engine with MathJax extension

The new default markdown engine used by Hugo is called goldmark. There is an extension goldmark-mathjax that seems to do exactly what we want. But as of Feb 2020, a PR to merge it into hugo for relying on unacceptable dependencies. So for the time being, this approach would require forking Hugo and modifying it to use this extension. I have no real experience with Go, so I decided to avoid this approach for now.

KaTeX math shortcode

If you are willing to use KaTeX instead of MathJax, then this approach may be a good option. But it is cumbersome to wrap all your inline math equations in a shortcode. It is already annoying that the backtick approach breaks in-editor latex rendering in most editors, but at least the raw latex code is displayed in monospace text, and the backticks do not take up much screen space. For example, to render $x=1$ you would need to type {{ < math > }}x=1{{ </math> }}, which makes it even more difficult to read and edit content in your markdown editor. I didn’t find the speed difference between KaTeX and Mathjax 3 to be sufficient to justify the decreased editing experience.


comments powered by Disqus