Detailed Images, With No Shortcodes

If you remember when I first added the Medium Zoom library, I wasn’t using one of its features: the ability to load a larger image when the user clicks to zoom in, only when the user wants to zoom in. However, I added this later, and in that, the method I used was to use a Hugo shortcode to include the image with proper attributes in the <img> tag. Well as it turns out, I don’t need to do this. Now, I can have that happen automatically with standard Markdown ![image links]().

Understanding the Zoom

The only thing that you need to do to make this actually work is add a data-zoom-src attribute to the image tag, and the library will load that resource when it needs to. if you don’t have this attribute, or the resource fails to load, it’ll just fall back to expanding the original source instead. What this means is that I can, in theory, not really worry about if my markup tells it an image exists when it doesn’t — it’ll just get a 404 and proceed as if it didn’t exist in the first place. And as it turns out, Hugo supports a way to do this itself, I don’t need to add any JS that executes at load time or anything.

Understanding Hugo

Fundamentally, we’re ‘compiling’, if you will, Markdown text, into HTML. Really this process is also called ‘rendering’ by a lot of things, but regardless, it’s converting relatively easy-to-write Markdown, and turning it into a complete, browser-ready HTML document, with all the extra markup and structure that’s required. There’s multiple programs and libraries for this, but the one that (this version) of Hugo uses is Goldmark (Previous versions used one called Blackfriday). Goldmark has a few interesting features, one of which is that you can override certain outputs. Within a Hugo project, the folder layouts/_default/_markup/ can contains some overrides, either within your top-level, or for a certain theme. This is effectively, in hugo terms, a template.

As I explained in my site overview, Hugo works off a series of templates, which are effectively just HTML files with extra processing instructions that use it like, well… a template. A lot of things in Hugo take similar processing instructions, another of which is shortcodes, which I explained when adding detailed images to begin with. But, quick recap, a shortcode is a template file (kinda) that can be executed on-demand wherever I like in a file, including the outputs right there. This is useful for things like embedding videos, YouTube links in a mini-player, and a few other things like that. Well, shortcodes aside, it turns out I can override standard image markup with a file named render-image.html in that folder above. And in my case, it looks like this:

{{ if or (hasPrefix .Destination "https://teknikaldomain.me/cdn") (hasPrefix .Destination "/cdn") }}
<img class="lazyload" data-zoom-src="{{ .Destination | replaceRE "\\.(png|jpg)$" "_detailed.$1" }}" data-src="{{ .Destination }}" alt="{{ .Text }}" data-zoomable="true"/>
{{ else }}
<img class="lazyload" data-src="{{ .Destination }}" alt="{{ .Text }}" data-zoomable="true"/>
{{ end }}

Really, it’s simple. If an image’s source contains my CDN path, it’ll include the first <img> tag, and if it does not, it’ll include the second. if you want to understand that a bit better, then know that the variable .Destination contains the image source, and .Text is the descriptive text. Or in other words, ![.Text](.Destination). Everything else is the same, images are to be lazy-loaded (not loaded until they’re about to come into view), and are allowed to be zoomed. The only difference is that CDN images have the data-zoom-src attribute, and there’s some weird RegEx magic going on with its contents. This is because my naming convention is predictable, for some image foo.png, the full-size detailed will be foo_detailed.png. Because of this, I can strip off the ending and replace it with the _detailed suffix.1 Now at this point, just about all of the images that I put there are going to have detailed versions, but not all of them do, since I didn’t start adding them until a little while in. So for those cases, it’ll 404 on the detailed images, and just use the original one, as it would have anyways. Now I (to this point) haven’t used JPEGs, because even if they’re smaller, I prefer the lossless PNG compression, but I put it in the expression just in case. (The $1 in the second parameter means “the value of the first group” which is either “png” or “jpg”, so it properly handles both extensions.) Now to those that know me, you might be thinking “Well, what about WebP?” And yes, I do love the good size/quality ratio of WebP, but… that’s not handled here. Nothing is WebP by default. That’s what the job of the CDN worker and Cloudflare’s Polish is for in the first place!

Now instead of slightly-uglier shortcode syntax, I can use standard Markdown image syntax, and not only will most Markdown editors actually understand this now, but it also, to use the term, just works.


  1. If you’re curious as to how this works when the RegEx has a \\ in it, this is because hugo runs one round of escaping before passing it to Go’s RegEx parser, meaning the actual interpreted RegEx only has one \, properly escaping the .↩︎