The (Hu)go template syntax is bad.

There is no two ways about it. It’s just bad and often counterintuitive. It’s hard to read, hard to write, hard to debug, hard to fill in blanks. Easy things should be easy and hard things should be possible. I feel like everything is just hard with it - not impossible per se, but often harder then it should be.

I mean just look at this one line:

{{ $paginator := .Paginate (where .Site.Pages.ByDate.Reverse "Type" "post") (index .Site.Params "archive-paginate" | default 25) }}

This does/means the following:

  • $paginator - that’s a custom variable, it needs to be prefixed with a $
  • := - you need to use this operator to declare a variable and assign a value. But later it’s fine to just use = to reassign it.
  • . - “the dot”. It holds the context, which is basically a kind of scope. Why the scope is not implied? I don’t know. I guess if you’d have a this keyword, you’d end up writing this.whatever… and you would still need to differentiate globals and in-scope vars, so I guess that’s better?
  • .Paginate - Inside “the dot” there exists a Paginate function which has to be upper case because that is the way to make a function visible outside of its “package” in go. If you look at the list of available functions in hugo, you will know that a function was exported, but not why. Also a lot of functions were not exported, but you also do not know why. I assume that the lower case functions are all part of hugo’s templating standard lib. But it’s not explained what is going on in the docs explicitly.
  • - A space (yup). Functions and parameters are separated by spaces, so this space indicates the start of the Paginate function call (And not the parentheses in front of the where, as one might think when coming from other languages).
  • ( - The start of the parentheses denotes a nested expression.
  • where - that’s the where function which takes an array and compares a value at a given key using an operator (equals is implied and can be omitted like has been done here) to a value and only keeps the elements that pass the test.
  • .Site.Pages.ByDate.Reverse - The .Site.Pages part is an array of all pages of the blog. The ByDate.Reverse part is used (and available only) on list pages - another detail you’ll have to know - that is pages that have other pages under them in the file hierarchy. AND that the homepage is a special kind of list page. This is a snippet from the home page, so you can use it here to change the order of the retrieved array. Why you can’t retrieve pages in similar manner on non list pages is unclear to me.
  • "Type" - This is the key parameter of the where function. The key is “Type” here. Why is Type capitalized? Maybe it has to because of exports? Maybe it’s some other reason that I don’t know. In any case it is part of the keys of the element that is kept (or discarded). For the pages array an element includes things like a Permalink, a published Date and a also a Type. How do you know what an element includes? Well, it’s hard to say, because the proposed solution to use something like &#123;&#123; printf "%#v" . }} within a range function call that references the pages from the Paginator above only prints garbage. An actual solution is to use <pre>&#123;&#123; . | jsonify (dict "indent" " ") }}</pre> which gives a pretty printed JSON representation of all the available properties within a given context. But you’ll not find this solution in the docs, you have to get lucky and find it in the forum or Stack Overflow.
  • "post" - this is the match parameter of the where function. In other words the value found for the key “Type” must match this value in order to be included in the filtered array.
  • ) ( - we finish one nested expression - which is parameter one of our .Paginate call (the array to paginate) and start another nested expression - parameter two (the page size) of .Paginate
  • index .Site.Params "archive-paginate" - this returns the value at index or key n of a given array. So in this case the value at .SiteParams.archive-paginate
  • | default 25) if index does not return a value the default function can take over and return a default value. I don’t know why this has to be a function. I also find the whole part index .Site.Params "archive-paginate" | default 25 difficult to parse: Is the pipe still part of the index function call? You’ll have to know what pipes are.

Finally, we have parsed the whole thing:

instantiate customVar = 
functionCallThatReturnsAPaginator(
    functionCallThatReturnsAFilteredArray,
    (
        functionCallThatReturnsANumber ||
        functionCallThatReturnsaAdefaultValue
    )
)

And .Paginator itself is just an object that makes it easier to refer to and implement a paged navigation for an array of given elements (posts).

Apart from the unusual syntax I find (Hu)go templates hard to parse, even if I grok them somewhat. (Hu)go’s way to write function expressions, namely using spaces instead of parentheses and commas make the whole line harder to read than it needs to be. Compare:

{{ $paginator := .Paginate (where .Site.Pages.ByDate.Reverse "Type" "post") (index .Site.Params "archive-paginate" | default 25) }}

vs.

{{$paginator := .Paginate(where(.Site.Pages.ByDate.Reverse,"Type","post"),(index(.Site.Params,"archive-paginate") | default(25)))}}

Granted, this is still hard to read, because a lot is happening in this one line of code, but still: I’d argue it’s much easier to parse, because opening and closing parentheses and commas carry much more information than a simple space could. Spaces are also commonly used to align or balance things as has happened around the pipe char. Does this carry semantic meaning? Nope, not in this case!

A visualization of what different symbols of the templating syntax mean in our example. It turns out that the space char carries three differnt meanings: an aesthtic space, a start of a parameter list, a delimiter between parameters.

The space is doing an enormous amount of overtime here and I have yet to see a good justification of muddling the waters like this. The only reason I could see is that you have to balance parentheses, meaning you’re ending up with the line ending in ))). The best part is that you still need parens in any case, you just have to put them around the whole function expression! The real template version safes you two parentheses for the price of a parsing headache. I feel like that’s not worth it.

Let’s move on: It should be super easy to limit the list of pages to only include a certain category of posts, right? Would this have been what you’d come up with on the first try?

{{ $allPosts := where .Site.Pages.ByDate.Reverse "Type" "post" }}
{{ $allDailyDogos := where .Site.Pages "Params.categories" "intersect" (slice "DailyDogo") }}
{{ $onlyDogos := intersect $allPosts $allDailyDogos }}
{{ $paginator := .Paginate ($onlyDogos) (index .Site.Params "archive-paginate" | default 25) }}

