Reference https://gohugo.io/render-hooks/

Table of contents:

Shortcodes vs Render Hooks

We studied shortcodes in the previous post. But what are the main differences between shortcodes and render hooks?

Shortcodes:

  • Used manually in content files. You control when and how they’re used. Ideal for complex elements/functionality:
    • Complex layout like a multi-column section
    • Dynamic data such as the latest blog posts list
    • Embed a specific piece of non-markdown content. E.g., a specific video player or an interactive map.
  • Use the {{< ... >}} syntax. Multiline {{< details >}}...{{< /details >}} or inline as {{< qr text="https://gohugo.io" >}}.
  • They allow custom Go HTML templates and complex logic, including calling Go functions. See the flowchart of the Highlight shortcode explanation: the template takes advantage of transform.go and highlight.go scripts.
  • .Page.RenderString method, described in previous post, or other rendering methods can be applied within the shortcode to parse its inner content as Markdown. But they are optional.
  • Shortcodes can be nested. Details here.

Render Hooks:

  • Usage globally for Markdown elements. Automatically intercept and modify how standard markdown elements render. It works implicitly, e.g. > [!NOTE] becomes an styled blockquote.
  • Standard Markdown syntax, e.g. [link](url). Thus, it’s more portable, though the custom rendering would not automatically work in other engines.
  • Specific Markdown rendering, it cannot call any Go function. Only certain Markdown elements can be customized (images, links, headings, tables, etc.), whereas shortcodes can create virtually anything.

Blockquote

Alerts

MWE

To achieve next automatic renders just follow the docs steps:

  1. Create the directory layouts/_markup/
  2. Copy the code in next section to layouts/_markup/render-blockquote.html
  3. Edit your markdowns following next Minimal Working Examples
> [!NOTE]
> Useful information that users should know, even when skimming content.

Which renders to

ℹ️ Note

Useful information that users should know, even when skimming content.

> [!TIP]
> Helpful advice for doing things better or more easily.

Previous renders to

💡 Tip

Helpful advice for doing things better or more easily.

> [!IMPORTANT]
> Key information users need to know to achieve their goal.

Now it renders to

ℹ️ Important

Key information users need to know to achieve their goal.

> [!warning]
> Urgent info that needs immediate user attention to avoid problems.

Outputs:

ℹ️ Warning

Urgent info that needs immediate user attention to avoid problems.

> [!CAUTION]
> Advises about risks or negative outcomes of certain actions.

Can render in markdowns to next

❗ Caution

Advises about risks or negative outcomes of certain actions.

Notice the red font-color is applied to every HTML element except the alert line (the first <p>aragraph) of the blockquote of type alert-caution. This can be customized by editing layouts/partials/custom_head.html:

blockquote.alert-caution *:not(p:first-of-type) {
  color: red;
}

Declaration

Edit the layouts/_markup/render-blockquote.html script:

{{ $emojis := dict
  "caution" ":exclamation:"
  "important" ":information_source:"
  "note" ":information_source:"
  "tip" ":bulb:"
  "warning" ":information_source:"
}}

{{ if eq .Type "alert" }}
  <blockquote class="alert alert-{{ .AlertType }}">
    <p class="alert-heading">
      {{ transform.Emojify (index $emojis .AlertType) }}
      {{ with .AlertTitle }}
        {{ . }}
      {{ else }}
        {{ or (i18n .AlertType) (title .AlertType) }}
      {{ end }}
    </p>
    {{ .Text }}
  </blockquote>
{{ else }}
  <blockquote>
    {{ .Text }}
  </blockquote>
{{ end }}

First of all I recommend you to understand basic Go code, with statement, etc. Read Go and Hugo code basics from my Hugo’s shortcode post.

Let’s dive in!

First a Go dictionary for emojis is defined. Depending on the Alertype-key the respective emoji-value is retrieved with index $emojis .AlertType.

The rest of the bloquote template is basicaly just:

{{ if eq .Type "alert" }}
  [...]
{{ else }}
  <blockquote>
    {{ .Text }}
  </blockquote>
{{ end }}

I.e., if the blockquote is not an alert, the {{ else }}, e.g. a single line like > Useful information that users should know not preceded by > [!NOTE] or alike, then we just print the content of the blockquote as it is (.Text).

