Hugo Theme Components, Modules, and All I Wanted Was Some Shortcodes

Fun With Shortcodes

I had been playing around with making some custom Hugo shortcodes to fill some gaps as far as functionality provided by Hugo’s existing (built-in) shortcodes was concerned. Others were for my own convenience to wrap frequently used elements, and some were just for fun as a learning exercise (and let’s be honest, usually all of the above combined).

On the “filling a gap” side, there’s my shortcode to embed videos. It’s not perfect, and I already found a smarter one in the wild, but it does its job and works for me:

1
{{< videofig mp4="my-file.mp4" loop=true autoplay=true caption="A catchy caption" >}}

This wraps a <video> element inside a <figure> element inluding a <figcaption>, as you might have seen in action already in a previous post. The output HTML looks roughly like this:

1
2
3
4
5
6
7
8
9
<figure>
  <video loop autoplay muted controls>
    <source src="my-file.mp4" type="video/mp4">
    Your browser does not support the video tag.
  </video>
  <figcaption>
    <p>A catchy caption</p>
  </figcaption>
</figure>

I generally like the added caption over a plain <video> tag, and since I learned that the <figure> tag is meant to hold all kinds of content including <pre> (for code), <video> and <picture>, I also added a shortcode to wrap highlighted code in <figure>, and a generalized shortcode to wrap anything inside <figure> with a caption. Once I copypasted Hugo’s embedded figure.html shortcode, the floodgates were open on my shortcoding and <figure>-wrapping.

A more complex thing I’m playing around with is a shortcode to mention R packages in text. You might have seen R packages referred to something like this: {ggplot2}. That’s a package name wrapped inside { } for whatever reason, and inside ` ` for the monospaced formatting. And I haven’t even linked the package to its website!
That’s a lot of work for just one package mention. Wouldn’t it be much easier to just type {{< pkg "ggplot2" >}}?

…What do you mean “no it wouldn’t, that’s worse”?

Well anyway, now I did it. Next I thought “wouldn’t it be cool if this was smarter” and, well, justified its syntactic overhead?
As it turned out, my previous ideas regarding package taxonomies have since lead to the realization that this is probably much better handled via Hugo’s data templates, and once you have some R package metadata lying around, that shortcode suddenly has a lot more potential.

The gist is this: Create a file named /data/packages.yaml (could also be .json), fill it with package metadata, and now you have access to said data in layout templates and shortcodes via .Site.Data.packages.
What is this for? Well, the current iteration of that pkg shortcode looks like in this:

Did you hear about {ggplot2} Create Elegant Data Visualisations Using the Grammar of Graphics ? It’s a neat package and has a fancy website. I also like {ggrepel} Automatically Position Non-Overlapping Text Labels with 'ggplot2' , which also has a fancy website but my shortcode hasn’t figured that out yet. Then there’s my own package, {tRakt} Get Data from 'trakt.tv' , which is not on CRAN so it gets a different icon 1.

All of them have a hover-tooltip with the package’s Title: from their DESCRIPTION file — which probably doesn’t work right on mobile. But nobody uses mobile devices these days anyway and this wasn’t a totally pointless feature to waste a night over because I couldn’t get the CSS positioning right, …right?
Please validate my bad life choices.
Thanks.

For Posterity

Depending on when you’re reading this, these examples either don’t work anymore, or they look completely different because I’ve changed my mind and/or learned a lot since I wrote this initially, and the shortcode has changed accordingly.
That’s the blog-post equivalent of a live demo, sorry.
You can check out the shortcode as of right now here.

This shortcode relies on the existence of packages.yml. I generated this from the packages’ DESCRIPTION files installed in my blog’s {renv} Project Environments -library, available.packages() for CRAN urls, and this result of a wasted evening. There’s probably better solutions available as Maëlle suggested 2, but I just wanted to get started with something relatively simple — after all, I was primarly after three things:

  • The package’s name (surprisingly)
  • A CRAN url, also serving as a (CRAN | Not CRAN)-indicator
  • A GitHub / source repository URL

