How would you apply render hooks to layouts, HTML shortcodes, partials, non-markdown templates, etc.?

In Jekyll I wrote a little plugin that let me hook into all links being rendered on my site with ~500+ posts and assorted pages, it doesn’t matter if it’s Markdown or HTML. It will look for all external links and add a few attributes to those links.

I saw Hugo has something similar with render hooks Link render hooks | Hugo, there’s even an example to to modify external links.

However, the big issue here is it seems to only apply to Markdown syntax in content files.

I happen to have dozens of links outside of the content files. They are in various layout files as well as HTML based shortcodes.

In any case, are there any options to globally sweep all tags (links, headers, etc.) to modify them based on conditions that aren’t limited to Markdown content files?

This is one of the last technical hurdles for me to cross to convert my site from Jekyll to Hugo. Any help would be much appreciated!

While I don’t know Go properly, I’d be open to modifying the source code and compiling a custom version of Hugo if it came down to it.

Could you provide some examples of what you want to achieve. how is working now for you and how you want to work, as hard to understand how to help you from your initial post.

Render hook only works on Markdown content.

I suggest you create the links in your templates with all needed attributes etc. directly. It is a one time job.

If the links are really complicated you could create a partial to create them and use that in all your templates. That gives you one place for your link creation codes.

Partials can be used from render hooks as well as shortcodes.

Could you provide some examples of what you want to achieve

Links can be internal or external. Historically I’ve added _target="_blank" to all external links. While I don’t do this personally you could also choose to append an icon next to the link text to denote it’s an external link.

Another use case is around adding a # link next to certain headers (h1, h2, etc.) to make it easy to copy the link and include <a name="..."> so it jumps there. Again, render hooks make this easy for Markdown files but it doesn’t work on the rest of your site. Your layouts and other HTML templates may have a bunch of these references.

Jekyll solves this problem in a different way by letting you create filters based on the final rendered HTML. Then you can use any Ruby library like Nokogiri to parse the HTML and use CSS selectors which lets you easily loop through all of the links in the HTML and modify their attributes if that meet whatever condition you want. The same strategy can be used to modify anything in the HTML.

It is a one time job but as I add more pages, it means having to remember to add those attributes manually, they could be easy to forget. Using a partial is an option but that means your code base now has multiple ways to create links (regularly and with the partial).

Is there a fallback strategy that works outside of Markdown, even if it’s a completely different implementation?

Well, you could include a DOMContentLoaded event listener in JavaScript that fixes all your links. Adding some overhead, of course.

True, this also has some subtle differences potentially. For example if you set rel="nofollow" that instructs search engines that you don’t want to provide that link’s domain backlink credit. If you do that sort of thing client side with JS, it’s possible that crawlers don’t respect it.

I was thinking another option could be to extract my Jekyll plugin into a standalone Ruby script and run it after Hugo builds the site. Since it just operates on HTML, it doesn’t matter where that HTML came from. This will slow down and complicate builds though.

Via render hooks layouts/_default/_markup/render-link.html you can add automatically, to all external links target="_blank" rel="noopener" like that.

{{- $u := urls.Parse .Destination -}}
{{- $href := $u.String -}}
{{- if strings.HasPrefix $u.String "#" }}
  {{- $href = printf "%s#%s" .Page.RelPermalink $u.Fragment }}
{{- else if not $u.IsAbs -}}
  {{- with or
    ($.Page.GetPage $u.Path)
    ($.Page.Resources.Get $u.Path)
    (resources.Get $u.Path)
  -}}
    {{- $href = .RelPermalink -}}
    {{- with $u.RawQuery -}}
      {{- $href = printf "%s?%s" $href . -}}
    {{- end -}}
    {{- with $u.Fragment -}}
      {{- $href = printf "%s#%s" $href . -}}
    {{- end -}}
  {{- end -}}
{{- end -}}
{{- $attributes := dict "href" $href "title" .Title -}}
<a
  {{- range $k, $v := $attributes -}}
    {{- if $v -}}
      {{- printf " %s=%q" $k $v | safeHTMLAttr -}}
    {{- end -}}
  {{- end -}}
  
  {{- if strings.HasPrefix .Destination "http" -}}
      target="_blank" rel="noopener"
  {{- end -}}

  >{{ .Text | safeHTML }}</a>
{{- /**/ -}}

You can add icon to external links via CSS like that

.article-entry a[target="_blank"]::after { 
  content: " ↗";
  vertical-align: super;
  font-size: var(--h5);
  margin-left: 0.15rem;
  display: inline-block;
  text-decoration: none !important;
  color: var(--rss);
}

For headings render-headings.html

<h{{ .Level }} {{- range $k, $v := .Attributes -}}{{- printf " %s=%q" $k $v | safeHTMLAttr -}}{{- end -}} id="{{ .Anchor | safeURL }}">{{ .Text | safeHTML }} <button type="button" class="anchor" data-title="{{ T "AnchorText" }}{{ .Text | safeHTML }}" aria-label="{{ T "AnchorText" }}{{ .Text | safeHTML }}" onclick="navigator.clipboard.writeText({{ print .Page.Permalink "#" .Anchor | safeURL }});this.insertAdjacentHTML('afterend', '<div class=link-copied>{{ T "LinkCopied" }}</div>');setTimeout(() => { document.querySelectorAll('.link-copied').forEach(el => el.remove()); }, 3000);"><svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 96 960 960" width="1em"><path d="M450 776H280q-83 0-141.5-58.5T80 576q0-83 58.5-141.5T280 376h170v60H280q-58.333 0-99.167 40.765-40.833 40.764-40.833 99Q140 634 180.833 675q40.834 41 99.167 41h170v60ZM325 606v-60h310v60H325Zm185 170v-60h170q58.333 0 99.167-40.765 40.833-40.764 40.833-99Q820 518 779.167 477 738.333 436 680 436H510v-60h170q83 0 141.5 58.5T880 576q0 83-58.5 141.5T680 776H510Z"/></svg></button></h{{ .Level }}>

Explanation of that: Supercharge your headings in Hugo with Render Hooks – Dariusz Więckiewicz 🇬🇧

Hope any of this helps.

Yep, I’ve done things like that. The problem is related to it only applying to Markdown files. I started this thread to see if hooks can be applied to other files.

Just an idea

In layouts you could operate on .Content or even .ContentRaw do some processing with Regex replace and output the result

{{ $content := .Content }}
{{ $content := $content | replaceRe pat repl }}
{{ $content | safeHTML }} or so

Instead of {{ .Content }}

That’s almost what the Jekyll plugin does except instead of being limited to a regex you can execute any arbitrary custom code, such as properly parsing an HTML tree with a library.

I don’t suppose this is possible with Hugo? I didn’t see anything in the docs that hinted at a way to run any custom code as a custom filter.

This has been discussed and the powers that be decided against it for security reasons.

Are you creating new templates every time you create a new page? Some kind of one off landing pages?