Go big or Go home?

How some image-handling code moved from Eleventy to Hugo.

2021-02-10

General note: This site’s appearance, configuration, hosting, and other basic considerations will change over time. As a result, certain content on this page could be at variance with what you’re currently seeing on the site, but the two were consistent when this post originally appeared.

Note: After you read this post, please see also its sequel for what I believe is a much more usable result.

Imagine that you’re about to take your first drive as owner of a shiny new vehicle which you chose after weeks of research and comparison.

Now, imagine that you’ve bought that vehicle even though its audio system, while superb, has all of its controls in a language that you barely can read.

That’s somewhat analogous to where I found myself a few days ago, when I committed to returning this site to the Hugo static site generator (SSG). Why? Because, with that commitment, also came a commitment to learning more about the Go programming language on which Hugo itself is based.

I’d been spoiled by how the site’s former SSG, Eleventy, famously allows use of multiple languages in building the templates that an SSG uses to convert plain text into web pages like the one you’re reading now. Of course, having had the site on Hugo for nearly all of its first year of existence, I was fully aware of the need to accept Go-based templating once more upon the return.

Still, there’s a difference between accepting it and embracing it.

While transitioning the site from Eleventy back to Hugo, I’d cobbled together a really spaghetti-ish Go version of one particular bit of code on which the site has been depending for some months now. However, the result’s inelegance and un-DRY-ness embarrassed me.

To be specific: while the original JavaScript code looped nicely through an array to do its thing, the Go code was repeating each part verbatim — and only because, frankly, it worked and I didn’t want to fool with the looping process in Go.

Cowardly, I know, but that’s how it was.

Anyway: after completing and announcing the transition, I was determined to improve that code as much as my extremely limited abilities would allow. In doing so, I felt, I’d be making at least a fair try at beginning to embrace Go-based templating.

This is the story of what resulted from that effort. Perhaps it will be instructive to others considering converting to Hugo from other SSGs.

Get short(code)y

The subject of this post is a shortcode. The definition of shortcode varies widely but, in SSG-land in general and for both Hugo and Eleventy in particular, it refers to a macro that you can drop into a site’s Markdown so it’ll produce the same effect as if you’d put actual code in there.

The shortcode we’re discussing here, initially created in the Eleventy site as lazy-picture.js and now existing in the Hugo site as imgc.html, makes it easy for me to insert responsive images from the free account that I have with Cloudinary. I stress that this is for responsive images because, if all you want to do is insert an image, Markdown already allows that on its own; but responsive images need some fairly involved HTML and CSS.

For example, I can insert all the code required for a responsive display of the following image . . .

Partially opened MacBook Pro laptop

Image: Apple, Inc.

. . . by inserting this shortcode in my Eleventy repo:

{% lazypicture "Apple_new-macbookpro-wallpaper-screen_11102020_1984x1118.jpg", "Partially opened MacBook Pro laptop", 1984, 1118 %}

. . . and this shortcode1 in the Hugo repo:

{{< imgc src="Apple_new-macbookpro-wallpaper-screen_11102020_1984x1118.jpg" alt="Partially opened MacBook Pro laptop" width="1984" height="1118" >}}

Here’s what each shortcode does:

  • Adds the necessary Cloudinary URL to the provided image file name.
  • Adds the provided ALT tag.
  • Sets the image’s correct aspect ratio based on the provided width and height.
  • Using all the above, pumps out all the HTML and CSS to make it look the way it should in your browser. (To see the resulting code, use your browser’s inspector tool on the image above.)

So you can assess the conversion required between the Eleventy and Hugo versions of the shortcode, I’ll provide each for you to see.

First, the JavaScript

Here’s the JavaScript version, on which the Go version was based2:

imgc.js

const respSizes = require(`../../../_data/siteparams.json`).respSizes
var cloudiBase = 'https://res.cloudinary.com/brycewray-com/image/upload/'
var xFmPart1 = 'f_auto,q_auto:eco,w_'
var xFmPart2 = ',x_0,z_1/' // note ending slash

module.exports = (url, alt, width, height) => {
	divClass = `relative`
	imgClass = `containedImage`
	nscClass = `containedImage`
	dataSzes = `(min-width: 1024px) 100vw, 50vw`

	var separator = ', '

	var stringtoRet = ``
	stringtoRet = `<div class="${divClass}">
	<img class="${imgClass}" data-src="${cloudiBase + xFmPart1 + "600" + xFmPart2 + url}" data-srcset="`
	respSizes.forEach(size => {
		if (size <= width) {
			stringtoRet += `${cloudiBase + xFmPart1 + size + xFmPart2 + url} ${size}w`
			stringtoRet += separator
		}
	})
	stringtoRet = stringtoRet.substring(0, stringtoRet.length - 2)
	stringtoRet += `" alt="${alt}" width="${width}" height="${height}" loading="lazy" sizes="${dataSzes}" />
	<noscript>
		<img class="${nscClass}" src="${cloudiBase + xFmPart1 + "300" + xFmPart2 + url}" alt="${alt}" />
	</noscript>
	</div>`

	return stringtoRet
}

