RSS Amplifier

BurgeonLab: Full-text · Jul 12, 2025

Hugo (Static Site Generator) CMS: Tips and Tricks

0
Sign in to vote or save

Naty S · BurgeonLab

A collection of notes and learnings from using Hugo as a primary CMS. This post will be updated periodically.

Here are some notes of things I’ve learnt while using Hugo as my main CMS. I will be updating this post as I discover new tips and tricks. Hope it helps!

Hugo Templates

Render-heading Anchors for Templates

Hugo provides layouts/_markup/render-heading.html for use to render anchor links in Markdown content. But what if you want to add anchor links to headings created in template files (.html)?

Create a partial e.g. layouts/_partials/render-heading.html and reference the example below:

1{{- $headingText := .text -}}
2{{- $anchor := .id | default ($headingText | anchorize) -}}
3<h{{ .level }} id="{{- $anchor | safeURL -}}" {{- if .class -}}class="{{- .class -}}"{{- end -}} {{- if .style -}}style="{{- .style | safeCSS -}}"{{- end -}}>
4  <a href="#{{- $anchor | safeURL -}}" class="h-anchor" title='Permalink to #{{- $headingText | plainify | safeHTMLAttr -}}'>
5    <svg>Use your own svg like a link logo or just use the symbol like #</svg>
6  </a>
7  {{- $headingText | safeHTML -}}
8</h{{ .level }}>

To use the partial in .html files:

1{{ partial "render-html-heading.html" (dict "text" "This Is a Test Heading" "level" 2) }}

This will create the heading anchor in H2. Adjust the “level” number for the respective H2-H6 options. If your heading has a class or id attribute, use the following syntax:

1{{ partial "render-html-heading.html" (dict "text" "Example Heading" "level" 2 "id" "unique-id-name") }}
2{{ partial "render-html-heading.html" (dict "text" "Another Heading" "level" 2 "class" "p-name") }}

If the link text is a Hugo computed value like title $name, the correct way is to compute the value first, then pass it into the partial. For example:

1<div class="taxonomy-list">
2{{ range $name, $taxonomy := .Site.Taxonomies }}
3{{ if gt (len $taxonomy) 0 }}
4  {{- $headingText := title $name -}}  <!-- compute first  -->
5  {{ partial "render-html-heading.html" (dict "text" $headingText "level" 4) }} <!-- pass $headingText into partial  -->
6{{ end }}
7</div>

Range: Section or Type

To iterate over a range, use section to select content by the content path name, e.g. /content/posts would be posts or /content/pages would be pages. The slice values must match the directory name exactly (case-sensitive). For example:

1{{- range where .Site.RegularPages "Section" "in" (slice "posts" "pages" "weeknotes") -}}

On the other hand, use type to select content based on the page’s front matter type value, e.g. type = "post". For example:

1{{- range where .Site.RegularPages "Type" "in" (slice "post" "page" "weeknote") -}}

Tip

To iterate over all pages (including non-regular pages like taxonomy pages), use .Site.Pages instead of .Site.RegularPages.

Another tip for debugging templates: check a page’s section or type status with: <pre>{{ printf "Section=%s Type=%s" .Section .Type }}</pre>

Minify

1[minify]
2  disableHTML = false
3
4[outputFormats.HTML]
5  mediaType = "text/html"
6  isPlainText = false
7  isHTML = true
8  noUgly = true

To have more readable HTML output, don’t use --minify in the build pipeline, or check if you have minification set to true in hugo.toml (config) and set HTML outputs.

Comparison between non-minified and minified HTML code.

Difference between minified and non-minified code

Whitespace Trimming

  • Use whitespace control {{- and -}} to remove unwanted extra blank lines when writing templates in Go. For example, template blocks like {{ if }}, {{ range }}, {{ else }} will include all the lines surrounding them in the output. This means when viewing the HTML like in “View Page Source”,1 there will be lots of empty lines.

ALTTEXT