On the other hand, if the markdown blockquote (first line) is an alert Type, {{ if eq .Type "alert" }} is true, then we run the code that [...] represents - a <blockquote> element:

  • The alert class and the alert-{{ .AlertType }} class (see AlertType) define the style of the blockquote. See the CSS style customization of a cauton blockquote at the end of previous subsection.
  • The first paragraph <p> starts with the respecitive alert emoji, see transform.Emojify.
  • Then it’s followed by the capitalized title of the alert, i.e. Note, Tip, Warning, etc. thanks to strings.Title.
  • The paragrah’s final content is the .AlertTitle if any. We skipped this in previous MWEs, read Extended syntax.
  • Finally a second <p> is created with just the content of the second blockquote line, i.e. {{ .Text }}, e.g. the line > Useful information that users should know if preceded by > [!NOTE] or other alerts.

Actually, a blockquote is allowed to have as many lines as we want, read this brief markdown guide. For example the .Text may include all next lines (from second line onwards):

> [!CAUTION]
> **Multiple Paragraphs**:
>
> Paragraph 1.
>
> Paragraph 2.
>
> **Other Elements**:
>
> #### The quarterly results look great!
>
> - Revenue was off the chart.
> - Profits were higher than ever.
>
>  *Everything* is going ~~according~~ to **plan**.
>
> **Nested Blockquotes**:
> 
> Dorothy followed her through many of the beautiful rooms in her castle.
>
>> The Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood.

The markdown code above create next multiline blockquote. Notice though that the nested blockquotes seem to weirdly format:

❗ Caution

Multiple Paragraphs:

Paragraph 1.

Paragraph 2.

Other Elements:

The quarterly results look great!

  • Revenue was off the chart.
  • Profits were higher than ever.

Everything is going according to plan.

Nested Blockquotes:

Dorothy followed her through many of the beautiful rooms in her castle.

The Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood.

As an HTML figure with an optional citation and caption

MWE

> Some text
{cite="https://gohugo.io" caption="—Some caption"}

Can be rendered to next HTML if layouts/_markup/render-blockquote.html is edited to next section content.

Some text

—Some caption

Which is close to the standard usage of this HTML element. Read the <blockquote> Mozilla docs.

{{< rawhtml >}}
<div>
  <blockquote cite="https://www.huxley.net/bnw/four.html">
    <p>
      Words can be like X-rays, if you use them properly—they’ll go through anything. You read and you’re pierced.
    </p>
  </blockquote>
  <p>—Aldous Huxley, <cite>Brave New World</cite></p>
</div>
{{< /rawhtml >}}

Previous raw HTML outputs:

Words can be like X-rays, if you use them properly—they’ll go through anything. You read and you’re pierced.

—Aldous Huxley, Brave New World

Declaration

  • Enable Markdown attributes editing your hugo.toml as explained here
  • And re-edit layouts/_markup/render-blockquote.html script:
<figure>
  <blockquote {{ with .Attributes.cite }}cite="{{ . }}"{{ end }}>
    {{ .Text }}
  </blockquote>
  {{ with .Attributes.caption }}
    <figcaption class="blockquote-caption">
      {{ . | safeHTML }}
    </figcaption>
  {{ end }}
</figure>

Now we no longer try to read Tipe, AlertType and AlertTitle attributes, but Attributes instead.

  • If the line after a markdown quote (wrapped in curly braces) shows the parameter cite, then it’s added to the <figure>-<blockquote> element.
  • Analogous for the caption. If it’s passed, then that’s the <figcaption> text.

Multiple render hooks

Notice that previous blockquote-alerts render hooks and the later figure citations render hooks are mutually exclusive because their respective HTML template layouts/_markup/render-blockquote.html are different.

I recommend to set as render hook the most frequent for you. The other one can be set as a custom shortcode, the process is detailed in my previous post; alternative, write HTML directly in your Hugo markdowns via the raw HTML shortcode.

This advise holds for ordinary blockquotes, but if your blockquote might display complex HTML elements, requires special Go embedding functions, etc. then write it in a custom shortcode instead of a render hook. Read the Shortcodes vs Render Hooks initial chapter.

Code block

Hugo’s internal template system for rendering code blocks is available since v0.93.0. Same release enabled diagram render hooks with GoAT (Go ASCII Tool) too.

MWE

As explained in the highlight shortcode example of my specific post, we can highlight code chunks with

{{< highlight go "linenos=inline, hl_lines=3 6-8, style=monokai" >}}
package main

import "fmt"

func main() {
    for i := 0; i < 3; i++ {
        fmt.Println("Value of i:", i)
    }
}
{{< /highlight >}}

Hugo renders this to:

1package main
2
3import "fmt"
4
5func main() {
6    for i := 0; i < 3; i++ {
7        fmt.Println("Value of i:", i)
8    }
9}

We can achieve the same via code blocks render hooks:

