Hugo + js = copy heading links

Hugo + js = copy heading links


Updated 2021-05-13: Added demo headings

Example on how to add copy-link buttons to headings for the convince of the user. While this is not the most important feature it is easy enough to implement so why not add it.

The (fairly lightweight) clipboardjs library is used to copy the code to clipboard. At the time of writing 2.0.6.

For the tooltips the tooltips CSS library is used.

This article was written for Hugo 0.71.

Usage

Hugo automatically generates the headings, so nothing else has to be done. The script will automatically wire the copy-link buttons and we’re done.

Demo

Here are some headers. Hoover over them to reveal the copy button.

Level deeper

Some text

Another level deeper

Other text

Yet deeper

Other text

The end (this is a heading)

Other text

Code

The overwritten Hugo’s markdown heading template, located at /layout/_default/_markup/render-heading.html. Because this template is used for the whole site and we only want heading links on single pages adding the copy-link button is conditional.

1
2
3
4
5
{{- if .Page.IsPage -}}
<h{{ .Level }} id="{{ .Anchor }}">{{ .Text | safeHTML }}<span class="headingAnchor" data-clipboard-text="{{ .Page.Permalink }}#{{ .Anchor }}">{{ partial "inline-svg" "link" }}</span></h{{ .Level }}>
{{- else -}}
<h{{ .Level }} id="{{ .Anchor }}">{{ .Text | safeHTML }}</h{{ .Level }}>
{{- end -}}

The copy to clipboard javascript, wire up the hide tooltip events and set-up clipboardjs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
window.addEventListener("load", function () {
    // Wire hide tooltip
    document.querySelectorAll('.headingAnchor').forEach(e => {
        e.addEventListener('mouseleave', clearTooltip);
        e.addEventListener('blur', clearTooltip);
    });

    const clipboard = new ClipboardJS('.headingAnchor');
    clipboard.on('success', function (e) {
        e.clearSelection();
        showTooltip(e.trigger, 'Copied!');
    });
    clipboard.on('error', function (e) {
        showTooltip(e.trigger, fallbackMessage(e.action));
    });
});

function clearTooltip(e) {
    e.currentTarget.classList.remove('tooltipped', 'tooltipped-s');
    e.currentTarget.removeAttribute('aria-label');
}

function showTooltip(elem, msg) {
    elem.classList.add('tooltipped', 'tooltipped-s');
    elem.setAttribute('aria-label', msg);
}

function fallbackMessage(action) {
    const actionMsg = ''; var actionKey = (action === 'cut' ? 'X' : 'C'); if (/iPhone|iPad/i.test(navigator.userAgent)) { actionMsg = 'No support :('; }
    else if (/Mac/i.test(navigator.userAgent)) { actionMsg = 'Press ⌘-' + actionKey + ' to ' + action; }
    else { actionMsg = 'Press Ctrl-' + actionKey + ' to ' + action; }
    return actionMsg;
}

The accompanying SCSS.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
.headingAnchor {
    color: var(--primary-color);
    font-size: 0.75em;
    cursor: pointer;
    margin-left: 0.5em;
    position: absolute;
    visibility: hidden;
}

// Note it does not include h1
h2:hover .headingAnchor,
h3:hover .headingAnchor,
h4:hover .headingAnchor,
h5:hover .headingAnchor,
h6:hover .headingAnchor {
    visibility: visible;
}

Don’t forget to include clipboardjs and tooltips CSS.

Noticed an error in this post? Corrections are appreciated.

© Nelis Oostens