Zalgorithm

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 three components of a (Hugo) markdown link:

[link text](/link/destination/ "link title")

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

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.

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;
}

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)

References

Hugo Documentation. “Link render hooks.” Last updated: December 19, 2025. https://gohugo.io/render-hooks/links/ .