```go {lineNos=inline hl_lines="3 6-8" style=monokai}
package main

import "fmt"

func main() {
    for i := 0; i < 3; i++ {
        fmt.Println("Value of i:", i)
    }
}
```

Which also renders to:

1package main
2
3import "fmt"
4
5func main() {
6    for i := 0; i < 3; i++ {
7        fmt.Println("Value of i:", i)
8    }
9}

One can pass even class-es and id. Read the docs https://gohugo.io/render-hooks/code-blocks/

GoAT

For example, the following GoAT diagram

```goat
      .               .               .--- 1
     / \              |           .---+     
    /   \         .---+---.       |   '--- 2
   +     +        |       |    ---+         
  / \   / \     .-+-.   .-+-.     |   .--- 3
 /   \ /   \    |   |   |   |     '---+     
 1   2 3   4    1   2   3   4         '--- 4
```

Gets converted to

123412341234

Many more Go ASCII diagram examples here!

Further references:

  • Docs
  • render-codeblock-goat.html
  • For other diagram renderes, not only GoAT, like Mermaid, read this. Follow the tip, create language-specific templates layouts/_markup/render-codeblock</-python/-mermaid>.html.

Heading

This will be summary in a nutshell.

# Heading 1
## Heading 1.1

Creates a default id for each header. It’s equivalent to:

# Heading 1 {#heading-1}
## Heading 1.1 {#heading-1.1}

Notice the id might appear, as in previous code, the first and only positional paramenter (detailed explaind in my shortcode post) or it can be defined as a named parameter (read prev. post too) like the class-es. E.g.

# Heading 1 {id="heading-1" class="foo"}
## Heading 1.1 {id="heading-1.1" class="bar"}

Or mix of both:

# Heading 1 {#heading-1 class="foo"}
## Heading 1.1 {#heading-1.1 class="bar"}

For further information and guide to override this default render hook read the docs.

Image

The default render hook funcionality can be customized/expanded applying image process methods in your custom shortcode template.

Let’s:

  • First understand how the image render hooks operates in next pararagraphs
  • Then, find out the how to the official figure shortcode works in my Hugo shortcodes post.

You can do it, but you don’t have to - kann man, muss aber nicht in German. No need to study any figure (image) shortcode nor image processing if next render hook already fulfills your requirements.

MWE

![favicon](images/favicon.png "This website favicon!")
{id="bar" class="foo"}

Renders to

<img
  src="/images/favicon.png"
  alt="favicon"
  title="This website favicon!"
  class="foo"
  id="bar">

And is displayed as

favicon

Note, Hugo automatically uses the embedded image render hook for multilingual single-host sites. Since next is the default config:

[markup.goldmark.embeddedImageRender]
enable = 'auto'  # or 'always', 'fallback', 'never'

Other options are:

  • always: Force use of embedded hook
  • fallback: Use only if no custom render hooks exist
  • never: Disable embedded hook

Do not forget to disable next as well:

[markup.goldmark.parser]
  wrapStandAloneImageWithinParagraph = false

Why is this needed at all? Well, this render hook processes Markdown image links to provide better resource handling:

  • Find the actual image file
  • Generate the correct URL for it

We will explain these two main purposes in detail in the next section studying the template code.

The docs emphasize the first feature, the image destination, so next we will exemplify the other one. Next embedded image:

