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
andhighlight.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:
- Create the directory
layouts/_markup/
- Copy the code in next section to
layouts/_markup/render-blockquote.html
- 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 thealert-{{ .AlertType }}
class (seeAlertType
) 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, seetransform.Emojify
. - Then it’s followed by the capitalized title of the alert, i.e.
Note
,Tip
,Warning
, etc. thanks tostrings.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
accordingto 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
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
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

{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

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 hookfallback
: Use only if no custom render hooks existnever
: 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:

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

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/
orstatic/
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

{id="bar" class="foo"}
Link
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:
- Its docs
- And its template render-link.html.
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:
- Create the directory
layouts/_markup/
- Copy the code in next declaration section to
layouts/_markup/render-table.html
- 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
Name | Age | Score | Country |
---|---|---|---|
Maria | 30 | 95 | Spain |
Daniel | 25 | 87 | Germany |
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:
Name | Age | Score | Country |
---|---|---|---|
Maria | 30 | 95 | Spain |
Daniel | 25 | 87 | Germany |
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:
Name | Age | Score | Country |
---|---|---|---|
Maria | 30 | 95 | Spain |
Daniel | 25 | 87 | Germany |
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
Name | Age | Score | Country |
---|---|---|---|
Maria | 30 | 95 | Spain |
Daniel | 25 | 87 | Germany |
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:
Name | Age | Score | Country |
---|---|---|---|
Maria | 30 | 95 | Spain |
Daniel | 25 | 87 | Germany |
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.
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 |
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:
- Process programmatically attributes. Explained in Safe HTML Attributes subsection.
- Correct alignment of table cells
Blogs
J. Marinero - Data Scientist & AI Engineer