Additionally, I haven’t found a good method of determining which package has a dedicated documentation website, e.g. via {pkgdown}, because preferably I’d like to make the package name link to its website and the associated icon either link to CRAN or to its source / GitHub repository. Guess there’s a lot of room for improvement 3.

But I digress, I wanted to talk about shortcode externalization.
Focus.

Anyway, with some new shortcodes in hand, I wondered how I’d get them to be usable with another Hugo site without having to copypaste them over. That’s when, once again, Maëlle literally pushed me into another rabbit hole about Hugo modules and theme components 4.

I hesitated to get into Hugo modules (built upon Go modules) at first because neither these Go modules docs nor this Go modules wiki was particularly easy to skim through, given my only contact with Go had been the second syllable in my chosen static site generator.

Theme Components and git Submodules

It turned out that theme components are a pretty nifty Hugo feature — you can basically mix and match various themes and theme-like components as you please, as long as you keep precedence in mind. That’s the perfect solution for a repository that only contains shortcodes, for example.

The first step was to remove my shortcodes from my site’s /layouts/shortcodes and place them into their own cozy little repository at jemus42/jemsugo 5. Note the file structure: They still live in /layouts/shortcodes so Hugo merges them correctly with other theme components.

Once that was done, I could add this new repository as a secondary git submodule in my site’s /theme/ directory, where you’d usually only find, well, your theme:

1
2
# Adding a git submodule
git submodule add https://github.com/jemus42/jemsugo.git themes/jemsugo

The next step was to adjust my config.toml to tell it about the secondary theme component:

1
2
3
4
# Before:
theme = "hugo-coder"
# After:
theme = ["jemsugo", "hugo-coder"]

Note that now the theme key (or whatever they’re called in TOML) is not a single string anymore, but an… array? Again, whatever they’re called in TOML. This is cool from Hugo’s side, but {blogdown} Create Blogs and Websites with R Markdown doesn’t seem to like it, at least blogdown::serve_site and blogdown::build_site(..., run_hugo = TRUE) seem to not expect this being a multi-valued element.

Besides that, everything seemed to work fine though. You can control the precedence of theme components by adjusting the order in which they appear in the theme setting, so in this case my jemsugo components take precedence over everything in hugo-coder — which in this case does not matter at all, as hugo-coder doesn’t provide any shortcodes. If there was a videofig shortcode in Coder though, it would be ovewritten by my own.

To update your git submodules, you can run git submodule update --rebase --remote.
Which is something that I did quite a lot, because once I externalized my shortcodes, I’d have to push the shortcode repository to GitHub and pull it from my blog’s repository locally every time I wanted to preview some changes.
That’s not a terribly nice workflow, and I assume the git people will know a better solution — but the point was moot as soon as I realized that Hugo modules were only a small step away, and not that hard to get into.

Switching to Hugo Modules

Why Hugo modules though? Didn’t git submodules work just fine?
Yes. Yes they worked just fine, and this just fine included updating submodules via git submodule update --rebase --remote after I first ran git push on the repository that contained my shortcodes.
What Hugo modules allow is to just run hugo mod get -u in my blog repo after I ran git push in — wait a minuute that’s not better!

Okay, there is a decent workaround to ease local testing with modules, but first, let’s walk through the steps to use Hugo modules instead of theme components + git submodules.

After skimming this helpful post on the Hugo forums, I realized that the thing I wanted to use as a module didn’t even need special configuration, meaning I didn’t need to run hugo mod init in my jemsugo repo (apparently), and I didn’t need my theme to be specially configured as well.
All it took (I think), was to declare my blog itself a Hugo module by running this in its root directory:

1
hugo mod init github.com/rbind/blog.jemu.name

Substitute the repo spec accordingly (or maybe remove it? I’m not sure)

This creates a go.mod (and later a go.sum) file in the site’s root that lists the modules you’re using, once you have declared any in your config.toml.

Here’s my config.toml for the theme component configuration from previously:

1
theme = ["jemsugo", "hugo-coder"]

Tired: Define theme components like this

The equivalent configuration using Hugo modules apparently looks like this, while removing the theme = line:

1
2
3
4
5
[module]
  [[module.imports]]
    path = "github.com/jemus42/jemsugo"
  [[module.imports]]
    path = "github.com/luizdepra/hugo-coder"