So far so good. How about the inverse? You’ll find that there is no way to tell the line $allDailyDogos to simply do the inverse. There is no "not intersect" or whatever. You have to use another function called symdiff:

{{ $allPosts := where .Site.Pages.ByDate.Reverse "Type" "post" }}
{{ $allDailyDogos := where .Site.Pages "Params.categories" "intersect" (slice "DailyDogo") }}
{{ $noDogos := symdiff $allDailyDogos $allPosts }}
{{ $paginator := .Paginate ($noDogos) (index .Site.Params "archive-paginate" | default 25) }}

Symdiff is short for symmetric difference and means here that we want all elements from the $allPosts array that are not part of the $allDailyDogos array. Meaning we are keeping only those posts, which is like filtering for the inverse of the $onlyDogos array from before.

We could have mashed this all into one line to make it totally unreadable, but I think this is instructive and it would have not been anymore readable in other template languages (if they even would’ve been able to deal with this as a one liner). Still:

  • Why do I need to make a slice/array out of “DailyDogo”?
  • Why is there no NOT operator? - it would’ve been nice in two instances: 1. inside the where function to save an extra call to symdiff or 2. as a better (I’d say) alternative to using symdiff, because people think more along the lines of “all but not these kinds of things” instead of “symmetrical difference of these two sets”.
  • Why is it so hard to chain conditions to the where function to the point where you’d rather create two arrays instead of filtering the array down in one step?

Ugh. There is so much in just this one line - and the subsequent slight changes I have done - that I find weird and unergonomic. I hope I could also show that it’s not complete and utter failure to understand what’s going on, either. Sometimes things are just badly designed for the most obvious use cases in order to accommodate fancier goals. I’d rahter have a templating syntax/language that makes things easier. If that makes it more boring: Good. I’m here to improve my blog first and foremost.

It’s really cool that the upstairs neighbor has decided to renovate their apartment without notice for who knows how long right when my vacation starts. Love it. Love. It. Especially because it seems to include ripping out the floors, too.

Previously (Dead Cells).

Previously (Diablo IV - same link as below).

Quick update on my Diablo IV enjoyment: I don’t have any. So I stopped playing and went back to DeadCells, which is so much more fun.

What was nice, though: I tried to use my Steam Deck as a little gaming PC and it worked like a charm (if a little slow: there were a lot of frame rate drops while playing Diablo IV through Lutris). No need for a Steam Deck Dock either, since I have had a USB-C hub laying around and that’s enough. This opens up some nice possibilities in terms of playing non-mac games for me.

First impression of Diablo IV. I hate the MMORPG aspects of it. I don’t need to see other people in my game. I don’t plan nor want to interact with anyone when playing games.

I also do not like how enemies spawn out of nowhere. In Diablo II you never saw them spawn - if that wasn’t part of their mechanics. Enemies were always already there. Seeing them spawn is another MMORPG thing.

Sadly a lot of Diablo IV’s MMORPG elements make the game very slot-machine-y feeling. You click a bunch in front of a beautifully bleak backdrop and then you find loot. And that loot might be good or bad, but certainly you have always the chance to find better loot. As long as you just click a little more. To be clear: This has always been what Diablo was about, in the end - and maybe it’s because I’m older and/or maybe because you can’t step into the same river twice - but I do not feel like story/progression and this game loop hang together correctly. It feels manipulative. Add on top the social elements - which are designed in a way that try to keep you in the game longer, by promising better rewards if you socialize (and forging relationships in a game like this makes it more likely for you to come back to socialize with game buddies) - and a shop to spend a weird in-game currency that you mostly can get by spending real-live money.

Diablo IV is a glorified slot machine casino.

That being sad, I have enjoyed the less gross aspects of the game, which is the campaign. (Mild spoilers ahead) I mostly like the lore - I’m on level 22 maybe? - but even there I feel like that Lilith, the main antagonist, is too visible and not very mysterious. It seems weird that she is so hands on with her plans, interacting with villagers in far away, unimportant locations directly. Where are her underlings? As a villain she is not super interesting, I’m afraid, and we haven’t seen a lot of other interesting characters - good or bad - so far. If this is a problem of lore, or more a lacking aspect of world building, or story telling I don’t know yet. It might be all of the above or maybe I’ll change my mind. Diablo’s stories have always been somewhat campy, but campy doesn’t mean their story telling was bad.

As you might imagine, I am not blown away by the game. A video game podcast I listen to, Tripple Click, likened the game to Destiny, which is a game with a similar slot machine loop built-in. I would relate it to Far Cry, which although different in many ways seems to me like yet another different take on the same basic idea: Loot-oriented, rpg-style, progressively open world-revealing, multiplayer friendly, story driven gaming.

If all AAA games become one game - at least as far as the general design blue print is concerned - maybe AAA games are not for me anymore. I guess it’s not fair to say, there are many different kinds of AAA games, but it at least feels like the amount of convergence has become more apparent over the last 10 years or so.

Forward.

A little addition to what I said the other day: The “take notes to reach your goals” is kinda important. Because I am a believer that you actually shouldn’t take (too many) notes that are not in some productive way connected to what you want to achieve.

The world is big. And the amount of potential notes you could take is immense. In my own note taking I try to frame the act of note taking as an activity done by an author, not an archivist. Be an author or creator and find ways in which note taking can support you, but try not to fall into the trap of writing everything down.1 Your job is not to have the perfect Zettelkasten (or whatever), but to have and stay in contact with whatever it is your try to do with your life.


  1. Even worse: trying to perfect all this random stuff: By making everything a beautiful, artfully crafted perfect little note. No! Only do that with the stuff that makes you better in your job/helps you reach your goals. ↩︎