aftermath
is a Node script for post-processing HTML that was generated by an SSG tool like Hugo. Its purpose is to "pre-render" some things that would otherwise be rendered with client-side Javascript, e.g. KaTeX equations.
Given a directory of HTML files, aftermath
will apply the following transformations:
- Replace mermaid code blocks with compiled SVGs.
- Replace KaTeX syntax with compiled SVGs.
- Replace D2 code blocks with compiled SVGs.
Because aftermath
is quite slow for decent sized blogs, I recommend using it for production builds only, where minimal Javascript is desirable. During development, client-side Javascript rendering is probably preferable.
Although designed to work with HTML output by Hugo, in theory aftermath
doesn't really care where the HTML came from, and could be used with the output from other SSGs (or even handwritten documents!)
Warning
aftermath
processes all the HTML using Cheerio. This may result in some spurious transformation (e.g. changing whitespace or escaping) if the output format of Cheerio's.html()
is different than the original HTML.In particular, HTML typographical character references e.g.
–
, which are generated by the Goldmark markdown renderer's typographer extension, are converted to their un-escaped Unicode equivalent. This aligns with W3C's recommendation but can cause encoding issues if you don't specify a utf8 charset in your HTML.
We can "install" aftermath
by adding it as a submodule in our blog's Git repository:
cd projects/blog
git submodule add https://github.com/luketurner/aftermath.git aftermath
The aftermath
tool is intended to be run in Docker (or equivalent container builder and runtime). Once we've cloned the repo, we can build an aftermath
image:
docker build aftermath -t aftermath
If you want to edit configuration settings, you'll find them in aftermath/src/config.mjs
, after which you'll need to rebuild the image.
Now we can run an aftermath
container:
docker run aftermath
Just running the container like that won't actually do anything useful, though; aftermath
transforms HTML in-place, but we haven't given it any HTML to transform.
In order for aftermath
to find our HTML, we need to bind-mount a local directory to the /home/aftermath/input
directory in the container.
In our case, for compatibility with Github Pages, we've set our blog's publishDir
to /docs
. So, that's the folder that we want aftermath
to transform.
The resulting docker run
call looks like:
docker run --mount "type=bind,source=$(pwd)/docs,target=/home/aftermath/input" aftermath
When doing a production build of our blog, we could use the following script (call it build.sh
):
#!/usr/bin/env bash
set -e
hugo
docker run --mount "type=bind,source=$(pwd)/docs,target=/home/aftermath/input" aftermath
Although aftermath
is open-source, it's ultimately a personal project designed for my specific needs, published without any guarantees of behavior. If you want to use aftermath
for your blog, I recommend forking it and modifying the code as needed to suit your unique needs.
Although Hugo configuration and theming is powerful, it's not possible to use custom programs during execution. Something like aftermath
could be entirely implemented in a Hugo theme if there was some way to call external programs inside a theme (e.g. an exec
shortcode). Or plugins, external helpers, or something. The issue, though, is security: something like an exec
shortcode could be easily abused by a malicious theme.
A few related Hugo issues:
Because of the above, Hugo is limited in what functionality you can add without forking the Hugo codebase. There are also Hugo modules but I don't really understand those yet.
(Why didn't I fork the codebase? Maintaining an up-to-date fork of hugo
is more effort than I want to put in, but I also don't want to get stuck with an out-of-date fork either! I prefer to program against the generated HTML, which should be a consistent interface even for new hugo
versions.)
The two main third-party dependencies -- katex
and mermaid-cli
-- are both installed with npm
. KaTeX even has a Javascript API. So Node felt like the natural choice.
Also, I'm familiar with it, so that helps.
aftermath
is a simple project, but it has a fair number of dependencies, including the Mermaid CLI which pulls in a full headless Chrome. With Docker, the whole development environment becomes self-contained and consistent across systems.
I'm moving towards having all of my development environments Dockerized, actually. And since aftermath
isn't really a polished tool, I haven't taken the time to support a non-Dockerized version. (You can still run it without Docker, of course, it's just not tested/documented.)
aftermath
isn't suitable for use with Hugo's development server.
In a development setting, it's preferable to do the transformation client-side (in the browser), e.g. with:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/katex.min.css" integrity="sha384-vKruj+a13U8yHIkAyGgK1J3ArTLzrFGBbBc0tDp4ad/EyewESeXE/Iv67Aj8gKZ0" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/katex.min.js" integrity="sha384-PwRUT/YqbnEjkZO0zZxNqcxACrXe+j766U2amXcgMg5457rve2Y7I6ZJSm2A0mS4" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/contrib/auto-render.min.js" integrity="sha384-+VBxd3r6XgURycqtZ117nYw44OOcIax56Z4dCRWbxyPt0Koah1uHoK0o4+/RRE05" crossorigin="anonymous"
onload="renderMathInElement(document.body);"></script>
<script defer src="https://cdn.jsdelivr.net/npm/mermaid@9/dist/mermaid.esm.min.mjs" onload="mermaid.initialize({ startOnLoad: true });"></script>
I've tried to write aftermath
so the transformations it performs are exactly the same as the ones done dynamically by the above client-side JS.
Note that d2
doesn't have a client-side transformation option that I'm aware of, so d2
diagrams will just render as source code blocks when using hugo serve
.