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:
|
|
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:
|
|
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:
|
|
The next step was to adjust my config.toml
to tell it about the secondary theme component:
|
|
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:
|
|
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:
|
|
Tired: Define theme components like this
The equivalent configuration using Hugo modules apparently looks like this, while removing the theme =
line:
|
|
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:
|
|
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 wherever 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:
|
|
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.
-
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. ↩︎
-
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. ↩︎ -
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 thepackages.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. ↩︎ -
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).
🐇🕳️ ↩︎ -
It turns out naming things is really hard. Have you heard? 😱 ↩︎