Link render hooks
I want to be able to make changes to how links are rendered to HTML under certain conditions. I’m starting from a link render hook that I copied off the internet as a tool for debugging broken internal links. Details about that are here: notes / Checking internal Hugo links with render link template. It was late at night when I found that code. It seems good! So I just accepted that it worked and didn’t pay much attention to it. Now I need to figure out how it works.
Reading through Link render hooks (Hugo docs) .
The components of a markdown link
The three components of a (Hugo) markdown link:
- link text
- link destination
- link title (optional)
[link text](/link/destination/ "link title")
The context available to the link render hook
I’m currently working on adding “infolinks” (actually “infobuttons”) to links on pages that have the
infolinks param set to true, see
notes / Infolinks.
To limit how much data
gets printed to the terminal, I’ll use warnf to log some data that’s available in the context
(cursor, or dot) for the case where infolinks is true:
/zalgorithm_theme/layouts/_markup/render-link.html:
{{- if .Page.Params.infolinks }}
{{ warnf ".Page.Title: %v" .Page.Title}}
{{ warnf ".Destination: %v" .Destination }}
{{ warnf ".PlainText: %v" .PlainText }}
{{ warnf ".Text: %v" .Text }}
{{ warnf ".Title: %v" .Title }}
{{- end }}
WARN .Page.Title: A simple document for testing
WARN .Destination: /notes/polynomial-functions/#solving-polynomial-equations-for-complex-roots
WARN .PlainText: notes / Polynomial function
WARN .Text: notes / Polynomial function
WARN .Title: This link has a title
The urls.Parse function
Looking at the render-link.html, it has a
notes / template
action
that contains the
following:
{{- $u := urls.Parse .Destination }}
urls is a function (
https://gohugo.io/functions/urls/
)
that has a Parse function (
https://gohugo.io/functions/urls/parse/
). It parses a URL into a URL structure. The URL can be relative (a path without a host) or absolute. .Destination is the URL that’s being parsed.
Let’s test it out:
{{- if .Page.Params.infolinks }}
{{- $testurl := urls.Parse .Destination}}
{{ warnf "Testing the urls.Parse function: %v" $testurl }}
{{- end }}
WARN Testing the urls.Parse function: /notes/parsing-html-files-with-lxml
WARN Testing the urls.Parse function: #a-third-level-heading
WARN Testing the urls.Parse function: /notes/polynomial-functions/#solving-polynomial-equations-for-complex-roots
The results for the relative URL (#a-third-level-heading) are interesting. Somehow a path is being
prepended to it prior to it being rendered as HTML. For my purposes the path is good:
<a href="/notes/a-simple-document-for-testing/#a-third-level-heading"
>A third level heading</a
>
urls.Parse parses a URL into
URL structure (Go docs)
.
Here’s the example URL structure from the Hugo docs (that’s a bit different than what’s shown on the Go docs):
{{ $url := "https://example.org:123/foo?a=6&b=7#bar" }}
{{ $u := urls.Parse $url }}
{{ $u.String }} → https://example.org:123/foo?a=6&b=7#bar
{{ $u.IsAbs }} → true
{{ $u.Scheme }} → https
{{ $u.Host }} → example.org:123
{{ $u.Hostname }} → example.org
{{ $u.RequestURI }} → /foo?a=6&b=7
{{ $u.Path }} → /foo
{{ $u.RawQuery }} → a=6&b=7
{{ $u.Query }} → map[a:[6] b:[7]]
{{ $u.Query.a }} → [6]
{{ $u.Query.Get "a" }} → 6
{{ $u.Query.Has "b" }} → true
{{ $u.Fragment }} → bar
My (borrowed) render-link.html template handles absolute links
The template sets an $attrs variable. If $u.IsAbs rel: external
{{- /* Set attributes for anchor element. */}}
{{- $attrs := dict "href" $u.String }}
{{- if $u.IsAbs }}
{{- /* Destination is a remote resource. */}}
{{- $attrs = merge $attrs (dict "rel" "external") }}
{{- else }}
The attributes for the godoc.org link that I just added to this page:
WARN $attrs: map[href:https://godoc.org/net/url#URL rel:external]
Understanding the Go with pipeline statement
The pattern that’s confusing me can be written in general form like this:
{{ with pipeline }}
T1
{{ end }}
Where pipeline is an expression that evaluates to a value. If pipeline is not empty (non-zero,
non-nil, non-empty slice/map/string), the current context (.) will be set to the value of
pipeline and template T1 will be evaluated with that context.
The pattern could also be written as:
{{ with pipeline }}
T1
{{ else }}
T2
{{ end }}
With the {{ else }} action, if pipeline is empty, the context will stay the same as it was
before the beginning of the with block and template T2 will be evaluated with that context.
The significance of the dollar symbol in a with block
Here’s the actual code I’m looking at. This is the {{- else }} block that follows the
code I
posted above:
{{- else }}
{{- with $u.Path }}
{{- with $p := or ($.PageInner.GetPage .) ($.PageInner.GetPage (strings.TrimRight "/" .)) }}
{{- /* Destination is a page. */}}
{{- $href := .RelPermalink }}
{{- with $u.RawQuery }}
{{- $href = printf "%s?%s" $href . }}
{{- end }}
The code makes sense up to ($.PageInner.GetPage .). The current context is $u.Path. That’s just
a string (I think). It doesn’t have a PageInner field (I checked). It turns out that the dollars
sign ($) has a special meaning in with blocks. When you enter the block the cursor (.) is
rebound to the value of the pipeline. The $ symbol is a special variable that is (automatically
(?)) assigned to the initial value of the dot (.) when template execution begins.
So ($.PageInner.GetPage .) is being called on the context that was initially passed to the link
render hook. Looking at the
link render hook
docs
, PageInner is a field that’s available on
the context: “A reference to a page nested via the RenderShortcodes method.” The mention of
rendering shortcodes seems to be throwing me off. I’m fairly sure that what’s happening is that
for my case (with no shortcodes) PageInner is falling back to Page. See
notes / Hugo PageInner
links render hook field.
Essentially ($.Page.GetPage .) will be executed. If a Page can’t be found with the path (.),
($.Page.GetPage) will be called again, with the trailing slash (/) trimmed from the context.
(I’m not going to trace through the case where that might be needed.)
Page resources in Hugo
Related to the following block:
{{- else with $.PageInner.Resources.Get $u.Path }}
{{- /* Destination is a page resource; drop query and fragment. */}}
{{- $attrs = dict "href" .RelPermalink }}
{{- else with (and (ne $.Page.BundleType "leaf") ($.Page.CurrentSection.Resources.Get $u.Path)) }}
{{- /* Destination is a section resource, and current page is not a leaf bundle. */}}
{{- $attrs = dict "href" .RelPermalink }}
{{- else with resources.Get $u.Path }}
{{- /* Destination is a global resource; drop query and fragment. */}}
{{- $attrs = dict "href" .RelPermalink }}
{{- else }}
{{- if eq $errorLevel "warning" }}
{{- warnf $msg }}
{{- if and $highlightBrokenLinks hugo.IsDevelopment }}
{{- $attrs = merge $attrs (dict "class" "broken") }}
{{- end }}
{{- else if eq $errorLevel "error" }}
{{- errorf $msg }}
{{- end }}
This is still in the {{- with $u.Path }} block, so we know the URL has a path (it’s not an anchor
link). The code is dealing with page resources.
From Hugo docs (Page resources) :
Use page resources to logically associate assets with a page.
Page resources are only accessible from page bundles
A page bundle is a directory with index.md or _index.md files at its root.
There are two types of page bundles:
Leaf bundles:
content/
blog/
my-post/
index.md ← The page
image.jpg ← Page resource
document.pdf ← Page resource
data.csv ← Page resource```
Branch bundles:
```text
content/
blog/
_index.md ← The section page
header-image.jpg ← Page resource for this section
The code block at the top of this section is checking links to page resources.
Anchor links
This is what I was wondering about in the
urls.Parse function section of this
note.
My question was “how are paths getting prepended to anchor links?”
Here’s the answer:
{{- else }}
{{- with $u.Fragment }}
{{- /* Destination is on the same page; prepend relative permalink. */}}
{{- $ctx := dict
"contentPath" $contentPath
"errorLevel" $errorLevel
"page" $.Page
"parsedURL" $u
"renderHookName" $renderHookName
}}
{{- partial "inline/h-rh-l/validate-fragment.html" $ctx }}
{{- $attrs = dict "href" (printf "%s#%s" $.Page.RelPermalink .) }}
{{- else }}
The page’s relative permalink is prepended to the fragment (the anchor link) here:
{{- $attrs = dict "href" (printf "%s#%s" $.Page.RelPermalink .) }}
Note that the context (.) is the fragment/anchor at this point.
How the render-link.html template validates URL
Even though it’s the reason I’m using the template, I’m not going to get into it here. It’s handled by the inline partial defined by:
{{- define "_partials/inline/h-rh-l/validate-fragment.html" }}
It works great. I may go into it some more here: notes / Checking internal Hugo links with link render template
Rendering the actual anchor element
Finally:
{{- /* Render anchor element. */ -}}
<a
{{- with .Title }} title="{{ . }}" {{- end -}}
{{- range $k, $v := $attrs }}
{{- if $v }}
{{- printf " %s=%q" $k ($v | transform.HTMLEscape) | safeHTMLAttr }}
{{- end }}
{{- end -}}
>{{ .Text }}</a>
I guess I could have just scrolled to line 178 in the template’s code. In any case, the context (.) is back to being the initial context that’s passed to the template.
Display something:
{{- /* Render anchor element. */ -}}
<a
{{- with .Title }} title="{{ . }}" {{- end -}}
{{- range $k, $v := $attrs }}
{{- if $v }}
{{- printf " %s=%q" $k ($v | transform.HTMLEscape) | safeHTMLAttr }}
{{- end }}
{{- end -}}
>{{ .Text }}</a>
{{- if .Page.Params.infolinks }}
<button class="infolink">?</button>
{{- end }}
button.infolink {
padding: 0.125rem 0.25rem;
margin-right: 0.125rem;
}
Getting local data into infolinks
For now I’ll just post the first pass at the code:
{{- if .Page.Params.infolinks }}
{{ $rel := $attrs.rel }}
{{- if not $rel }}
{{ $fragmentsMap := site.Data.fragments.sections }}
{{ $url := $attrs.href }}
{{ $fragmentData := index $fragmentsMap $url }}
{{- if $fragmentData }}
{{ $dbId := $fragmentData.db_id }}
{{ $apiUrl := site.Params.apiUrl }}
<button class="infolink"
hx-get="{{ $apiUrl }}/fragment/{{ $dbId }}"
hx-target="#fragment-display-{{ $dbId }}">?</button>
<div id="fragment-display-{{ $fragmentData.db_id}}" class="infolink-display"></div>
{{- end }}
{{- end }}
{{- end }}
The key is getting the link’s URL ($url) from the attributes that have been previously set in the render-link.html template. The URL is then used as the index to look up the database ID that’s associated with the URL from a sections.json
Data source (Hugo docs)
.
The sections.json file is created in a Python script that I’m running locally. The script goes
through the site’s built HTML files and generates an HTML fragment (and text chunks for embedding)
for each of the site’s heading sections. The fragments are saved to an SQLite database.
(notes /
Hello htmx)
Related to
References
Hugo Documentation. “Link render hooks.” Last updated: December 19, 2025. https://gohugo.io/render-hooks/links/ .