Self-hosted, server-side MathJax
According to a followup blog post, the method presented here no longer works with modern versions of MathJax. If you have insights about how to make them work currently, I'd be interested to know, please email me!
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 span
s with
the right class
es (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)=(∏F∈Iπ(F))×(∏F∈J∖I(1−π(F))).