Rendering LaTeX in Hakyll

Web DevelopmentTutorial
📝

A note before we start. This post describes a general technique for rendering LaTeX on a Hakyll site — it is not about this website specifically. This site has since migrated to Lean 4 and Verso. The approach below still applies to any Hakyll/pandoc project, so I've kept it around.

What is Hakyll?

Hakyll is a Haskell library for generating static sites, aimed at small-to-medium sites and personal blogs. It is highly configurable and uses an xmonad-like DSL for configuration.

When I first put this site together, I wanted more control over how posts were rendered than plain Markdown gave me. LaTeX offered exactly the expressiveness I needed — math, theorem environments, custom macros — while still compiling down to clean HTML.

Markdown vs. LaTeX: when to reach for each

Hakyll happily renders both, so the question is what you need:

  • Markdown is perfect for prose: headers, lists, links, and fenced code blocks with syntax highlighting.

  • LaTeX wins when you need real mathematical notation, numbered theorem-style environments, or macros you want to reuse across posts.

The good news: because both go through pandoc, you can mix and match per file.

Rendering Markdown

Hakyll is integrated with pandoc, so Markdown with proper formatting "just works". Headers double as a table of contents when toc is set to true:

# Header Text
## Subheader Text

Fenced blocks get syntax highlighting:

```haskell
main :: IO ()
main = putStrLn "Hello, world!"
```

Compiling Markdown to HTML

Place your posts in a directory (say posts/) and compile:

match "posts/*.md" $ do
    route $ setExtension "html"
    compile $ pandocCompiler
        >>= relativizeUrls

Rendering LaTeX

TeX files are supported too, but the pipeline differs slightly. In its simplest form, a .tex source looks like:

\title{Some Title}
\section{Some Section}
Some text.

Theorem environments

This is where LaTeX earns its keep. Define reusable environments once:

\newtheorem{theorem}{Theorem}
\newtheorem{corollary}[theorem]{Corollary}
\newtheorem{lemma}[theorem]{Lemma}
\theoremstyle{definition}
\newtheorem{definition}[theorem]{Definition}
\theoremstyle{remark}
\newtheorem{remark}{Remark}

…then use them with real mathematical notation inside:

\begin{definition}[Prime numbers]
    A \emph{prime number} is a natural number greater than 1 that
    has no positive divisors other than 1 and itself.
\end{definition}

\begin{lemma}[Infinitude of primes]
    There are infinitely many prime numbers.
\end{lemma}

Compiling LaTeX to HTML

The trick is to run pandoc as a filter, going from latex to html5 and asking it to emit MathJax-friendly markup with --mathjax:

match "*/*.tex" $ do
    route   $ constRoute "*/index.html"
    compile $ getResourceString
        >>= withItemBody (unixFilter "pandoc"
            ["-f", "latex", "-t", "html5", "--mathjax"])
        >>= relativizeUrls

Making the math actually render

--mathjax only tells pandoc to emit math in a form a JavaScript typesetter can read — you still need to load the typesetter in your template. Drop MathJax (or the lighter KaTeX) into your HTML <head>:

<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
        async></script>

With that in place, pair a little CSS for the theorem environments with MathJax for the formulas, and the LaTeX above renders like this:

Rendered output

Definition (Prime number). A prime number is a natural number greater than 1 whose only positive divisors are 1 and itself.
Lemma (Pythagoras). For a right triangle with legs \(a\) and \(b\) and hypotenuse \(c\), we have \(a^2 + b^2 = c^2\).
Theorem (Euclid). There are infinitely many primes: for every \(n\) there exists a prime \(p > n\).

The boxes come from CSS targeting the environment class names pandoc emits; the equations are typeset live by MathJax from the \(…\) and \[…\] delimiters.

Wrapping up

Hakyll's pandoc integration makes LaTeX a first-class citizen alongside Markdown: write prose in Markdown, switch to .tex when you need theorems and math, and let a single MathJax include tie the formulas together. Even though this site now runs on Verso, the same pandoc-as-a-filter idea carries over to just about any static-site generator.