Alright, let’s talk about making decisions. In Hugo-land, you’re not just describing content; you’re building logic to shape it. That’s where these comparison and logic functions come in. They’re the if/else statements in your template’s brain, and frankly, they’re a bit of a mixed bag. Some are brilliant in their simplicity, others will make you wonder what the designers were thinking that day. Let’s get into it.

The Core Comparison Gang

These are your workhorses: eq, ne, lt, le, gt, ge. They stand for equals, not equals, less than, less than or equal to, greater than, and greater than or equal to. You know, the classics.

The first thing you need to know, and this is the part that trips up everyone at least once, is that eq takes two arguments and only two arguments. It’s fiercely committed to this minimalist lifestyle. Trying to compare three things is a syntax error, not a clever way to check if they’re all the same.

{{/* This works perfectly */}}
{{ if eq .Title "My Fantastic Page" }}
  <h1>A Fantastic Headline for a Fantastic Page</h1>
{{ end }}

{{/* This will cause Hugo to look at you with deep disappointment */}}
{{/* {{ if eq .Title .Section "blog" }} */}} <!-- ERROR! -->

{{/* This is how you do it. Nest them. It's clunky but it works. */}}
{{ if and (eq .Title "My Blog") (eq .Section "blog") }}
  <h1>Welcome to the blog</h1>
{{ end }}

The other comparison functions work the same two-argument way. They’re mostly straightforward with numbers, but with strings, lt, gt, and friends perform lexicographical comparison (basically, alphabetical order based on Unicode values). This is great for sorting “apple”, “banana”, “cherry” but will utterly ruin your day if you ever try to compare “10” and “2” as strings. “10” is lexicographically less than “2” because ‘1’ comes before ‘2’. Always cast numbers to integers first using int.

{{ $stringNum := "10" }}
{{ $anotherStringNum := "2" }}

{{/* Don't do this: lt will return TRUE */}}
{{ if lt $stringNum $anotherStringNum }} ... {{ end }}

{{/* Do this: convert to int first */}}
{{ if lt (int $stringNum) (int $anotherStringNum) }} ... {{ end }} <!-- FALSE -->

The Logic Bouncers: and, or, not

These are your boolean operators. and and or are also, and I’m sorry about this, limited to two operands. Yes, it’s 2024 and we’re manually chaining basic logic operations. It’s the most genuinely absurd part of this whole system. You want to check if a value is A, B, or C? Prepare for some visual noise.

{{/* The correct, albeit verbose, way to check multiple conditions */}}
{{ if or (eq .Kind "page") (eq .Kind "section") (eq .Kind "home") }}
  This is a page, a section, or the homepage. Probably.
{{ end }}

{{/* You might see people try to use "in", which is often cleaner for equality checks */}}
{{ if in (slice "page" "section" "home") .Kind }}
  This is the much smarter way to do the above. Use this.
{{ end }}

and and or are also “short-circuit” operators, which is a fancy way of saying they’re lazy. and stops evaluating as soon as it finds a false condition. or stops as soon as it finds a true one. This is a feature, not a bug, and it’s crucial for writing efficient templates. It means you can safely do something like this:

{{/* If .Params.author exists, *then* check its length. Safe because of short-circuiting. */}}
{{ if and (isset .Params "author") (gt (len .Params.author) 0) }}
  <p>Author: {{ .Params.author }}</p>
{{ end }}

If isset returns false, Hugo never bothers to run the len check, which prevents a nasty error you’d get from calling len on a non-existent value.

The Quirks and The “Why”

You’ll notice a theme: a dogmatic adherence to two arguments for the core functions. The reason is buried in Go’s template language design. These functions are mapped directly to Go’s built-in comparison operators, which are binary. The Hugo team didn’t extend them, likely to maintain compatibility and simplicity at the engine level. It’s a questionable choice for user-friendliness, but a defensible one for maintainability.

not is your friend. It takes a single argument and returns the boolean opposite. Use it to invert conditions for cleaner logic.

{{ if not (eq .Type "blog") }}
  <p>This is not a blog post. It's something more... mysterious.</p>
{{ end }}

Best Practice: When dealing with potentially empty values, not is often safer than checking for equality against an empty string. A non-existent value and an empty string both evaluate to false in a boolean context.

{{/* This handles both nil and "" */}}
{{ if not .Params.subtitle }}
  <p>No subtitle was provided.</p>
{{ end }}

So there you have it. These functions are the bedrock of template logic. They’re simple, occasionally frustrating, but incredibly powerful. Master their two-argument quirk, embrace short-circuiting, and always, always cast your numbers to integers before comparing them. Now go build something intelligent.