Wired: Using Hugo modules like that

After running hugo mod get -u and/or a quick hugo server for testing, the go.mod in my blog’s root now looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
module github.com/rbind/blog.jemu.name

go 1.14

// For local testing
replace github.com/jemus42/jemsugo => /Users/Lukas/repos/github/jemus42/jemsugo

require (
	github.com/jemus42/jemsugo v0.0.0-20200524200711-6fca0e2b9d66 // indirect
	github.com/luizdepra/hugo-coder v0.0.0-20200521121849-ff8d5364ad00 // indirect
)

Satisfyingly self-explanatory

That replace line is used, as the comment suggests, for local testing. It’s mentioned in the Hugo docs, but without much info about what it really does or if its placement in go.mod matters. Thankfully this blog post was helpful to get the gist, and I think it now works as expected.
And it’s the “local testing” thing that really makes the difference in workflows compared to submodules: I can tweak my shortcodes in their local folder outside the blog repo, and when I save changes the hugo server running in my blog repo automatically picks them up. It’s almost as if this is the way it’s supposed to work in the first place!
…And what I already had when I still had the shortcodes in my blog rather then external, so… yeah.
But external though!

From local testing to deployment

Before you deploy your site by pushing to whereever your site is built from (like Netlify), you’ll have to comment out that replace line in go.mod again, because Netlify won’t know that local path of yours.

I have now deleted my themes directory, ran git submodule deinit on both submodules, and it still works — even on netlify! So I’m reasonably confident that yes, this modules thing… it might actually not have been that complicated?
Just like that?

I’m not sure how to handle the precedence thing though, so what if I wanted to make sure some shortcode in jemus42/jemsugo takes precedence over a shortcode with the same name in a different module — I assume there’s a solution for that, but I’ll look into that some more once I actually have the need for it.
In any case, according to bep, modules are here to stay and the theme = thing is left for compatibility, so I doubt there’s something modules can’t do that was possible before — they already have a much greater set of features.

Conclusion

This whole shift in dependency workflows is still pretty new to me, and I mainly discovered my way through it while I was still writing this post.

I haven’t gathered a lot of experience with the Hugo modules approach yet, and there might be a case or two in which I wish I’d still be using the original approach (using my theme as a git submodule and my shortcodes in my site).
I guess the worst thing that could happen would be learning more about how Go works, especially with regards to module caching (where are they even stored?) and versioning, or whatever that _vendor thing is all about.

Well, I’ll see how it *clear’s throat* … Goes.

Addendum Re: Module Location

Turns out hugo modules are by default stored in your /tmp/ (or equivalent), and you can use the environment variable HUGO_CACHEDIR, which I’ve now set as follows:

1
export HUGO_CACHEDIR=$HOME/.local/share/

And as far as the local-testing vs. deployment goes, I guess that’s what vendoring is for? If you hugo mod vendor your modules, hugo will look for them in _vendor first, so you could then probably use that fact and the --ignoreVendor flag for local testing?
Needs more playing around with.


  1. I should note that I wouldn’t be using so many shortcodes if it wasn’t for Alfred‘s snippet functionality. Seriously, give whatever snippet tool you have access to a go. It’s great. ↩︎

  2. The output of codemetar is a lot more complex and takes a moment to generate per package, so it’s probably not feasible if I want to generate metadata for a lot of packages maybe? But it’s cool for what it does — I’d just need this in one big file for all packages and also some non-CRAN packages form I think. ↩︎

  3. Wonder why I didn’t use the shortcode to refer to pkgdown? Well that package isn’t installed in my blog’s project library, so it’s not in the packages.yaml (because I didn’t want one file for all packages ever (yet)), and… well, I have since updated that file from all packages installed on my local machine, but left this note here as a reminder about the limitations of this approach. ↩︎

  4. I kid, of course. I’m starting to like the dynamic where I have a half-baked idea and she throws enough ideas and suggestions my way to actually make them work (kind of).
    🐇🕳️ ↩︎

  5. It turns out naming things is really hard. Have you heard? 😱 ↩︎