Then, the Go

Here’s the corresponding Go version for Hugo:

imgc.html

{{/* init vars */}}
{{- $respSizes := slice "300" "450" "600" "750" "900" "1050" "1200" "1350" "1500" -}}
{{- $src := .Get "src" -}}
{{- $alt := .Get "alt" -}}
{{- $width := .Get "width" -}}
{{- $height := .Get "height" -}}

{{/*
	separating the Cloudinary-related vars for
	greater flexibility, especially in case
	somebody else wants to borrow this code
	for their own Cloudinary setup and
	transformation ("xFm") choices
*/}}
{{- $cloudiBase := "https://res.cloudinary.com/brycewray-com/image/upload/" -}}
{{- $xFmPart1 := "f_auto,q_auto:eco,w_" -}}
{{- $xFmPart2 := ",x_0,z_1/" -}}

{{/* Some of these vars seem pointless, but am keeping in case I ever decide to use other kinds of images again. */}}
{{- $divClass := "relative" -}}
{{- $imgClass := "containedImage" -}}
{{- $nscClass := "containedImage" -}}
{{- $dataSzes := "(min-width: 1024px) 100vw, 50vw" -}}
{{- $stringtoRet := "" -}}{{/* init */}}
{{- $separator := ", " -}}
{{- $innerString := "" -}}{{/* init */}}

{{- $stringtoRet := printf "%s%s%s%s%s%s%s%s%s%s%s" "<div class='" $divClass "'><img class='" $imgClass "' src='" $cloudiBase $xFmPart1 "600" $xFmPart2 $src "' srcset='" -}}
{{- $.Scratch.Set "innerString" $stringtoRet -}}
{{- range $respSizes -}}
	{{- if ge $width . -}}
		{{- $innerString := printf "%s%s%s%s%s%s%s%s%s%s" $innerString $cloudiBase $xFmPart1 . $xFmPart2 $src " " . "w" $separator -}}
		{{- $.Scratch.Add "innerString" $innerString }}
	{{- end -}}
{{- end -}}
{{- $stringtoRet := .Scratch.Get "innerString" }}
{{- $stringtoRet := substr $stringtoRet 0 -2 -}}
{{- $stringtoRet := printf "%s%s%s%s%s%s%s%s%s%s" $stringtoRet "' alt='" $alt "' width='" $width "' height='" $height "' loading='lazy' sizes='" $dataSzes "' />" -}}
{{- $stringtoRet := printf "%s%s%s%s%s%s%s%s%s%s%s%s" $stringtoRet "<noscript><img class='" $nscClass "' src='" $cloudiBase $xFmPart1 "300" $xFmPart2 $src "' alt='" $alt "' /></noscript></div>" -}}

{{- $stringtoRet | safeHTML -}}

And, yes: further down, I am going to try to explain all that .Scratch stuff — although I must also point you to what pretty much is .Scratch canon, namely Régis Philibert’s article about the subject, “Hugo .Scratch explained.” How canonical is it? When you go to Hugo’s own documentation about .Scratch, it points you to that Philibert article so you can get “a detailed analysis”!

I’ll also explain all those %s items and a few other seeming oddities.

Same and different

Now that you’ve seen both, let’s compare/contrast how they work.

  • Each accesses four values for the image that I supply when using the shortcode in Markdown:
    • The image file name, either url (JS) or $src (Go).
    • The ALT content, either alt (JS) or $alt (Go).
    • The image’s width in pixels, either width (JS) or $width (Go).
    • The image’s height in pixels, either height (JS) or $height (Go).
  • Each uses a variable, either stringtoRet (JS) or $stringtoRet (Go), into which it then collects the necessary HTML and CSS; and, at the end, returns that variable’s contents to the web page.
  • Each stores the desired image display sizes for the srcset in either an array called respSizes (JS) or a slice called $RespSizes (Go).3
  • After providing the opening HTML/CSS to the variable, each shortcode loops through the array or slice and fills in the necessary per-size segments. The looping in JS is done with forEach and in Go with range. Immediately after each loop ends, a substring or substr statement chops off the last two characters of the result, so there’s not a hanging comma-and-space combination after the last item.
  • At the end, each finishes filling the variable by adding the closing HTML/CSS, including noscript material for those browsers where JS has been disabled. The latter dates back to when the shortcodes were handling lazy-loading via JS added to each web page; while that’s no longer true, I kept the noscript items just in case I decide to revert to that lazy-loading method for some reason.

Passing Go

Now, as promised above, I’ll take you through some of the Go-isms in this shortcode, since they probably look strange if you’re new to Go — and perhaps even if you’re not.

The Dot

You’ll notice a little dot (.) here and there in the Go shortcode, especially in the loop. If you’re familiar with other programming languages, you might mistake that for a concatenation operator; but, in Go-based templating, the dot provides context regarding the item to which you’re referring at any given time. For example, this part of the loop through the $respSizes slice . . .

