Tobias Alexander Franke

Pre-baked MathML in Jekyll

I often run into two problems when reading a blog post on mathematics: My feed-reader does not evaluate Javascript, and all equations are presented as raw Mathjax or Latex-inline code.

My feed-reader presenting me a bunch of gibberish
My feed-reader presenting me a bunch of gibberish

There are of course feed-readers that do, but I don't use them. I could open the article in a browser, but alas I do block Javascript there as well.

Mathjax in Firefox with Javascript disabled
Mathjax in Firefox with Javascript disabled

Whilst Mathjax is a very comfy solution to displaying nice looking equations on a webpage, most blog-pages these days (with their respective feeds included) are rendered using static website generators such as Jekyll or Hugo. This means all equations can be pre-baked and do not need any Javascript at all.

Math equations can be generated in multiple different ways though:

  1. SVG: Render the entire equation into an SVG. These will scale nicely and always look sharp.
  2. MathML: Use the W3 standard to render math equations directly in HTML. They too scale well.
  3. Image: Render the equation into a regular image. This can look jaggy/aliased.

I decided to investigate options 1 and 2. It turns out however that SVG is an element that gets blocked by many feed-readers or filtered out entirely.

Therefore, option 2 was left. My page is generated using Jekyll and uses plugins to render math with the help of mathjax-node-page, a Node module which receives a piece of Latex-infused text and then spits out MathML.

If you want to replicate this process, read on.

Node and mathjax-node-page

As a first step, install npm and the mathjax-node-page module.

$ PKGMANAGER install npm
$ npm install mathjax-node-page

You'll end up with a directory node_modules that contains the binary mjpage which is used by the script in the next section.

00-render-math.rb

The following piece of code renders MathML in case the page features any Latex markers (and is not an XML file). It calls mjpage to replace said markers with MathML code, then puts the content back. Store this as _plugins/00-render-math.rb.

The script does two more things to make sure each equation is followed by a reference number, and removes some annoying leftover attributes from MathML because I format everything using CSS.

require 'nokogiri'

MATH_TAG_REGEX = /(<script[^>]*type="math\/tex|\\\[.*\\\]|\\\(.*\\\))/im

FIELDS = {
  "format" => "--format",
  "font" => "--font",
  "equation_number" => "--eqno",
  "output" => "--output",
  "eqno" => "--eqno",
  "ex_size" => "--ex",
  "width" => "--width",
  "extensions" => "--extensions",
  "font_url" => "--fontURL"
}

FLAGS = {
  "linebreaks" => "--linebreaks",
  "single_dollars" => "--dollars",
  "semantics" => "--semantics",
  "notexthints" => "--notexthints",
  "fragment" => "--fragment"
}

def run_mjpage(config, output)
  mathified = ""
  exit_status = 0

  command = "node_modules/mathjax-node-page/bin/mjpage"

  FIELDS.each do |name, flag|
    unless config[name].nil?
      command << " " << flag << " " << config[name].to_s
    end
  end

  FLAGS.each do |name, flag|
    unless config[name].nil?
      command << " " << flag
    end
  end

  begin
    Open3.popen2(command) {|i,o,t|
      i.print output
      i.close
      o.each {|line|
        mathified.concat(line)
      }
      exit_status = t.value
    }
    return mathified unless exit_status != 0
    Jekyll.logger.abort_with "render-math:", "'node_modules/mathjax-node-page/mjpage' not found"
  rescue
    Jekyll.logger.abort_with "render-math:", "Failed to execute 'node_modules/mathjax-node-page/mjpage'"
  end

end

Jekyll::Hooks.register([:pages, :posts], :post_convert) do |page|
  if MATH_TAG_REGEX.match?(page.content) && !page.relative_path.end_with?(".xml")
    page.content = run_mjpage(page.site.config["mjpage"], page.content)

    # exchange for regular mathml rows
    page.content = page.content.gsub(/<\/?mlabeledtr\b([^>]*)>/i) { |tag| tag.start_with?('</') ? "</mtr>" : "<mtr#{$1}>" }

    doc = Nokogiri::HTML(page.content)

    # add column alignment for labels
    doc.css('mtable').each do |mtable|
      mtable.remove_attribute('width')
      mtable.remove_attribute('columnspacing')
      mtable.remove_attribute('rowspacing')
      mtable['columnalign'] = "left"

      # Move first mtd to the end in each mtr
      mtable.css('mtr').each do |mtr|
        first_mtd = mtr.css('mtd').first
        if first_mtd
          first_mtd.remove
          mtr.add_child(first_mtd)
        end
      end
    end

    page.content = doc.to_html

    Jekyll.logger.info("Mathified " + page.relative_path)
  end
end

You can configure the behavior of this script in your _config.yml. Here's the configuration I currently run.

mjpage:
  single_dollars: true
  eqno: all
  width: 83
  output: MML
  fragment: true

Importantly, output: MML sets the output to MathML instead of SVG.

01-add-rss-content.rb

Instead of re-rendering the output for RSS, this plugin takes the already mathified content variable of each page and puts the entire output into a custom page variable for later. Store it as _plugins/01-add-rss-content.rb. The number in the plugin file determines the execution order, so this one is executed after 00-render-math.rb.

Jekyll::Hooks.register([:posts], :post_convert) do |p|
  doc = Nokogiri::HTML.parse(p.content)
  p.data["rsscontent"] = doc.at('body').inner_html.strip
end

rss.xml

In the final RSS XML, we can access the page's generated rsscontent variable from the plugin and just dump it directly into the description. This ensures that the generated MathML code is also present in the RSS item, and the HTML of the webpage's body matches exactly with the RSS item's <description>.

...
<description><![CDATA[ {{ post.rsscontent }} ]]></description>
...

And that's it. Now the webpage and the feed both contain beautiful rendered math equations, even when Javascript is entirely disabled, just as the W3 intended!

My god, it's full of stars!
My god, it's full of stars!
 2026-08-10
 RSS MathML SVG Mathjax Jekyll
 Comments