Removed whitespace vs non-trimmed code
  • {{- will remove all the whitespace and new lines left of the action, and vice versa for -}}. I do this for all if, else if, end, range, with, and block. But don’t do it for all curly brackets to prevent breaking layouts, as sometimes new lines and spaces are necessary for HTML formatting. Always test after changing.

  • For {{ end }}, I tend to add the trim to left of the action like {{- end }} because it is closing a block, is on its own line, and I want the next item to start on a new line. As for partials, use selectively—it may trim too much, so try to fix whitespace within the partial itself.

  • I used to use HTML comment syntax like: <!-- This is a comment --> but I realise that they would appear in HTML output. So I’ve switched to Hugo comment syntax like: {{/* Another comment */}}

Custom Templates

  • If you’ve read how to make customizations to your Hugo theme, you probably came across the instruction not to modify the theme files directly; but make use of Hugo’s lookup order.

  • Any file with the same name in the project’s layouts directory will override the one in /themes/theme_name/layouts. This is to prevent your changes overwritten during theme upgrades.

  • I also suggest reading how to use blocks and partials to incorporate your own theme customizations as it will make code maintenance easier in the long run.

Exclude Partials in Server Mode

  • I use iine upvote buttons for most of my pages on the site. The iine section is in it’s own partial called “iine.html”. To exclude something while working in hugo server locally (but shown on the live site), wrap the partial or whatever you don’t want to see with:

    1{{ if not hugo.IsServer }}
    2  {{ partial "iine.html" . }}
    3{{ end }}
  • If the opposite is what you want, i.e., you don’t want something to load on the live site, but only in hugo server mode, use:

    1{{ if hugo.IsServer }}
    2  {{ partial "draft-status-indicator.html" . }}
    3{{ end }}

Hugo Front Matter

  • Having a slug parameter in the front matter is useful as it prevents Hugo from creating the url based on the title, which can cause 404 errors if you decide to rename the folder/file.

  • If you need to redirect old URLs, use the handy aliases parameter to point to the new URL.

  • I use TOML for my front matter, and I recommend " double quotes as it’s more reliable and less error prone than using ' single quotes (tip don’t wrap dates in quote marks though!) For example:

1+++
2draft = true
3type = "post"
4date = {{ .Date }}
5# lastmod = 
6title = "{{ replace .File.ContentBaseName "-" " " | title }}"
7slug = "post-slug"
8description = "160 char meta description"
9+++

Hugo offers options to output many formats in the config file. I wanted my sections (i.e. the folders within content, e.g. posts, pages, weeknotes ) to have RSS feeds generated, but I wanted to exclude pages.

Apparently, there’s no way to selectively choose sections or have an exclude option. To workaround it:

  1. Turn on ‘RSS’ generation globally in the hugo.toml config file. (I have my own custom outputFormats called ATOM, but the default is rss—check your config’s outputFormats).

    1section = ["HTML", "ATOM"]
  2. Override the global setting by adding the following line in the front matter of content/pages/_index.md, the section you want to exclude, like so:

    1outputs = ["HTML"]

Any outputs included in the front matter of a section will OVERWRITE the output setting regardless of what is set in the global config. But this is not true for pages, where it will APPEND the extra outputs on that specific page, e.g. content/pages/about/index.md.

Tip

Upper and lower case matters here—if your outputFormat is RSS, use that consistently.

Also the order of the output is important, HTML should remain first so that it can act as the primary output format.

Internal Links

When linking internal, pages or posts, a simple Markdown link pointing to a relative file path will work fine if you won’t be changing the target folder name or directory structure.

1Page link: [About page](/pages/about)
2Post link: [A post](/posts/2025/001-hugo-tips/index.md)

Shortcodes

Note

ref Shortcodes are obsolete, please skip to the render hook section!

In the beginning, I used Hugo’s ref shortcode to generate the link dynamically like this example screenshot below. It will generate the correct permalink on build based on Hugo’s content structure and front matter settings—namely slug or url.

ALTTEXT

An example of using the rel shortcode to link a section of one of my other posts

Tip

To get the right syntax highlighting in VSCodium for Hugo shortcodes, I recommend the Hugo Utilities extension.