{{- if ge $width . -}}

. . . means: “if this number (the context at this point in the loop) that I’m currently pulling from $respSizes is greater than or equal to (ge in Go) the image’s $width as supplied by the Markdown.”

Once again, Régis Philibert is the Go-to guy, so to speak, when it comes to explaining Hugo’s reliance on “The Dot” via his article, “Hugo, the scope, the context and the dot.”

.Scratch

Now, let’s deal with .Scratch. I reiterate that there are others, particularly Philibert, who explain it far better than I’m about to do, but here’s my quick-and-dirty attempt.

The name .Scratch comes from the concept of a “scratchpad” on which you can save notes while you’re otherwise busy. .Scratch is something the Hugo devs cooked up years ago to get the templating process past the difficulties caused by an intentional characteristic of the Go language. You see, Go is a lot pickier than many other languages about the scope of a variable. One of those cases where that bites you in Hugo templating is when you’re trying to get at a variable as modified within a slice (again, assume a slice is about the same as an array).

While the JS version happily adds to stringtoRet as it loops through respSizes and then keeps adding to it after the loop, the Go version can’t do that to $stringtoRet while looping through $respSizesunless we use .Scratch to create $innerString, so it can “hold” the result of that loop and then, after the loop is done, add it to $stringtoRet.

In the case of this shortcode, not using .Scratch in this fashion would result in an empty srcset and, thus, display only a small, fuzzy fallback image rather than the full responsive image srcset.

printf and %s

As for the printf stuff and all those %s items: printf is the recommended way to build a Hugo variable through concatenation. Each separate string or variable so concatenated requires its own iteration of the %s “verb,” instructing that particular printf to handle the string or variable as a string. (Go has lots of other such “verbs.”) If you don’t use the right number of %s “verbs” for each printf, the $stringtoRet result has %!(EXTRA string-kinda error messages in it, FUBARing the resulting HTML/CSS.

Hyphens and curly brackets

Finally, those hyphens connected to many of the curly brackets ({{- and -}}) eliminate extraneous white space in the resulting HTML. To be sure, the --minify flag in my site’s hugo build command gets rid of that space, too, but I just personally hate to see it when I’m in dev mode. However, if you’re not as picky about the appearance of your resulting HTML source code in dev mode, plain ol’ {{ and }} do the job just fine. (Where a set of curly brackets has only one hyphen rather than both, that’s because I deemed it necessary to dump only some of the white space it involved.)

Twisted, mister

During an email exchange with one of my readers not long after I announced the site’s return to Hugo, he — a recent convert to Eleventy from other, more “opinionated” JS-based SSGs — remarked how much he was enjoying the relative ease of templating in Eleventy. Only minutes after finishing the Go shortcode we’ve covered herein, I replied:

As for templating: I just spent three hours whipping a Hugo shortcode into order so, yes, I do miss the simplicity of [templating] in Eleventy.  . . . Anyway, there is kind of a twisted logic in how Go works, and maybe I’m just twisted enough to get it eventually.

Although there’s never been any mystery about whether I’m twisted, it remains to be seen how well I’ll get Go-based templating in Hugo.

However, I do like how this shortcode came out.4 I’ll never be an expert in Hugo templates by any means; but, at least, imgc.html works, it’s as DRY as I feel it needs to be, and I was sufficiently pleased — or maybe I should say, I was not sufficiently embarrassed — that I wrote this post about it.

Perhaps I’ll get lucky and, twisted or not, won’t find myself regretting that “not sufficiently embarrassed” part.


  1. If you happen upon this site’s repo out of curiosity and check out this post’s Markdown file, you’ll notice that this text’s bounding {{ and }} also have wrapping /* and */, respectively. That’s because, otherwise, Hugo sees it as real code, not just a representation of it, and acts accordingly — in this case, once again displaying the image. I found this otherwise undocumented workaround in a 2015 comment on the Hugo Discourse forum. (Update from the future: Later, the workaround did become part of the documentation.) ↩︎

  2. This is a revised version because the original JS provides for an LQIP-using preview, the need for which ended when I removed hero images). ↩︎

  3. The JS fills respSizes by pulling from a site parameters file in the Eleventy repo’s site-wide _data directory. The Go fills $RespSizes from this code, but I could easily have brought in the values from the site-wide config.yaml configuration file. I wrote the JS version of this particular part as I did because, early on, I was frequently experimenting with different values and felt it easier to go this route. By the time I got to the Go version, I had settled on these values and felt no more need to separate their entry in this way. ↩︎

  4. Based on what I’d learned in this process, I also fixed/DRY-ed a very similar shortcode, twitscrn.html, that I formerly used for bringing in Twitter tweets’ screen captures as per the site’s privacy policy, prior to later reverting to bringing them only as static text. ↩︎

NEXT   

PREVIOUS