Doing What Markdown Can't: Specifying Image Width and Height

By default, the Markdown renderer in Hugo, at this point in time, is Goldmark, a CommonMark compliant renderer. CommonMark makes no provisions for manually specifying the width or height of an image. There’s various extensions, like those for Pandoc, Kramdown, and GFM, but Goldmark doesn’t support those. Google is getting a little cranky with the amount of CLS on some pages, especially on mobile, so it’s a good idea for me to start specifying sizes for most images. How should I do this? By hacking it on as a feature that is in no way the intended use for anything involved.

This was done almost immediately after Integrating Medium Zoom detailed images with Markdown, and given that I expect these two to come out relatively close to each other, I’m not going to explain everything that’s happening on the Hugo side, you can check that article out for the details of how Hugo works, templates, markdown overrides, and the like.

Why?

Three words: Cumulative Layout Shift. Also known as “the amount that parts on your page shift around as content loads in” You know that thing where you’re scrolling through something, and it keeps jumping around because stuff (likely, ads) is loading in? That, or right as you’re about to click a link, something loads in and you end up going to something else that you never wanted instead? (Again, probably an ad, I’m seeing a pattern…)

That is an example of bad layout shift. And Google, unsurprisingly, tracks this. Also unsurprisingly, this affects mobile devices a lot more, since with smaller screens, it doesn’t take much to make your entire page start jumping around. Well for some posts I have, the amount of images is large enough that you can get some bad CLS, especially with lazy loading, meaning every time you scroll far enough for a new image to load, you get another sudden content shift.

For a while I had kinda kicked the can down the road, seeing as I knew no way to manually specify the required attributes for images within the Markdown that Hugo understands, and just have to keep it the way it is. I finally realized (for some unknown reason) that if Markdown doesn’t support it by itself, then I’ve got to make it support that.

Important Image Attributes

Every <img> tag within a webpage has a few attributes that are, well, pretty important. src is the big one, which has the actual resource to load, but there’s also others, like alt (text to show when image doesn’t load, also useful for screen readers), title (text to show when mouse is hovered over the image in a tooltip), and… width and height. These specify the… width and height of the image, in pixels. You don’t actually need these two, without specifying them, an image will either be its full, native resolution if allowed, or evenly scaled down if it’s inside some other fixed-size container that’s smaller. For example, a 2000px wide image would take the full width of the 1000px content area here (the white part where text is, in this theme), but no more, having been scaled by an even 50%. with these two, the image element will take up exactly that many pixels for content1, and no more, meaning that uneven scaling can be applied.

The problem with not specifying width and height is that the browser doesn’t know the width and height until the image loads, meaning the size that it uses for the placeholder can be… anything, but it’s likely going to be smaller than the image itself. Which means once the image loads, it has to recalculate everything, and stuff moves. If you do specify these, then even an unloaded placeholder has the same size as the final image, meaning that there should be 0 layout shift when it loads, since it can just draw the image in the pre-spaced blank area that it left.

Hacking the Sys- Features

So I already know that most extensions for this don’t work. Meaning I’m working with the base Markdown standard for images. Really, that means I have three parts to work with:

![Alt text](URL "Title")

I can cram this into the alternate text, the image URL itself, or the image title. Now for this, I want the ability to specify the width and height independently, meaning that I can specify either, and not the other, and it’ll only insert the one relevant attribute.

So with this knowledge, I really don’t want to meddle with the alt text. I’d rather just have it the way it is, a description of the image itself. I don’t think that adding this into the title text would be good either, since I’d need to come up with some method of delimiting the values, like "Title;x=10;y=5" that can be individually (not) included, even supporting image dimensions but no title. That would be a lot of work, which leaves me with one available option: Put this in the URL. Sounds impossible, yes? Well, sorry to say, but that’s exactly what I did.

Adding Data in the URL

As to how, well I used the one part of a URL that is likely never to be used for an image: the fragment. In URL terms, a fragment is an identifier, separated from the rest of the URL by a # character, that specifies some named location to automatically scroll to.

For example, the link https://tekdmn.me/code/markdown-image-sizes#adding-data-in-the-url will load this page, then automatically scroll down to this heading. Fragments are only used client-side, they’re not sent to the server, and they have no use on images. Images will have no ids or names to scroll to, meaning that the browser will just… ignore it. And when embedding, as far as I know, it’s just straight up ignored, regardless. What this means is that, conveniently, a URL has one piece that isn’t used that I can take advantage of, imagine that.

If I wanted to take an image, say, https://example.com/kitten.png, I can add any fragment to that URL without changing the embedded image. Thus, I can encode size information, like this: https://example.com/kitten.png#500x500. The URL is required, meaning that I don’t have to handle the case where it doesn’t exist, and I can split on that x character, independently adding “if this exists, put it in the attribute.” The end result is this file:

{{ $imgSizeFragment := (urls.Parse .Destination).Fragment }}
{{ $imgSizeX := index (split $imgSizeFragment "x") 0 }}
{{ $imgSizeY := index (split $imgSizeFragment "x") 1 }}
{{ if or (hasPrefix .Destination "https://teknikaldomain.me/cdn") (hasPrefix .Destination "/cdn") }}
<img class="lazyload" data-zoom-src="{{ .Destination | replaceRE "\\.(?:png|jpg)$" "" }}_detailed.png" data-src="{{ .Destination }}" alt="{{ .Text }}" title="{{ .Title }}" {{ with $imgSizeX }}width="{{ . }}"{{ end }} {{ with $imgSizeY }}height="{{ . }}"{{ end }} data-zoomable="true"/>
{{ else }}
<img class="lazyload" data-src="{{ .Destination }}" alt="{{ .Text }}" title="{{ .Title }}" {{ with $imgSizeX }}width="{{ . }}"{{ end }} {{ with $imgSizeY }}height="{{ . }}"{{ end }} data-zoomable="true"/>
{{ end }}

Because the way that Go HTML template parsing works, that actually works, and doesn’t throw some “index out of bounds” error when attempting to index a nonexistent index. I’m not complaining, that makes my handling a lot simpler.

What this all means is that for a second time in one week, I have crammed features into standard markdown image markup using Hugo’s rendering overrides, first to add CDN detailed images, and second now, to specify width and height attributes.


  1. I’m referring specifically to the content area of the CSS box model, I’m not referring to the padding, border, or margin areas. Additionally, my previous statement only applies if there’s no other CSS on the image itself modifying it. ↩︎