Skip to content

Create header links: Hugo vs JavaScript

GitHub adds header links to markdown documents by default. I like the way this looks and how it makes sharing link fragments easier, so I added the feature to my lil boot theme.

The first iteration was with JavaScript.

let icon = '<i data-feather="link"></i>';
let headings = document.getElementsByTagName('h2');
for (let i = 0; i < headings.length; i++) {
  let id = headings[i].id;
  let iconLink = '<a class="header-icon-link" href="#' + id + '">' + icon + '</a> ';
  let headingWithIconLink = iconLink + headings[i].innerHTML;
  headings[i].innerHTML = headingWithIconLink;
}

What the above does:

This code worked just fine. But I wanted to do as much with Hugo as possible, so I asked the folks over on the forums for assistance. The helpful kaushalmodi replied with just the solution I needed. With a few tweaks to adapt it to my needs, here’s what the Hugo iteration looks like:

{{ . | replaceRE "(<h[2-9] id=\"([^\"]+)\">)(.+)(</h[2-9]+>)" `${1}<a class="header-icon-link" href="#${2}"><i data-feather="link"></i></a> ${3}${4}` | safeHTML }}

If you’re like me, you’re thinking “wtf did I just read?” Yes. That was my initial response as well. I had to plug that regex into an online playground (don’t forget to choose the GoLang flavor) to figure out what was going on. So, let’s break it down:

I will assume you’re already familiar with the “dot”, replaceRE, and safeHTML, since they’re things from Hugo-land. But if not, search the docs for each of those things and read up.

(<h[2-9] id=\"([^\"]+)\">)(.+)(</h[2-9]+>) is a regular expression. Each set of parentheses is a “group” and will return a “match”. Given the header <h2 id="some-header">Some Header</h2>, see the following table for how it will be parsed.

Group Regex Match
1 (<h[2-9] id=\"([^\"]+)\"> <h2 id="some-header">
2 ([^\"]+) some-header
3 (.+) Some Header
4 (</h[2-9]+>) </h2>

When building the replacement string, each group match can be referenced by its number. Say you want to use the value of group 1’s match, you’d do ${1}.

And there you have it. To see the code in action, checkout this demo.