Today, we’ll be adding support for math in our Hugo-generated website!
First, let’s consider that we have two different options for rendering math: client-side or server-side.
Client-side rendering involves sending the raw math code (TeX) and then using JavaScript on the client convert the TeX code into rendered output, whether that’s HTML, or SVG, or some other representation. Client-side rendering is in many cases easier, as it doesn’t require any changes on the part of the content creator, since they just add some JS and CSS to the page, which is then rendered by the client browser, thus it enables a simple workflow.1
Server-side rendering is done at the time of the site generation, which is preferred as it is done once (since our site is static), the page loads much faster (don’t need to load extra JavaScript) and renders much faster (no client-side JavaScript running to render math).
Given this, the server-side approach is much more preferred due to its overall efficiencies, but most folks choose to use client-side rendering since the server-side rendering adds a few difficulties for the developer.
Let’s take a look to see what’s involved in making this work, specifically with Hugo — if you’re using another static site generator, or you’re writing HTML manually yourself, you may not have the same constraints as I do.
Spoiler: if you want to see what I ended up using, jump ahead to the Conclusion section at the end of this post.
Client-side rendering
Today, we will be using KaTeX and MathJax, popular JavaScript libraries for client-side rendering of mathematical formulas and equations. From some quick research, KaTeX is faster than MathJax 2, but the MathJax 3 has improved performance significantly over its previous version.
That said, what I read suggests that MathJax has more complete support for LaTeX, so if one doesn’t work for you, try the other one.
In the end, it’s up to you which one you use, and in this post, we will add optional support for both MathJax and KaTeX, and you can choose which one to use for each individual page of your site. The support is optional so that when you don’t enable either of them, the libraries will not be loaded, leading to a faster loading experience for your users.
We’ll start with the instructions from this blog post, but with some additional adjustments to get more complete support for LaTeX features we need.
First, create layouts/partials/helpers/katex.html
with the following contents:
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css"
integrity="sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ"
crossorigin="anonymous"
referrerpolicy="no-referrer">
<script
defer
src="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.js"
integrity="sha384-VQ8d8WVFw0yHhCk5E8I86oOhv48xLpnDZx5T9GogA/Y84DcCKWXDmSDfn13bzFZY"
crossorigin="anonymous"
referrerpolicy="no-referrer"
type="text/javascript"></script>
<script
defer
src="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/contrib/auto-render.min.js"
integrity="sha384-+XBljXPPiv+OzfbB3cVmLHf4hdUFHlWNZN5spNQ7rmHTXpd7WvJum6fIACpNNfIR"
crossorigin="anonymous"
referrerpolicy="no-referrer"
type="text/javascript"></script>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() {
renderMathInElement(document.body, {
delimiters: [
{left: "$$", right: "$$", display: true},
{left: "\\[", right: "\\]", display: true},
{left: "$", right: "$", display: false},
{left: "\\(", right: "\\)", display: false},
],
});
});
</script>
Expand to see my current version of katex.html
.
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css"
integrity="sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ"
crossorigin="anonymous"
referrerpolicy="no-referrer">
<script
defer
src="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.js"
integrity="sha384-VQ8d8WVFw0yHhCk5E8I86oOhv48xLpnDZx5T9GogA/Y84DcCKWXDmSDfn13bzFZY"
crossorigin="anonymous"
referrerpolicy="no-referrer"
type="text/javascript"></script>
<script
defer
src="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/contrib/auto-render.min.js"
integrity="sha384-+XBljXPPiv+OzfbB3cVmLHf4hdUFHlWNZN5spNQ7rmHTXpd7WvJum6fIACpNNfIR"
crossorigin="anonymous"
referrerpolicy="no-referrer"
type="text/javascript"></script>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() {
renderMathInElement(document.body, {
delimiters: [
{left: "$$", right: "$$", display: true},
{left: "\\[", right: "\\]", display: true},
{left: "$", right: "$", display: false},
{left: "\\(", right: "\\)", display: false},
],
});
});
</script>
The above contents were taken from the KaTeX website with minor changes; check back there for any newer versions of files and their hashes.
Similarly, let’s create layouts/partials/helpers/mathjax.html
with the
following contents per the MathJax docs:
<script type="text/javascript">
MathJax = {
tex: {
displayMath: [['$$', '$$'], ['\\[', '\\]']],
inlineMath: [['$', '$'], ['\\(', '\\)']],
},
};
</script>
<script
async
id="MathJax-script"
src="https://cdn.jsdelivr.net/npm/mathjax@3.2.0/es5/tex-mml-chtml.js"
integrity="sha384-+BSz3oj3ILMYvOBr16U9i0H4RZRmGyQQ+1q9eqr8T3skmAFrJk8GmgwgqlCZdNSo"
crossorigin="anonymous"
referrerpolicy="no-referrer"
type="text/javascript"></script>
Expand to see my current version of mathjax.html
.
<script type="text/javascript">
MathJax = {
tex: {
displayMath: [['$$', '$$'], ['\\[', '\\]']],
inlineMath: [['$', '$'], ['\\(', '\\)']],
},
};
</script>
<script
async
id="MathJax-script"
src="https://cdn.jsdelivr.net/npm/mathjax@3.2.0/es5/tex-mml-chtml.js"
integrity="sha384-+BSz3oj3ILMYvOBr16U9i0H4RZRmGyQQ+1q9eqr8T3skmAFrJk8GmgwgqlCZdNSo"
crossorigin="anonymous"
referrerpolicy="no-referrer"
type="text/javascript"></script>
Note 1: this is a very simple usage of MathJax meant for illustration and convenience. MathJax is highly configurable, and you can choose specific modules you would like to load, based on your needs. Please see the documentation for more details and adjust for your use case if needed.
Note 2: MathJax does not provide integrity hashes for their JavaScript files; how do we compute them for this and future versions? You have several options:
The easy way: put in a fake integrity, and open the URL in Chrome; it will fail to load the resource, but when you open the Developer Console, Chrome will tell you what hash it computed for the file (which didn’t match your fake one), which you can then use yourself.
The formal way, thanks to this comment on MathJax issue about subresource integrity:
$ curl '<URL>' -o - | openssl dgst -sha384 -binary - | openssl base64 -A
Another blog post talks about adding the following contents
to the layouts/partials/head.html
which involves making a copy of your theme’s
head.html
and then extending it with those contents. However, the theme I’m
currently using (Papermod) already supports easy extensibility
of the <head>
element contents — it provides and auto-includes an empty
file layouts/partials/extend_head.html
(really, it only has comments) and
includes it into
layouts/partials/head.html
, so I don’t
have to branch layouts/partials/head.html
and can just create
extend_head.html
in my own directory.
Thus, in my case, I created a new file layouts/partials/extend_head.html
in my
directory which overrides the default version of extend_head.html
and added
the following contents into it:
{{ if (eq $.Page.Params.math "katex") }}
{{ partial "helpers/katex.html" . }}
{{ else if (eq $.Page.Params.math "mathjax") }}
{{ partial "helpers/mathjax.html" . }}
{{ end }}
Note: if your theme doesn’t support extensibility for the
<head>
element easily as Papermod does, you will need to copy your theme’slayouts/partials/head.html
and extend it, as described in the other blog post. Also, consider submitting a feature request or providing a patch to your Hugo theme with a similar change as you can see in the Papermod theme for everyone’s convenience.
Finally, to enable math in particular post, we have to add one of the following to the frontmatter:
math: katex
to enable KaTeX for the postmath: mathjax
to enable MathJax for the post
Here’s an example snippet of this post’s frontmatter, which is using KaTeX:
---
title: "Writing math with Hugo"
tags: [hugo, math]
math: katex
---
To switch this post to use MathJax instead, just make the following change:
---
title: "Writing math with Hugo"
tags: [hugo, math]
-math: katex
+math: mathjax
---
Let’s test it out! For starters, here’s a well-known formula:
$$ E = mc^2 $$
and here’s another equation that puts together a few fundamental constants:
$$ e^{i \pi} + 1 = 0 $$
We can also do write integrals:
$$ \int \cos(x) dx = \sin(x) + C $$
and define derivatives:
$$ f’(x) = \lim_{t \to 0} \frac{f(x + t) - f(x)}{t} $$
All seems fine, right? Let’s try a matrix:
$$
\begin{bmatrix}
a & b \\
c & d \\
e & f \\
\end{bmatrix}
$$
which renders in MathJax as:2
and in KaTeX as (yes, TeX shows up on the page, mixed with HTML):
Neither of these renderings looks right; what’s going on?
Turns out, Hugo’s Markdown parser (Goldmark) converts the \\
sequences into
\<br>
, so the above TeX matrix definition results in the following HTML:
<p>
$$
\begin{bmatrix}
a & b \<br>
c & d \<br>
e & f \<br>
\end{bmatrix}
$$
</p>
However, \\
are newlines in TeX, and critical for matrices as well as
aligning multiple equations in a single environment. Since the KaTeX and
MathJax processing is happening client-side, by the time the JavaScript code
runs, it only sees \<br>
, not \\
, which means that KaTeX doesn’t process
the math inside those regions due to the spurious HTML tag in the middle of the
LaTeX region, while MathJax is simply missing the newline.
There are several possible solutions to this issue:
- use
\\\\
instead of\\
in Markdown source to keep\\
in the output - use
\newline
instead of\\
in LaTeX code, as that won’t disappear - keep using
\\
with a custom shortcode to avoid any processing of the\\
The first two options are straight-forward, and don’t
require any configuration, but they only work with MathJax — it won’t
help if you’re using KaTeX. Option 3 lets you use the standard \\
newline
markers, but requires using the custom shortcode described below, which is a bit
of an overhead.
Adding the math
shortcode to handle newlines in LaTeX
I saw this identified as an issue in Hugo’s Discourse, and although folks have provided answers, they are not included on the site itself, but rather via external links, and all the links are dead (and not available on the Internet Archive), so there are no complete examples there.3
As discussed in a related thread
regarding similar application of MathJax (alternative to KaTeX) together with
Goldmark, to pass through the sequence \\
correctly from Markdown all the way
to client-side rendering, we need to take a few additional steps.
First, we need to create a Hugo shortcode to keep the LaTeX literal code as-is,
without processing by Goldmark. Create a file named
layouts/shortcodes/math.html
with the following contents:
{{ .Inner }}
which basically just says to pass through the inner contents of the shortcode as-is.
Then, surround the math code block $$ ... $$
with the shortcode, so now it
should look like this:4
{{< math >}}
$$
\begin{bmatrix}
a & b \\
c & d \\
e & f \\
\end{bmatrix}
$$
{{< /math >}}
and it will render as follows:2
$$ \begin{bmatrix} a & b \\ c & d \\ e & f \\ \end{bmatrix} $$Success!
Summary
To summarize, here are all the steps that we took to enable client-side rendered math in our posts.
First, one-time setup for your site:
- Create the files:
layouts/partials/helpers/katex.html
andlayouts/partials/helpers/mathjax.html
- Add the handling for the
katex
andmathjax
params in the frontmatter. Depending on your theme, either:- copy your theme’s
layouts/partials/head.html
into your tree, or - if your theme already has a blank partial that’s already loaded into
head.html
and intended for extensibility, create a file matching that name instead
- copy your theme’s
- Create the file
layouts/shortcodes/math.html
Then, for each post:
- Specify either
math: katex
ormath: mathjax
in the frontmatter - Surround math blocks that use
\\
with the{{< math >}}
shortcode
Here’s the difference in rendering the above matrix in KaTeX vs. MathJax:2
KaTeX (client-side) | MathJax (client-side) |
---|---|
The renderings are quite similar with some minor differences in spacing.
Server-side rendering
In the ideal world, we would just integrate the math processing directly into Hugo, so that running the Hugo process, we would get both the Markdown processing as well as the rendering the TeX math code as well. And this is what was tried with a Hugo pull request to add KaTeX server-side math rendering support to Hugo in Feb 2020 which integrates the Goldmark extension for rendering KaTeX with QuickJS5.
However, it was not merged, because Hugo has a policy of not allowing any code
that uses C (rather than pure Go), because it limits Hugo’s portability. The one
exception that Hugo supports right now is Sass, which is part of the
hugo-extended
binary, so it looks like the server-side rendering of math via
KaTeX will not be merged in the foreseeable future.6
It is worth mentioning that the person who proposed that pull request switched from Hugo to Hexo, another static site generator which is written in Node, to get the benefit of server-side rendered math, because it is so important to them. I’m not ready to move away from Hugo as that will require finding another theme and adapting my entire site to use it, so I am still interested in making this work with my current Hugo workflow.
An alternative was proposed to add goldmark-mathjax to
Hugo; however, per a comment on that
PR,
goldmark-mathjax
doesn’t actually do server-side
rendering, but rather avoids the need for the {{< math >}}
shortcode we
needed to add previously. That said, FastAI folks are maintaining a fork of
Hugo (repo) with that patch
if you’re interested.
The author of goldmark-qjs-katex
ended up forking Hugo to their own
repository named kahugo with the necessary changes to make it
render math statically during site generation. He’s also written two blog posts
(one, two) about the development and
usage of their KaTeX-enabled Hugo fork.
Using kahugo
If you’re interested in trying this out, you can build kahugo
via:
$ git clone https://github.com/graemephi/kahugo.git
$ cd kahugo
$ go build
Note: this requires Go 1.16 or later to build.
The post frontmatter should be changed as follows:
---
title: "Writing math with Hugo"
tags: [hugo, math]
-math: katex
+math: katexSsr
---
Update layouts/partials/extend_head.html
as follows:
{{ if (eq $.Page.Params.math "katex") }}
{{ partial "helpers/katex.html" . }}
+{{ else if (eq $.Page.Params.math "katexSsr") }}
+ {{ partial "helpers/katex_ssr.html" . }}
{{ else if (eq $.Page.Params.math "mathjax") }}
{{ partial "helpers/mathjax.html" . }}
{{ end }}
Expand to see my current version of extend_head.html
.
{{ if (eq $.Page.Params.math "katex") }}
{{ partial "helpers/katex.html" . }}
{{ else if (eq $.Page.Params.math "katexSsr") }}
{{ partial "helpers/katex_ssr.html" . }}
{{ else if (eq $.Page.Params.math "mathjax") }}
{{ partial "helpers/mathjax.html" . }}
{{ end }}
And here’s what should be in layouts/helpers/katex_ssr.html
:
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css"
integrity="sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ"
crossorigin="anonymous"
referrerpolicy="no-referrer">
Expand to see my current version of katex_ssr.html
.
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css"
integrity="sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ"
crossorigin="anonymous"
referrerpolicy="no-referrer">
Server-side rendering does not require using the {{< math >}}
shortcode
that we developed above for client-side rendering, but if you already have
pages using the {{< math >}}
shortcode that you would like to make
compatible with server-side rendering (as I do), you can update it as follows:
-{{ .Inner }}
+{{ if (eq $.Page.Params.math "katexSsr") }}
+ {{ .Inner | markdownify }}
+{{ else }}
+ {{ .Inner }}
+{{ end }}
Expand to see my current version of layouts/shortcodes/math.html
.
{{ if (eq $.Page.Params.math "katexSsr") }}
{{ .Inner | markdownify }}
{{ else }}
{{ .Inner }}
{{ end }}
Then, develop and render your site with the kahugo
binary you built above:
$ kahugo server [...flags...]
You can then build the minified site for production locally via:
$ kahugo --minify [...flags...]
and if you happen to use Netlify (as I have been for this site), you can upload it manually using Netlify’s CLI via:
$ netlify deploy
To integrate this into Netlify build, add a command to your netlify.toml
such
as the following (see this blog post for an example):
# Warning: I have not tested this myself, and I'm not using this myself at this
# time; use at your own risk. If this works well for you, please let me know!
[build]
publish = "public"
command = "curl -sL [kahugo binary URL] && ./kahugo --minify"
[context.deploy-preview]
command = "curl -sL [kahugo binary URL] && ./kahugo --minify -b ${DEPLOY_PRIME_URL}"
Note: you would need to provide hosting for your pre-built
kahugo
binary, since the repo mentioned above does not provide any binaries in the releases section. I wasn’t able to buildkahugo
directly from the Git repo via ago install
orgo get
commands; YMMV.
Caveats of server-side rendering with kahugo
The downside here is that kahugo
, the fork of Hugo with KaTeX support, does
not appear to be actively maintained, so you will be stuck on an older version
of Hugo, or you will need to take on the maintenance of this fork and re-apply
the changes to each new release of Hugo, which I do not have the time for right
now.
Additionally, my initial testing of server-side rendering with kahugo
shows
that variables are not italicized by default, leading to the following rendering
of variables in a matrix:2
KaTeX (server-side) | KaTeX (client-side) | MathJax (client-side) |
---|---|---|
To get equivalent behavior server-side that’s default in client-side rendering
with KaTeX and MathJax (and TeX in general), I would have to enclose each
variable in {\it ...}
, which significantly reduces readability, and increasing
amount of typing, so I opted not to use this approach.
Potential server-side rendering alternatives
An issue filed on goldmark-qjs-katex suggests exploring godzilla and/or otto, which are different JavaScript runtimes, and if they don’t depend on CGO, they may be eligible to be included into Hugo mainline by default.
However, this seems like a non-trivial undertaking, and it’s unclear if anyone is currently working on this. If you do make progress on this, please update that issue.
According to Hugo documentation, Hugo supports Pandoc
for files with the *.pandoc
or *.pdc
extension (or the markup
value in
front matter set to pandoc
or pdc
), and Pandoc supports rendering math in
HTML. This might lead one to assume that we can get
server-side rendering of math this way, but alas, most of the options are not
applicable:
--mathjax
(passed by Hugo by default) is a path to the MathJax JS file, and Pandoc just marks the TeX math in the file with the markers that MathJax will recognize--mathml
produces MathML, but that’s only supported by Firefox and Safari--webtex
converts LaTeX to URL-encoded image URLs which will be rendered by third-party servers dynamically--katex
has the same functionality as--mathjax
above--gladtex
may be the only viable server-side rendering option here, but it requires running an additional command-line toolgladtex
afterpandoc
is run (which is invoked byhugo
), which complicates local development sincehugo server
never exits
There are Pandoc filters for both MathJax as well as KaTeX, so that may be helpful to convert TeX to SVG and display math inline in the HTML documents.
Pandoc seems to be also well-integrated in Quarto, a publishing system for scientific and technical documents. Quarto also supports Hugo, so there’s a potential integration / reuse possibility there as well, without having to switch to another static site generator and look for another theme, and adopt all existing posts to a new blog engine.
Conclusion
Having spent a lot of time researching server-side rendering for math, and trying out the available options for Hugo (which I’ve documented above), I decided to use client-side rendering with KaTeX for now, which is how this page is currently rendered, at the time of publication.
Hope this article and the pointers here have been useful!
If you end up using anything described here, or if you end up pursuing server-side rendering, please let me know how it works for you.
There are also client-side Markdown processors as well as client-side syntax highlighting libraries. ↩︎
All illustrative renderings in this post (both client-side and server-side) were previously included on the page using declarative shadow DOM with templates and slots in an attempt to isolate the KaTeX and MathJax rendering in a separate DOM subtree, but we ended up needing to contain them via
<iframe>
to isolate the independent renderings entirely, because without<iframe>
, shadow DOM was unable to isolate JavaScript entirely and their effects were leaking out of their shadow DOM container and impacted the rest of the page and other rendered chunks, leading to very confusing results. ↩︎ ↩︎ ↩︎ ↩︎This is why Stack Overflow likes to avoid link-only answers, because inevitably, external sites go down, disappear, or their domain names are lost, sold, etc. and then the only reference for the solved problem is now gone. Always copy the relevant portion of a code snippet so that your answer is self-contained! ↩︎
Note that to include the shortcode literal into a post, you need to escape the internal contents of
{{...}}
with/* ... */
; for details, see this very helpful blog post. ↩︎See also additional discussion on another Hugo issue: Consider markdown extensions for math typesetting in Hugo. ↩︎