How My Blog Handles Math and Images in HTML, Atom, and Email in 2025
I want to explain computer science on my blog, and show photos, and I want every article to look exquisite. My goal is to display math and images (especially SVGs) as beautifully as possible for people who read my articles on my site, and via an Atom feed and email. This is hard with today’s technology, plus I don’t have real frontend or design skills.
I’ve recently started a series of articles about epistemology and distributed systems, which includes diagrams and equations that broke my existing publication system. I came up with a new stack of kludges that works for me. I’ll write them down here so I remember, and perhaps you’ll learn something you can use.
Throughout this post, you can refer to this map:
My site generator #
I use Hugo. I learned about Hugo early, since Steve Francia was my boss at MongoDB when he created it, but I resisted using it for some time. It’s not as extensible as the Python-based static site generators, and it relies on Markdown, which is a hacky format. But the various Python generators were all much too slow for me, so eventually I switched. Hugo is super fast enough: even now that my site has 700 articles and thousands of images, it does a full rebuild in under a second.
Images and previews #
While I’m drafting an article, I run the Hugo server and preview the article locally. Whenever I save a change to the article, Hugo rebuilds it and auto-reloads it in the browser. For some reason, Hugo doesn’t properly reload when I change an image, even though it’s perfectly aware that the image has changed. I fixed this by creating this template:
{{/* This is layouts/_default/_markup/render-image.html */}}
{{- $img := .Page.Resources.GetMatch .Destination -}}
{{/* to make .IsBlock available see gohugo.io/render-hooks/images */}}
{{- if .IsBlock -}}<p>{{- end -}}
{{- if not $img -}}
<img src="{{ .Destination | safeURL }}" alt="{{ .Text }}" title="{{ .Title }}">
{{- else -}}
{{- $r := $img -}}
{{- if hugo.IsServer -}}
{{/* Make autoreload work for images with Hugo dev server */}}
{{- $r = $img | resources.Fingerprint "sha256" -}}
{{- end -}}
<img src="{{ $r.RelPermalink }}" alt="{{ .Text }}" title="{{ .Title }}">
{{- end -}}
{{- if .IsBlock -}}</p>{{- end -}}
This template interprets Markdown images, like 
. If it’s running inside Hugo’s development server, then it adds a cache-busting fingerprint to the image URL. Now whenever the contents of foo.jpg
change, Hugo creates a copy of the file with its hash included in the filename, like foo-xxx.jpg
. Hugo updates the HTML page to point to foo-xxx.jpg
, so my browser will definitely load the newest version of the image. When I change foo.jpg
again, its hash is now different, so it’s named like foo-yyy.jpg
, and the browser reloads it properly.
This shouldn’t be necessary—this is what ETags are for—but Hugo needs this kludge to properly refresh the preview when I change an image.
Publishing an article #
I manage a variety of tasks with some PyCharm custom shortcuts and an ever-growing Python script called blog
. Working together, PyCharm and my script can do some advanced tricks, like pasting an image from the macOS clipboard into a Markdown file, or creating a photo gallery, or starting up the Hugo development server and opening the browser to a preview of the draft I’m currently editing.
The blog
script works especially hard when I finish a draft and publish it. I’ll describe the publication pipeline below.
SVGs #
I love to draw diagrams in Excalidraw and export them as SVGs like this:
An intricate diagram. I drew this for an article about knowledge.
I’ve learned that mail readers (even web-based ones like GMail) won’t show SVGs, so I need to give them a fallback. Remember, the article pipeline is HTML → Atom → email, so my Atom feed must provide that fallback, which is then passed through Kit to email subscribers.
When I publish a draft, my Python script creates a PNG for every SVG in the article (or a JPEG, if the PNG is too many bytes). I have a template that converts every SVG in the feed to a <picture>
that prefers the SVG but falls back to the PNG or JPEG. This template is called from the template that generates the whole Atom feed.
Math #
A well-rendered formula is beautiful, like this Riemann sum:
I use both block formulas like the one above, and also inline formulas, like . By default, Hugo renders math with client-side Javascript called MathJax, but I want to show math on the web using the modern HTML <math>
tag instead. I override Hugo’s default with this template. Hugo requires it to be called render-passthrough.html
, but don’t be fooled: it’s not for a generic “passthrough” feature, it’s for rendering math specifically. The template also sets a variable hasMath
to true, so this other template knows to include special math CSS.
HTML <math>
tags may not display in Atom readers or email, so once again my Python publication script has a job to do. When I publish an article, the script searches it for formulae, and creates an SVG for each. The best way I found to do this was install the MathJax library on my laptop and execute it at build time to produce an SVG. Then my Python script produces a PNG for each formula’s SVG.
The filenames for the SVGs and PNGs include a hash of the formula itself. Imagine the “xxx” in the filenames above are 64-bit hashes. On the web, you see <math>
tags rendered by your browser, but in Atom (and therefore in email), you see the SVG, or fall back to the PNG. This template replaces <math>
tags with <picture>
tags in the Atom feed, using the hashes of the formulas to determine the correct image filenames.
Block and inline images #
I didn’t use many inline images until I started writing a lot of math formulae. But now, since inline formulae become inline images in Atom and email, I need inline images to render correctly. At the start of this journey I was using Mailchimp’s feed-to-email automation, and Mailchimp seems unable to show both inline and block images in emails from feeds. By default, inline images are okay, but block images expand to a monstrous width. If you select Mailchimp’s option “Resize RSS feed images to fit template”, then block images look good, but inline images are stupidly converted to blocks, which is abominable.
I switched from Mailchimp to Kit (formerly ConvertKit) for my feed-to-email automation. Kit worked perfectly, right away.
Conclusion #
The feeding machine in Charlie Chaplin’s “Modern Times”
What a tzimmis. What a kludge. Why is all this necessary in 2025? I guess the future is already here, it’s just not evenly distributed: feed readers are behind the curve, and email readers farther behind, because of security concerns or other issues I don’t understand. So I have to build a complicated pipeline to transform math into SVGs, and SVGs into other formats, to ensure graceful degradation. Furthermore, Hugo is fast but hard to customize, and it takes a precarious stack of templates to (usually) produce the HTML that I want, where I want it. These templates use regexes to parse HTML, which will inevitably break on some page someday.
I hope all this is useful to someone else. Maybe you don’t need these hacks. Maybe a basic Hugo setup works fine for your blog, or you don’t mind if your images don’t appear exactly right in all channels. Good for you. But there may come a time when you need more. I will friend you, if I may, in the dark and cloudy day.