a3nm's blog

Self-hosted, server-side MathJax

I use MathJax on my website to render math equations to HTML. The standard way to use MathJax is to use JavaScript so that visitors will get it from their CDN. While this is simpler and also a good idea for caching, it has drawbacks which I did not find acceptable:

  • It means that the MathJax CDN may be pinged whenever a visitor loads a page, which is bad for privacy.
  • It makes your website's security dependent on that of the CDN: if the CDN starts distributing malicious JS (i.e., MathJax turns evil or it gets hacked), then your visitors will be getting them.
  • It renders math in the browser using JavaScript. This is jarring (as the page jumps around while rendering is done), and I find it esthetically unpleasant. All of this website is static and pre-generated on my machine, I don't see why math rendering would be an exception. I find static websites preferable in terms of deployability, security, and elegance.

This post is just to explain how to render MathJax when generating static pages, using MathJax-node. As I wanted to play with Docker, and didn't want to install Node or run the code directly on my real machine, I will also explain how to set up the environment in a Docker container, but there's nothing forcing you to do that. :)

I am now using this setup on this website, so all math should now be served with server-side rendering, without requests to third-party CDNs. (In fact, this removes what was essentially the only use of JavaScript on this site.)

Setting up a mirror of the fonts

While we won't need to serve the MathJax JavaScript code to readers, we will need to serve them the fonts used by MathJax.

Fortunately, on Debian systems, these fonts are already packaged as fonts-mathjax and fonts-mathjax-extras, so you can just rely on the package manager to retrieve them and keep them up to date. The fonts are installed in /usr/share/javascript/mathjax/, so you just have to configure your Web server to serve this folder. I serve it as a3nm.net/mathjax. It's preferable to serve it from the same domain that you will otherwise use, otherwise it's necessary to jump through additional hoops because of the same-origin policy: see an explanation here.

Installing MathJax-node

I installed MathJax-node in a Docker image, and as I was paranoid I also generated my own base image for the underlying system. Feel free to simplify the instructions if you don't need to do any of this.

I'm using an amd64 Debian system. I installed Docker as docker.io (packaged with Debian), added myself to the docker group, logged out and logged in. I tweaked Docker by editing /etc/default/docker and symlinking /var/lib/docker to move its files to a partition with more disk space.

I created the base system by issuing the following:

mkdir testing
sudo debootstrap testing testing/
sudo tar -C testing/ -c . | docker import - my-debian-testing

Here is the Dockerfile:

The following commands no longer work, because npm is no longer packaged in Debian. You should probably install npm manually instead (I haven't tried it yet). Thanks to Ted for pointing this out!

FROM my-debian-testing:latest
RUN apt-get -y update && apt-get install -y npm nodejs-legacy && apt-get clean
RUN npm install mathjax-node
RUN npm install cssstyle
CMD ["bash"]

As mathjax has reorganized their repositories, to make the following work, you will probably need to install manually mathjax-node-cli, as well as maybe installing mathjax-node and possibly mathjax-node-page. Again, I haven't tried it. Thanks again to Ted for pointing this out!

In the folder of the file, issue:

docker build -t my-mathjax .

You can now use the image by starting a container, let's call it my-container:

docker run -di --name=my-container my-mathjax bash >/dev/null

And you can then apply page2html by piping your HTML code in the following invocation:

docker exec -i my-mathjax node_modules/mathjax-node/bin/page2html \
    --fontURL https://a3nm.net/mathjax/fonts/HTML-CSS"

Replace the fontURL parameter by the URL you are serving the MathJax fonts.

Another possibility is to use page2svg to render the math to SVG instead of HTML markup. However, this means that text-based browsers will not be able to see it.

The actual code that I use is here. I also increase the size of math by adding the following CSS as indicated here:

.mjx-chtml {font-size: 2.26ex ! important}
.mjx-chtml .mjx-chtml {font-size: inherit ! important}

Marking up MathJax

I use Markdown to write Web pages. I use python-markdown-math to convert math notation from a dollar-based notation in Markdown to HTML spans with the right classes (class="tex" or class="math"). To use python-markdown-math with the server-side setup, simply prevent it from adding the MathJax script boilerplate. I also use the render_to_span config parameter to ensure that no script is being generated.

To prepare the MathJax afterwards, you should be careful that the command of the previous section needs to apply to the entire HTML document, not just the HTML markup generated from Markdown before applying a template. Indeed, you will see that modifies the head element as well.

Performance

On modern hardware with this setup, it takes about one second to process an HTML page (even when there is no math in it). It takes a few seconds to process HTML pages with real markup such as this one.

Here's an example formula to see how it works: it should look the same as any regular MathJax formula, but without the blinking caused by JavaScript rendering: π(I)=(FIπ(F))×(FJI(1π(F))).

comments welcome at a3nm<REMOVETHIS>@a3nm.net