HUGO

Adding anchor next to headers

We already do GitHub style highlighting, so I don’t think we’re gonna include an external lib just to get the header linking going.

It was just an idea.

I like this idea and it turns out that it requires no core changes at all. Apparently hugo is already adding an id to every header on each page, so all you need is a link to them. A ghetto, poorly styled option would simply be:

$('h2').each(function() { $(this).prepend('<a href=#' + $(this).context.id + '>🔗 </a>') })

Of course you’d need to do that for all of h1 through h5 (or whatever.) But it works and requires no extra libraries or anything. I hope to make it styled like other things where the link is invisible unless you hover over it and it’s to the left of the header.

Thanks. I needed a version without jQuery so here it is:

<script>
function addAnchor(element) {
  element.innerHTML = `<a href="#${element.id}">${element.innerText}</a>`
}

 document.addEventListener('DOMContentLoaded', function () {
  var headers = document.querySelectorAll('article h2')
  if (headers) {
    headers.forEach(addAnchor)
  }
})
</script>
2 Likes

AnchorJS can be used to do this.

Being JS-illiterate, I just use this one-liner in Hugo partial :smiley:

Use it as:

{{ partial "headline-hash.html" .Content }}

Example

6 Likes

Slight variation to make it closer to a GitHub style:

.hanchor { font-size: 50%; visibility: hidden}
h2:hover a { visibility: visible}
function addAnchor(element) {
    element.insertAdjacentHTML('beforeend', `<a href="#${element.id}" class="hanchor" ariaLabel="Anchor">🔗</a>` )
}
document.addEventListener('DOMContentLoaded', function () {
    // Add anchor links to all headings
    var headers = document.querySelectorAll('article h1[id], article h2[id], article h3[id], article h4[id]')
    if (headers) {
        headers.forEach(addAnchor)
    }
 });
1 Like

Oh, and if you want to color that character something other than black, you will need to add the “variation selector” character after it.

	&#x1F517;&#xFE0E;

Will do it.

That’s because the link icon is treated as an emoji and they have their own colours, they don’t take the CSS text colour.

I’m using this now in my theme. Way better than AnchorJS since AnchorJS is client side while this is static. :smile:

4 Likes

Nice, I’m certainly going to borrow that approach - another bit of JS removed from the site!

1 Like

Here’s what I ended up with in my single.html layout:

    {{- with .Content -}}
    <div>
      {{ . | replaceRE "(<h[1-9] id=\"([^\"]+)\".+)(</h[1-9]+>)" `${1}<a href="#${2}" class="hanchor" ariaLabel="Anchor"> 🔗&#xFE0E;</a> ${3}` | safeHTML }}
    </div>
    {{- end -}}

The CSS needed (I only show anchors on headings 1-4) is:

.hanchor { font-size: 100%; visibility: hidden; color:silver;}
h1:hover a, h2:hover a, h3:hover a, h4:hover a { visibility: visible}

The anchor shows in silver when you move the mouse over the heading and turns black when you hover over the anchor itself.

Also note the ariaLabel which you should really include for accessibility.

12 Likes

Love the static versions! Thanks all!

1 Like

I wanted to have anchor before heading text. For that what I did is:

Render Content in the following manner, meaning in _default/single.html replace .Content with:

{{ .Content | replaceRE "(<h[1-6] id=\"(.+)\".*>)(.*)(</h[1-6]>)" `${1}<a href="#${2}">#</a>${3}${4}` | safeHTML }}

The regex is simply grouping header tag elements and replacing it with the same thing with an anchor tag in the middle of it.

2 Likes

I want to have the anchors before the heading.

The problem is that I use CSS counters to number my headings automatically.

_index.md

## This is heading 1

## This is heading 2

styles.css

h2::before {
	counter-increment: h2;
	content: counter(h2) ". ";
}

Frontend

1. This is heading 1

2. This is heading 2

So when I use this

the result is

1. #This is heading 1

2. #This is heading 2

But I want

# 1. This is heading 1

# 2. This is heading 2

Is this even possible?

I haven’t tried this personally:

{{ .Content | replaceRE "(<h[1-6] id=\"(.+)\".*>)(.*)(</h([1-6])>)" `${1}<a href="#${2}">${5} #</a>${3}${4}` | safeHTML }}

