Validation for links <a href="#xyz"> to anchors <a id="xyz"> in the same page

I’m trying to include validation for internal links (i.e. within the site) in the link render hook. Basically to make Hugo throw a warning upon building, if there is a mistyped link. It works mostly fine, but I get a false positive case, see below:

  1. Link to same page: clean (no warning)
  2. Correct link to other page: clean
  3. Wrong link: warning
  4. Correct link to other page heading: clean
  5. Wrong link to other page heading: warning
  6. Correct link to same page heading: clean
  7. Wrong link to same page heading: warning
  8. Correct link to other page anchor: clean
  9. Wrong link to other page anchor: warning
  10. Correct link to same page anchor: warning
  11. Wrong link to same page anchor: warning

I distinguish between headings and anchors because headings (# xyz, ## xyz and the such) become part of a page’s Fragments, but arbitrary anchors like <a id="xyz">...</a> do not.

Question 1: is there a way to have Hugo make any element with id attribute a valid fragment? This would solve everything.

I tried to find a workaround by detecting any element with the searched id in the .Content of the destination page with findRE. This actually works, except if the link leads to an element within the same page - see case 10 above.

Apparently -again- Hugo does not allow access to the .Content of the same page that contains the link from the render hook. It works fine for the .Content of other pages.

Question 2: is there a reason for not accessing the .Content of the same page through the link render hook, but other things like .Title or .Fragments are accessible? Do you think there can be a workaround for that?

Thanks :grinning_face:

Content structure:

.
└── content/
    └── posts/
        ├── post-1
        └── post-2

Render hook:

{{- $u := urls.Parse .Destination -}}

{{- /* Initializing destination page and its title */ -}}
{{- $DestPage := .Page -}}
{{- $InternalLinkTitle := "" -}}

{{- /* Validation only if the link is internal, i.e. not absolute */ -}}
{{- if not $u.IsAbs -}}
	
	{{- /* If there is a path, then get its page and title */ -}}
	{{- if $u.Path -}}
		{{- $DestPage = .Page.GetPage $u.Path -}}
		{{- $InternalLinkTitle = $DestPage.Title -}}
	{{- end -}}
	
	{{- /* If a destination page exists (i.e. valid path or same page), validate the fragment */ -}}
	{{- if $DestPage -}}
	
		{{- /* If there is a fragment in the link */ -}}
		{{- if $u.Fragment -}}
			
			{{- /* If the fragment is not found in the destination page fragments */ -}}
			{{- if not ($DestPage.Fragments.Identifiers.Contains $u.Fragment) -}}
				
				{{- /* Look for the fragment in arbitrary element id attributes */ -}}
				{{- $FragmentRegex := print "<\\S+?.*? id=\"" $u.Fragment "\".*?>.*?</\\S+?>" -}}
				{{- $DetectedFragment := findRE $FragmentRegex $DestPage.Content 1 -}}
				
				{{- /* Finally, if the fragment is not detected at all then give the warning */ -}}
				{{- if not $DetectedFragment }}
					{{- warnf "Broken link fragment in %s: [%s](%s)" .Position .Text .Destination -}}
				{{- end -}}
			{{- end -}}
		{{- end -}}
		
	{{- /* If a destination page doesn't exist, the link is broken */ -}}
	{{- else -}}
		{{- warnf "Broken link path in %s: [%s](%s)" .Position .Text .Destination -}}
	{{- end -}}
{{- end -}}

{{- /* Render the link */ -}}
<a href="{{ .Destination | safeURL }}"
	{{- if $u.IsAbs -}}
		{{- with .Title }} title="{{ . }}"{{ end }} target="_blank" rel="external"
	{{- else -}}
		{{- with $InternalLinkTitle }} title="{{ . | safeHTMLAttr }}"{{- end -}}
	{{- end -}}>
  {{- with .Text }}{{ . }}{{ end -}}
</a>
{{- /* Chomp trailing newline */ -}}

Page 1:

---
title: "Post 1"
author: ""
date: ""
---

## Section 1

[1. link to same page](/posts/post-1/)

[2. correct link to other page](/posts/post-2/)

[3. wrong link](/posts/post-3/)

[4. correct link to other page heading](/posts/post-2/#section-1)

[5. wrong link to other page heading](/posts/post-2/#section-3)

[6. correct link to same page heading](#section-2)

[7. wrong link to same page heading](#section-3)

[8. correct link to other page anchor](/posts/post-2/#anchor)

[9. wrong link to other page anchor](/posts/post-2/#anchor-2)

[10. correct link to same page anchor](#anchor)

[11. wrong link to same page anchor](#anchor-2)

## Section 2

<a id="anchor"></a>

Page 2:

---
title: "Post 2"
author: ""
date: ""
---

## Section 1



## Section 2

<a id="anchor"></a>


Warnings:

WARN  Broken link path in "content\posts\post-1\index.md:14:1": [3. wrong link](/posts/post-3/)
WARN  Broken link fragment in "content\posts\post-1\index.md:18:1": [5. wrong link to other page heading](/posts/post-2/#section-3)
WARN  Broken link fragment in "content\posts\post-1\index.md:22:1": [7. wrong link to same page heading](#section-3)
WARN  Broken link fragment in "content\posts\post-1\index.md:26:1": [9. wrong link to other page anchor](/posts/post-2/#anchor-2)
WARN  Broken link fragment in "content\posts\post-1\index.md:28:1": [10. correct link to same page anchor](#anchor)
WARN  Broken link fragment in "content\posts\post-1\index.md:30:1": [11. wrong link to same page anchor](#anchor-2)

Using Hugo v0.164.0

It’s the chicken and egg problem in which there are no solution. It creates an infinite loop.

Thanks for your response, I suspected that to be honest. Is there a reason Hugo doesn’t make <a id="xyz"> elements .Fragments (unlike <h2 id="xyz">, which do become .Fragments)?

Fragments maps to .TableOfContents. I would flip your question around, is there a reason to include links? I understand your question re link validation, but 1) That was not the primary use case of Fragments and 2) It seem like an odd case to link to … links.

Ok, I understand. Thanks again :slightly_smiling_face:

To explain, I sometimes use <a id="xyz"> elements without href to link to a point in the text that is not a heading.

I remember vaguely that we do something similar for description lists in the Hugo Docs repo, so you may want to have a look there.

in your case where you control the anchor in your markdown you could scan .RawContent at the top of the template and .Store the id.
scan that list in the Link-render-hook

I wasn’t familiar with .RawContent so I read the documentation. Seemed to be exactly what I needed.

Amazingly, it worked like a charm once I just changed this line in the link render hook:
{{- $DetectedFragment := findRE $FragmentRegex $DestPage.Content 1 -}}

to this:
{{- $DetectedFragment := findRE $FragmentRegex $DestPage.RawContent 1 -}}

Without needing to .Store at the page template (if I understood you correctly).

I also tweaked the regex a little bit because some anchors were still missed due to unrelated reasons. This is the one I ended up using:
{{- $FragmentRegex := print "<\\S+?.*? id=\"" $u.Fragment "\".*?>??.*?<??/\\S*?>" -}}
(I don’t claim expertise in regex, but it gets the job done for now)

I will mark your answer as the solution because it pointed me to the right direction.

Thank you :smiley:

Your Solution scans the Raw Content each time an in page fragment is used.
The Store solution avoids that at the cost of creating the Store (on every page) . The check later is a simple lookup.

if your site builds fast enough I would use the simple one and just mark it as “PerfRelevent”. The break even depends on your whole site content. just set a marker.

Tip: use raw strings for your regex creation.

{{ printf `<\S+?.*?id="%s".*?>??.*?<??/\S*?>` $u.Fragment }}
{{ print `<\S+?.*?id="` $u.Fragment `%s".*?>??.*?<??/\S*?>` }}

p.s. imho the store could be created on affected pages only and
it would also allow for checking unused/misspelled anchors…

I understand, and also think it feels “tidier” as you describe it, even though I don’t have any issue with build time.

I didn’t know you could use raw strings when including a variable, so thanks for that too!