Detect & Format Dead Links

Hello, all! I’ve been enjoying getting a basic Hugo site set up for my personal worldbuilding wiki. The nature of the wiki is that I’ll often link to articles that don’t exist yet, but which I’m planning to create later on; this means that when I do get around to making that new article, I don’t have to go back and figure out everywhere I wanted it linked.

I’m trying to set up some way of automatically identifying these “dead” links and applying a class to them, so I can format them to look different than “live” links. I thought that using the render-link hook might be the answer, but it appears I can’t access the full range of functions there. Here’s what I’ve been trying (with some additional code for formatting external links):

{{ $live := false }}
{{ range .Site.RegularPages }}
  {{ if eq .Params.destination .Destination }}
    {{ $live = true }}
  {{ end }}
{{ end }}

<a href="{{ .Destination | safeURL }}"{{ with .Title}}title="{{ . }}"{{ end }}
{{ if strings.HasPrefix .Destination "http" }}target="_blank" rel="noopener"{{ end }}
{{ if not $live }}class="dead"{{ end }}>
{{- .Text | safeHTML -}}{{ if strings.HasPrefix .Destination "http" }}<span class="material-icons"> &#xE89E</span>{{ end }}</a>

And the error I’m getting:

execute of template failed at <.Site.RegularPages>: can’t evaluate field Site in type goldmark.linkContext 

https://gohugo.io/templates/render-hooks/#context-passed-to-render-link-and-render-image

Render hooks don’t receive .Site in context. Use either .Page.Site or (preferably) the site function.

But your render hook will get very expensive.

Take a site with 100 pages. You’re ranging through 100 pages for every link. With 10 links per page that’s 100x100x10 = 100,000 iterations. For a 1000 page site that would be 10,000,000 iterations.

1 Like

I see. Right now my site is not particularly large, but I do want to create it in a sustainable way. Is there a different approach you’d recommend I look into? (By the way, I found your blog post on link and image render hooks when I was exploring this question–it was a bit over my head but what I could glean was very interesting!)

Yeah. If it were me I’d start with this, and then merge a class into the attributes map when the resource can’t be found. Keep the error/warning system as-is.

1 Like

Thank you for the quick responses! I had found your blog post when I was exploring this question earlier, but it was a bit over my head. I’ll dig in deeper and see if I can figure out how to apply it for my purposes.

It’s a trivial change…

diff --git a/layouts/_default/_markup/render-link.html b/layouts/_default/_markup/render-link.html
index ac7b00b..c64b435 100644
--- a/layouts/_default/_markup/render-link.html
+++ b/layouts/_default/_markup/render-link.html
@@ -133,6 +133,7 @@ either of these shortcodes in conjunction with this render hook.
             {{- /* Destination is a global resource; drop query and fragment. */}}
             {{- $attrs = dict "href" .RelPermalink }}
           {{- else }}
+          {{- $attrs = merge $attrs (dict "class" "broken") }}
             {{- if eq $errorLevel "warning" }}
               {{- warnf $msg }}
             {{- else if eq $errorLevel "error" }}
@@ -155,6 +156,7 @@ either of these shortcodes in conjunction with this render hook.
       {{- partial "inline/h-rh-l/validate-fragment.html" $ctx }}
       {{- $attrs = dict "href" (printf "%s#%s" $.Page.RelPermalink .) }}
     {{- else }}
+      {{- $attrs = merge $attrs (dict "class" "broken") }}
       {{- if eq $errorLevel "warning" }}
         {{- warnf $msg }}
       {{- else if eq $errorLevel "error" }}

1 Like

Thanks, I’m teaching myself from scratch (and very out of order) so it’s not trivial for me! At first attempt this is applying the “broken” format to all of my internal links regardless of if they’re broken or not. I’ll poke around a bit and see if I can figure out why.

I guess that depends on what your internal links look like. Examples?

Sure! Here is an example of markdown where I have both an internal link (to another article in the same section) and an external link (to Wikipedia). In the case of a dead link, this “mechanism.md” file wouldn’t yet exist.

In its [premature death throes](../mechanism) our sun has ballooned into a [red giant](https://en.wikipedia..org/wiki/Red_giant), cooling while increasing in size and brightness.

1) The hook currently doesn’t fail on bad remote URLs, though that can be added.

2) The link uses the .Page.GetPage method to find a page.

These will work from anywhere on the site:

/abs/path/to/mechanism.md
abs/path/to/mechanism.md
/abs/path/to/mechanism
abs/path/to/mechanism

These will work from a page in the same section:

/abs/path/to/mechanism.md
abs/path/to/mechanism.md
mechanism.md
/abs/path/to/mechanism
abs/path/to/mechanism
mechanism

I suppose we could choose to ignore the leading ../

Removing the ../ from the internal links worked! I don’t remember exactly why I had them formatted that way, I’m sure it was some accumulation of newbie misunderstandings. Thank you for your patience.

1 Like

For future reference, I studied @jmooring’s code and tried to incorporate what I learned in a way that felt more manageable for me as a beginner. I have a hangup about using code I don’t understand in my projects, even though I’m sure the original is much better! I expect this approach may need refinement, but here’s what I have working at the moment:

{{- $live := true -}}
{{- $url := urls.Parse .Destination }}

{{- if $url.IsAbs }}
{{- else}}
  {{- with $.Page.GetPage $url.Path }}
  {{- else }}
    {{- $live = false }}
  {{- end }}
{{- end }}

<a href="{{ (.Page.GetPage .Destination).RelPermalink | safeURL }}"{{ with .Title}}title="{{ . }}"{{ end }}
{{ if not $live }}class="dead" title="Coming soon!"{{ end }}
{{ if strings.HasPrefix .Destination "http" }}target="_blank" rel="noopener"{{ end }}>
{{- .Text | safeHTML -}}{{ if strings.HasPrefix .Destination "http" }}<span class="material-icons" style="font-size: 13px; vertical-align: middle; padding-left: 2px;"> &#xE89E</span>{{ end }}</a>
1 Like

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.