Basic idea is to group 1 from h1 and then utilize it. You can notice digit from heading tag (1 from h1) is included in the 5th group. And then it is used.

I tried your solution but it seems to fail. I get this error:

unexpected "{" in operand

I noticed that by quoting your code the code changed (DISCOURSE BUG?). I think that’s why your code doesn’t work (you copied from the quote with the wrong code). I figured out how to get your code to work:

{{ . | replaceRE "(<h[1-6] id=\"(.+)\".*>)(.*)(</h([1-6])>)" "${1}<a href=\"#{2}\"># ${5}. ${3}${4}" | safeHTML }}

Your code uses the number after the h and uses this as numbering. This is wrong because with your code

## This is my heading 1

## This is my heading 2

looks like

# 2. This is my heading 1

# 2. This is my heading 2

(both get the number 2 from h2)

Another solution

In your code the a-tag is included in the h-tag. (e.g. <h2><a href="bla"></a>Bla</h2>)

Another option is to place the a-tag before the h-tag (like in the code I quoted in my first post). And give the a-tag a class with the number of the h-tag and change the CSS:

<a class="h2-anchor" href="bla">#</a><h2>Bla</h2>

styles.css

h2-anchor::before {
	counter-increment: h2;
	content: counter(h2) ". ";
}

…but I don’t know how the modify the Regex to my solution :confused:

{{ .Content | replaceRE "(<h[1-6] id=\"(.+)\".*>)(.*)(</h([1-6])>)" `<a class="h${5}-anchor" href="#${2}">#</a>${1}${3}${4}` | safeHTML }}
1 Like

Your code

{{ .Content | replaceRE "(<h[1-6] id=\"(.+)\".*>)(.*)(</h([1-6])>)" "<a class=\"h${5}-anchor\" href=\"#${2}\">#</a>${1}${3}${4}" | safeHTML }}

(…I just escaped the " of the class…)

worked like charm. Thank you!

The only thing I had to modify was to set my headings to inline (if not the a-tag and the h-tag will appear on two different lines) and to adapt the font size of the a-tag to that of the h-tag).

It works and looks really good.

BTW: I saw it first on this page.

Summary

I have a partial with the name headings-anchor.html which I call in my list.html with {{ partial "headings-anchor.html" .Content }}. Then I choose one of the following options:

Anchor after heading

{{ . | replaceRE "(<h[2-6] id=\"([^\"]+)\".+)(</h[2-6]+>)" "${1}&nbsp;<a class=\"anchor\" href=\"#${2}\">#</a> ${3}" | safeHTML }}

Output

My heading 1 # 

My heading 2 #

Anchor before heading (without CSS counters)

{{ . | replaceRE "(<h[2-6] id=\"(.+)\".*>)(.*)(</h[2-6]>)" "${1}<a href=\"#${2}\">#</a>${3}${4}" | safeHTML }}

Output

# My heading 1

# My heading 2

Anchor before heading (with CSS counters)

{{ . | replaceRE "(<h[2-6] id=\"(.+)\".*>)(.*)(</h([2-6])>)" "<a class=\"h${5}-anchor\" href=\"#${2}\">#</a>${1}${3}${4}" | safeHTML }}

Output

# 1. My heading 1

# 2. My heading 2

For the last option I use the following CSS:

CSS counters


h1 {
  counter-reset: h2;
}

h2::before {
	counter-increment: h2;
	content: counter(h2) ". ";
}

h2 {
  counter-reset: h3;
}

h3::before {
	counter-increment: h3;
	content: counter(h2) "." counter(h3) ". ";
}

h3 {
  counter-reset: h4;
}

…and so on.

h-anchor and h on same line

h2,
h3,
h4,
h5,
h6 {
    display: inline;
}

Set space between # and heading

.h2-anchor,
.h3-anchor,
.h4-anchor,
.h5-anchor,
.h6-anchor {
    margin-right: 1em;
}

and the font sizes of h-anchor and h should correspond.

Maybe it’s worth mentioning that replaceRE over the .Content here isn’t necessary anymore, since this issue could now be solved by using markdown render hooks.

2 Likes