Strip number prefixes from ID in heading and TOC

I have content with hardcoded numbers in headings e.g 1. Hugo is great. Since the numbers appear in heading IDs and TOC links, I want to strip these out. What’s the Hugo way to do it?

Use markdown attributes to change the heading id attributes:

## 6. Section A {#section-a}

## 7. Section B {#section-b}

## 42. Section C {#section-c}

Then (optionally) use a regex to remove the numbers from the link text in the table of contents:

{{ .TableOfContents | strings.ReplaceRE `>\d+\.\s(.+)</a>` ">$1</a>" | safeHTML }}

The result is:

<nav id="TableOfContents">
  <ul>
    <li><a href="#section-a">Section A</a></li>
    <li><a href="#section-b">Section B</a></li>
    <li><a href="#section-c">Section C</a></li>
  </ul>
</nav>
<h2 id="section-a">6. Section A</h2>
<h2 id="section-b">7. Section B</h2>
<h2 id="section-c">42. Section C</h2>

Rendered:

I don’t have that privilege because much of the content goes back 10 years.

Other than passing .Content through an ugly regex, which you really don’t want to do, I don’t have any other suggestions.

This addressed the IDs in headings (suggested by ChatGPT)

{{/* Get the raw text of the heading */}}
{{ $text := .Text | plainify }}

{{/* Clean the text for the id: remove leading numbers + dot + space (e.g. "1. Foo" → "Foo") */}}
{{ $cleanID := replaceRE `^[0-9]+\. ` "" $text | urlize }}

<h{{ .Level }}
  id="{{ $cleanID }}"
  {{- range $k, $v := .Attributes -}}{{- printf " %s=%q" $k $v | safeHTMLAttr -}}{{- end }}
>
  {{ .Text | safeHTML }}
</h{{ .Level }}>

The TOC is the ‘culprit’ here now since it parses the headings rather than IDs for href.

I should clarify I just need to strip the number from ID and href. The rest of the automated id is okay.

As expected, because you can’t change the underlying id in a heading render hook. See https://github.com/gohugoio/hugo/issues/8383.

So if you’re going to change the rendered id in a heading render hook, you’ll have to pass the rendered TOC through yet another regex… yuck. Are you really sure you want to do this?

OK, I guess this isn’t so bad…

{{ .TableOfContents |
  strings.ReplaceRE `>\d+\.\s(.+)</a>` ">$1</a>" |
  strings.ReplaceRE `<a\shref="\#\d+-(.+)">` "<a href=\"$1\">" |
  safeHTML
}}

I think "<a href=\"$1\">" should be "<a href=\"#$1\">", otherwise the TOC links return 404.

hugo --logLevel info

INFO  timer:  name HeadingRenderHook count 272 duration 17.629324ms average 64.813µs median 58.886µs
INFO  timer:  name TableOfContents count 431 duration 41.417421ms average 96.096µs median 10.542µs

I assume the time here is fast.

Yeah, I omitted the hash symbol. Should be:

{{ .TableOfContents |
  strings.ReplaceRE `>\d+\.\s(.+)</a>` ">$1</a>" |
  strings.ReplaceRE `<a\shref="\#\d+-(.+)">` "<a href=\"#$1\">" |
  safeHTML
}}

And in the heading render hook you need to use anchorize not urlize, for example:

{{ $id := .PlainText | strings.ReplaceRE `^\d+\.\s` "" | urls.Anchorize }}
{{ $attr := merge .Attributes (dict "id" $id) }}
<h{{ .Level }}
  {{- range $k, $v := $attr }}
    {{- printf " %s=%q" $k $v | safeHTMLAttr }}
  {{- end }}>
  {{ .Text }}
</h{{ .Level }}>

The difference between the two functions is described here:
https://gohugo.io/functions/urls/anchorize/

Keep in mind that the above has no effect on Page.Fragments.

1 Like

Perfect! Thanks. Btw, how is the anchor being added in the Hugo Docs on hover? I checked and there was no heading render hook.

JavaScript.

1 Like

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