![Unrecognizable woman walking dogs on leashes in countryside](https://images.pexels.com/photos/7210754/pexels-photo-7210754.jpeg?width=640 "Image title")

It’s rendered to (notice the ?width=640 in the src-URL is kept):

<img
  src="http://images.pexels.com/photos/7210754/pexels-photo-7210754.jpeg?width=640"
  alt="Unrecognizable woman walking dogs on leashes in countryside"
  title="Image title"
>

Which leads to

Unrecognizable woman walking dogs on leashes in countryside

Open the image in a new tab to confirm that it maintains the width query parameter. No need to load a heavier image (which would happen if enable='never') if the web editor finds a lighter one good enough.

Declaration

Code of render-image.html:

{{- $u := urls.Parse .Destination -}}
{{- $src := $u.String -}}
{{- if not $u.IsAbs -}}
  {{- $path := strings.TrimPrefix "./" $u.Path -}}
  {{- with or (.PageInner.Resources.Get $path) (resources.Get $path) -}}
    {{- $src = .RelPermalink -}}
    {{- with $u.RawQuery -}}
      {{- $src = printf "%s?%s" $src . -}}
    {{- end -}}
    {{- with $u.Fragment -}}
      {{- $src = printf "%s#%s" $src . -}}
    {{- end -}}
  {{- end -}}
{{- end -}}
<img src="{{ $src }}" alt="{{ .PlainText }}"
  {{- with .Title }} title="{{ . }}" {{- end -}}
  {{- range $k, $v := .Attributes -}}
    {{- if $v -}}
      {{- printf " %s=%q" $k ($v | transform.HTMLEscape) | safeHTMLAttr -}}
    {{- end -}}
  {{- end -}}
>
{{- /**/ -}}

Relative Path Resolution

{{- if not $u.IsAbs -}}  // If it's not an absolute URL (remote image)
{{- $path := strings.TrimPrefix "./" $u.Path -}}
  • Removes ./ prefix if present
  • Handles relative paths correctly

Resource Lookup

{{- with or (.PageInner.Resources.Get $path) (resources.Get $path) -}}
  • First tries to find the image in page resources (images in the same directory as the content file)
  • Falls back to global resources (images in assets/ or static/ directories)

Preserves URL Components

{{- with $u.RawQuery -}}
  {{- $src = printf "%s?%s" $src . -}}
{{- end -}}
{{- with $u.Fragment -}}
  {{- $src = printf "%s#%s" $src . -}}
{{- end -}}
  • Maintains query parameters like ?width=640 of the original URL in previous section example
  • Preserves fragments as #thumbnail

Safe HTML Attributes

{{- range $k, $v := .Attributes -}}
  {{- if $v -}}
    {{- printf " %s=%q" $k ($v | transform.HTMLEscape) | safeHTMLAttr -}}
  {{- end -}}
{{- end -}}

This chunk processes programmatically assigned attributes.

It’s like the {...} attribute syntax that we saw on heading render hooks. E.g. the {id="bar" class="foo"} in

![favicon](images/favicon.png "This website favicon!")
{id="bar" class="foo"}

The link render hooks in Hugo work in a completely analogous way to the image render hooks just studied.

Therefore read the previous section about images render hooks to better comprehend:

Example usage:

[Tolkien: books, podcasts and much more!](/blogs/tolkien)
{class="font-middle-earth-joanna-vu"}

Renders to:

Tolkien: books, podcasts and much more!

Passthrough

Passthrough render hooks allow to override the rendering of raw Markdown text that is preserved by Goldmark’s Passthrough extension. This is for content wrapped in special delimiters that should bypass normal Markdown processing.

The passthrough extension is often used in conjunction with the MathJax extension, check out my post concerning $\LaTeX$ and $\text{Ti}\textit{k}\text{Z}$ for web dev .

References: https://gohugo.io/render-hooks/passthrough/

Table

To achieve next automatic renders just follow the docs steps:

  1. Create the directory layouts/_markup/
  2. Copy the code in next declaration section to layouts/_markup/render-table.html
  3. Edit your markdowns following next Minimal Working Examples

MWE

Default left alignment
| Name      | Age   | Score       | Country |
| ---       | ---   | ---         | ---     |
| Maria     | 30    | 95          | Spain   |
| Daniel    | 25    | 87          | Germany |
{id="t1"}

Renders to

<table id="t1">
  <thead>
      <tr>
          <th>Name</th>
          <th>Age</th>
          <th>Score</th>
          <th>Country</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Maria</td>
          <td>30</td>
          <td>95</td>
          <td>Spain</td>
      </tr>
      <tr>
          <td>Daniel</td>
          <td>25</td>
          <td>87</td>
          <td>Germany</td>
      </tr>
  </tbody>
</table>

And looks like next, notice the left alignment

NameAgeScoreCountry
Maria3095Spain
Daniel2587Germany
Block display
| Name   | Age   | Score | Country |
| ---    | ---   | ---   | ---     |
| Maria  | 30    | 95    | Spain   |
| Daniel | 25    | 87    | Germany |
{id="t2" style="display: block"}

Renders to

<table id="t2" style="display: block">
  <thead>
      <tr>
          <th>Name</th>
          <th>Age</th>
          <th>Score</th>
          <th>Country</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Maria</td>
          <td>30</td>
          <td>95</td>
          <td>Spain</td>
      </tr>
      <tr>
          <td>Daniel</td>
          <td>25</td>
          <td>87</td>
          <td>Germany</td>
      </tr>
  </tbody>
</table>

Output:

NameAgeScoreCountry
Maria3095Spain
Daniel2587Germany
Alignment wrong way
| Name   | Age   | Score | Country |
| ---    | ---   | ---   | ---     |
| Maria  | 30    | 95    | Spain   |
| Daniel | 25    | 87    | Germany |
{id="t3" style="text-align: right"}

Renders to

<table id="t3" style="text-align: right">
  <thead>
      <tr>
          <th>Name</th>
          <th>Age</th>
          <th>Score</th>
          <th>Country</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Maria</td>
          <td>30</td>
          <td>95</td>
          <td>Spain</td>
      </tr>
      <tr>
          <td>Daniel</td>
          <td>25</td>
          <td>87</td>
          <td>Germany</td>
      </tr>
  </tbody>
</table>

Notice the fail in the right alignment:

NameAgeScoreCountry
Maria3095Spain
Daniel2587Germany
Alignment correct approach
| Name   | Age   | Score | Country |
| ---    | :---: | ---:  | ---     |
| Maria  | 30    | 95    | Spain   |
| Daniel | 25    | 87    | Germany |
{id="t4" style="color: blue"}

Renders to

<table id="t4" style="color: blue">
  <thead>
      <tr>
          <th>Name</th>
          <th style="text-align: center">Age</th>
          <th style="text-align: right">Score</th>
          <th>Country</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Maria</td>
          <td style="text-align: center">30</td>
          <td style="text-align: right">95</td>
          <td>Spain</td>
      </tr>
      <tr>
          <td>Daniel</td>
          <td style="text-align: center">25</td>
          <td style="text-align: right">87</td>
          <td>Germany</td>
      </tr>
  </tbody>
</table>

Thanks to the :---: and ---: we can align the columns

NameAgeScoreCountry
Maria3095Spain
Daniel2587Germany
ID and classes
| Name   | Age   | Score | Country |
| ---    | :---: | ---:  | ---     |
| Maria  | 30    | 95    | Spain   |
| Daniel | 25    | 87    | Germany |
{id="t5" class="font-middle-earth-joanna-vu"}

Or directly

| Name   | Age   | Score | Country |
| ---    | :---: | ---:  | ---     |
| Maria  | 30    | 95    | Spain   |
| Daniel | 25    | 87    | Germany |
{#t5 .font-middle-earth-joanna-vu}

Renders the next HTML where the Middle Earth font is applied

<table class="font-middle-earth-joanna-vu" id="t5">
  <thead>
      <tr>
          <th>Name</th>
          <th style="text-align: center">Age</th>
          <th style="text-align: right">Score</th>
          <th>Country</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Maria</td>
          <td style="text-align: center">30</td>
          <td style="text-align: right">95</td>
          <td>Spain</td>
      </tr>
      <tr>
          <td>Daniel</td>
          <td style="text-align: center">25</td>
          <td style="text-align: right">87</td>
          <td>Germany</td>
      </tr>
  </tbody>
</table>

Output:

NameAgeScoreCountry
Maria3095Spain
Daniel2587Germany
Long tables
| Name   | Age   | Score | Country | City   | Nationality | University          | Experience (years) |
| ---    | :---: | ---:  | ---     | ---    | ---         | ---                 | ---                |
| Maria  | 30    | 95    | Spain   | Madrid | Spanish     | UNED                | 3                  |
| Daniel | 25    | 87    | Germany | Berlin | German      | Hochschule Muenchen | 2.5                |
{#t6}

The display indicates that the render hook template has considerable room for improvement. For example a scrollable table.

NameAgeScoreCountryCityNationalityUniversityExperience (years)
Maria3095SpainMadridSpanishUNED3
Daniel2587GermanyBerlinGermanHochschule Muenchen2.5

Declaration

The render-table.html template is

<table
  {{- range $k, $v := .Attributes }}
    {{- if $v }}
      {{- printf " %s=%q" $k ($v | transform.HTMLEscape) | safeHTMLAttr }}
    {{- end }}
  {{- end }}>
  <thead>
    {{- range .THead }}
      <tr>
        {{- range . }}
          <th
            {{- with .Alignment }}
              {{- printf " style=%q" (printf "text-align: %s" .) | safeHTMLAttr }}
            {{- end -}}
          >
            {{- .Text -}}
          </th>
        {{- end }}
      </tr>
    {{- end }}
  </thead>
  <tbody>
    {{- range .TBody }}
      <tr>
        {{- range . }}
          <td
            {{- with .Alignment }}
              {{- printf " style=%q" (printf "text-align: %s" .) | safeHTMLAttr }}
            {{- end -}}
          >
            {{- .Text -}}
          </td>
        {{- end }}
      </tr>
    {{- end }}
  </tbody>
</table>

The template basically adds:


Blogs
J. Marinero - Data Scientist & AI Engineer