Hugo provides ref and relref shortcodes which generates absolute or relative URLs respectively. The advantages of using Hugo shortcode for generating links are:

  • Hugo validates that the target exists; warning you on build that the link is broken, therefore reducing broken links on your site
  • Shortcodes are useful for multilingual sites as it can add language prefixes to the permalink
  • Using shortcodes means nothing is hardcoded, e.g., you decide to change your slug, the link will not break as Hugo will read the updated front matter to generate the new permalink

But after a while, with about 20 of these ref shortcodes scattered in my Markdown content, I decided it was maybe not such a good idea to internally link with a non-portable, Hugo-specific method. Let’s say one day I decide to switch to another static site generator, or even, just wanting to browse my content locally in a Markdown parser, these clunky links, will not work.

1[Link]({{< ref "/posts/2025/001-post-title/index.md" >}})

And guess what, I didn’t even know, but according to the docs, the ref shortcode is already depreciated and obsolete for some time (since v0.123). It is now recommended to use Embedded Link Render Hooks.

Render Hooks

So what are Hugo render hooks? From my understanding, they are like templates for generating how links are rendered in HTML from Markdown content. You can customize and add custom rules to make the links have features that you otherwise wouldn’t get in plain Markdown links, e.g., adding attributes like class, rel, target, or validating if a link target exists, etc. In other words, it can do all that of the ref shortcode and more without cluttering the Markdown files with Hugo shortcode syntax; by using standard Markdown link syntax. I think it’s pretty powerful.

I have now replaced all ref shortcodes with standard looking Markdown link syntax. In the link render hook template (/_layouts/_markup/render-link.html), there are these features, doing more than the original shortcode:

  • Opens external links (not internal ones) in new tabs with target="_blank" rel="noopener noreferrer" and adds an inline SVG to indicate it is an external link

  • Generates the correct permalink using the front matter

  • Warns if the target doesn’t exist on build

  • Has the option to add link title (i.e. tooltip) with this Markdown syntax:

    [Topic](/posts/topic "Read more about topic")

Shortcode vs Render Hook

Featureref ShortcodesRender Hooks
Lock-in RiskHigh (Hugo-specific syntax)Low (Markdown is standard syntax, only the hook is Hugo-specific)
Link ValidationValidates target page existsCan add logic to template
Broken Link RiskLow (build-time validation)Low (with validation logic)
Dynamic URL SupportResolves to permalink or slugResolves via template (e.g., .Permalink)
Multilingual SupportHandles language prefixesCan add logic to template
Ease of UseComplex syntaxSimple input; but complex initial template setup
Status (as of 2025)Obsolete, not recommendedRecommended in official docs
CustomizationLimited to shortcode paramsHighly customizable via template

Preventing Shortcodes From Rendering in Code Examples

In scenarios when you want type an example of a real Hugo shortcode you’re using (say for a tutorial post), how can you stop it from activating or rendering? Wrapping it in a single or triple backtick (`) doesn’t work. To fix this, comment out the shortcode name like so:

1{{</* random-function */>}}

Dependencies

I’ve used additional tools with Hugo to add extra functionality. I will add notes on each in due course.

Static Search Function: Pagefind

Pagefind adds search functionality to static sites, perfect for my use case because it wasn’t built-in to the theme I use. Follow the official Quick Start guide to index the site.

On the results page, Pagefind displays the subheadings as links of every matching term within a single page result. It looked very cluttered, so I used a CSS rule to only show a single link to the page where the search term appears multiple times.

1/* Show the context heading ONLY on the first occurrence of each page */
2.pagefind-ui__result-nested .pagefind-ui__result-title a {
3  display: none;
4}

For sections that I don’t want to be indexed, use the class no-index. Pagefind also supports a config file, pagefind.toml in the root directory of the site. Mine is set to:

1site = "public"
2output_subdir = "pagefind"
3exclude_selectors = [".no-index"]

Diagrams

D2 Lang (Declarative Diagramming) is what I use to generate all my Open Graph images. I used to use Mermaid but I’m transitioning away from it due to its hard AI push and reliance on JS (I’m using SVG export for D2 diagrams which is simpler to manage). See my guide on using D2 for OG images to learn more.

To Be Continued

Consider this post work in progress.


  1. To see page source with word wrap in Firefox, go to about:config and switch view_source.wrap_long_lines to true↩︎

Read the original on burgeonlab.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.