Supercharge your headings in Hugo with Render Hooks

Contents

The great thing about Hugo, a static site generator, is that it got a lot of options for customisation and more are constantly added.

There are templates embedded into it, but they can be easily overridden by custom templates, like headings, through render hooks.

When you write text with markdown, a single hash # resolve as the title of the document. This symbol is not really in use in the content that you writing in Hugo and is replaced with title used in frontmatter when the website template is set right.

---
title:
---

However, other symbols, like ## resolve as headings, adding sections to the text you are writing. In this instance, this double hash resolves as <h2>.

In the default approach, Hugo will generate it from markdown into HTML in the following way:

<h2 id="this-is-your-second-heading">This is your second heading</h2>

With just a simple tweak, but adding render-heading.html placed in \layout\_default\_markup\ you can take control of how these parts will be generated. For example, you can add a direct link to the heading like mentioned here.

<h{{ .Level }} id="{{ .Anchor | safeURL }}">{{ .Text | safeHTML }} <a href="#{{ .Anchor | safeURL }}">¶</a></h{{ .Level }}>

You can hover over the symbol, or whatever you decide to use (I used initially #) and by right-clicking on it you can copy a link to that section.

This super simple approach is great, but I would like to get something more.

I have been inspired by Google Search Central Documentation and their approach to linking to headings of the document.

By default, an icon (button) showing that the heading got a link is only reviled when you hover your mouse on it (on desktop). At that point, you will see a link icon on the side, which will give you the ability to copy a link to the clipboard when you click on it.

You cannot hover over it and do a right-click to get a link, as it works as a button rather than a standard link (<a href), but there are other things that are appealing in a matter of usability.

Firstly, it is considered at the beginning to give the user feedback on wheat this icon is. When you hover your mouse over it you will have a tooltip saying Copy link to this section: .... This incorporate accessibility (a11y) approach in the background with the utilisation of aria-label.

When you click on this “not-typical” button, the link will be copied into your clipboard and additionally, you will be informed about that by a pop-up box in the left bottom corner (on desktop) or on the bottom of your screen (on mobiles).

This is something that I am interested to achieve.

I put this into a todo list:

  • Full link icon visible when hovered over, or when clicking on the heading (on mobile devices). In other instances is still visible but with less transparency;
  • Copy the link to the clipboard when clicked on it;
  • Tooltip when hovering over (on desktop);
  • Receive a visual prompt about copy to clipboard action.

I always want a simple approach. I know that to achieve the above I will need to look into involving some JavaScript magic, but do I?

Considering my approach to simplicity, I want to do this with performance in mind. Less reliance on JavaScript is better as some dudes are surfing without JavaScript enabled.

But first I want to improve my heading and then look into ways how it’s implemented on the example from Google Search Central.

The base heading with anchor

The base render-heading.html currently looks as follows.

<h{{ .Level }} id="{{ .Anchor | safeURL }}">{{ .Text | safeHTML }} <a class="anchor" href="#{{ .Anchor | safeURL }}">#</a></h{{ .Level }}>

I changed to the # symbol.

The anchor problem

The problem with this approach is when somebody will highlight text on your website and copy it into the clipboard, it will copy this to this # symbol as well.

Also, when printing a page, this will be visible.

The printing aspect can be sorted by hiding it by implementing simple CSS.

@media print {
	a.anchor { display: none; }
}

The anchor solution

To solve this problem I decided to change my # symbol to an icon in SVG.

In theory, it was simple, however, as you notice, each heading got a different size controlled by a specified font size. I would like to have SVG icons responsible based on that.

I can specify in CSS the size of the SVG icon for each heading, but that’s adding unnecessarily 5 lines (for h2 to h6). If you remove CSS the icon will display huge!

Then I found the post Control Icons with Font Size quickly explaining how to have icons aligned with text by just implementing width="1em" height="1em" into our SVG and controlling the rest through font size in our CSS.

This approach let me rethink the use of SVG images on my, and others, website. I decided to revisit all of them with disabled styles (in Safari > Develop > Disable Styles) to see where my SVGs are huge and how to change it. This reminds me of a CSS Naked Day that typically runs on the 9th of April.

Before I will go any further, I need my icon. For this purpose, I used a link icon from Material Symbols in Google Fonts, where I just downloaded SVG file.

Simple approach

Let class="anchor" be partly visible by default and be shown fully when the mouse will hover over the heading.

The problem is, that this will only work on devices where you can hover (typically when used with a pointer). On any touch devices like mobiles (with touch screens), this will not work.

I have looked at how to implement this to work independently if you hover or you touch. For this purpose, I found out very useful video Dealing with hover on mobile by Kevin Powell

So I used media classes in CSS to work differently on hover-able devices and touch-able.

Visualisation

At first, the class="anchor" will be visible with some transparency and will become fully visible when hovering over the heading.

On the second, it will be fully visible from the start as the hover effect doesn’t apply to it.


a.anchor {
  color: var(--main);
  text-decoration: none !important;
}

@media (hover: hover) {
	a.anchor {
	  opacity: 0.6;
	}
  h2:hover a.anchor,
  h3:hover a.anchor,
  h4:hover a.anchor,
  h5:hover a.anchor,
  h6:hover a.anchor {
    opacity: 1;
  }
}

@media (hover: none) {
	a.anchor {
	  opacity: 1;
	}
}

Let’s don’t forget (as mentioned earlier) to remove our anchor from prints.

@media print {
	a.anchor { display: none; }
}

My variable color: var(--main); is specified at the beginning of my CSS file as:

:root {
  --main: #137faa;
}

Accessibility for all

I have added a descriptive label for my anchor link using aria-label for users using screen readers, but also title, to add descriptive information for all others, to let them know what is the purpose of it.

<h{{ .Level }} id="{{ .Anchor | safeURL }}">{{ .Text | safeHTML }} <a class="anchor" href="#{{ .Anchor | safeURL }}" title="Link to section: {{ .Text | safeHTML }}" aria-label="Link to section: {{ .Text | safeHTML }}">#</a></h{{ .Level }}>

Advanced approach

Some of you may stay at the simple approach, but others, like me, would like to experiment a bit further.

Here I will cover relacing # with the SVG link icon that I downloaded earlier and presenting a visual tooltip.

Let’s change a bit in our downloaded SVG link icon by opening it in VS Studio Code or any other editor.

There I changed it from:

<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48">

To:

<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 96 960 960" width="1em">

By copying the whole code in an SVG file I paste it into my render-heading.html file in place of the # symbol.

To get this displayed on the side of the heading, I just added a small fix in CSS

a.anchor > svg { display: inline-block; }

This way I got a link icon that inherits size from the heading font size as well as changing its opacity when hovered over.

There was just a small issue here.

When I used # inside <a class="anchor" the symbol inherits the colour of the links. In my case blue-ish.

The predefined anchor colour as below does not work any more.

a.anchor { color: var(--main); }

This is because I replace text, that colour is defined by the CSS color option, with an icon, that colour is based on CSS fill element.

After changing color to fill all come back to the intended behaviour.


This way I completed this part:

  • Full link icon visible when hovered over, or when clicking on the heading (on mobile devices). In other instances is still visible but with less transparency;

The default behaviour of a link, when you click on it, under the # or SVG link icon is that your browser will head to it. The URL will change to the one that includes a link to the header and the site will scroll. At this stage, you can copy the link from the browser bar to get a pointer to a specified header in the text.

This default behaviour is not bad, however, not everybody would like this type of link to behave like that.

The ideal behaviour will be copying the anchor link to the clipboard without changing the URL in the browser but that will require replacement of the anchor with a button (like Google did).

Let’s start with implementation into anchor before we will work with the button later.

The easiest way to add this type of behaviour will be to add onclick action.

onclick="navigator.clipboard.writeText({{ print .Page.Permalink "#" .Anchor | safeURL }});"

Hugo will print the current page link, merge it with the # symbol and link to the header.

A part of the code will look like that:

<a class="anchor" href="#{{ .Anchor | safeURL }}" title="{{ T "AnchorText" }}{{ .Text | safeHTML }}" aria-label="{{ T "AnchorText" }}{{ .Text | safeHTML }}" onclick="navigator.clipboard.writeText({{ print .Page.Permalink "#" .Anchor | safeURL }});">

The link will still behave as a link when is clicked, but will also copy the address into the clipboard.


The {{ T "AnchorText" }} is specified in i18n\en.toml file as:

[AnchorText]
other = "Link to section: "

If you are not using multilanguage implementation on your website you can hardcode this text there.


This way I completed this part:

  • Copy the link to the clipboard when clicked on it;

Converting anchor into a button

You may ask what to do if we don’t want a link (as above) to behave as a link (redirect to address) but just do one thing only, which is copying the address into the clipboard.

If you will start looking into this through Google, some people will advise you to leave it as an anchor (<a) but remove href= reference. Please don’t!. Instead, let’s replace this link with a button as Google did in their documentation.

Example from Google:

<button type="button" class="devsite-heading-link button-flat material-icons" aria-label="Copy link to this section: What is SEO?" data-title="Copy link to this section: What is SEO?" data-id="what-is-seo"></button>

At the current stage, my code for render-heading.html, with implemented inline SVG link symbol instead # symbol, looks like that:

<h{{ .Level }} {{- range $k, $v := .Attributes -}}{{- printf " %s=%q" $k $v | safeHTMLAttr -}}{{- end -}} id="{{ .Anchor | safeURL }}">{{ .Text | safeHTML }} <a class="anchor" href="#{{ .Anchor | safeURL }}" title="{{ T "AnchorText" }}{{ .Text | safeHTML }}" aria-label="{{ T "AnchorText" }}{{ .Text | safeHTML }}" onclick="navigator.clipboard.writeText({{ print .Page.Permalink "#" .Anchor | safeURL }});"><svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 96 960 960" width="1em"><path d="M450 776H280q-83 0-141.5-58.5T80 576q0-83 58.5-141.5T280 376h170v60H280q-58.333 0-99.167 40.765-40.833 40.764-40.833 99Q140 634 180.833 675q40.834 41 99.167 41h170v60ZM325 606v-60h310v60H325Zm185 170v-60h170q58.333 0 99.167-40.765 40.833-40.764 40.833-99Q820 518 779.167 477 738.333 436 680 436H510v-60h170q83 0 141.5 58.5T880 576q0 83-58.5 141.5T680 776H510Z"/></svg></a></h{{ .Level }}>

Let’s replace <a href= with <button type="button" at first and adjust the CSS accordingly.

<h{{ .Level }} {{- range $k, $v := .Attributes -}}{{- printf " %s=%q" $k $v | safeHTMLAttr -}}{{- end -}} id="{{ .Anchor | safeURL }}">{{ .Text | safeHTML }} <button type="button" class="anchor" title="{{ T "AnchorText" }}{{ .Text | safeHTML }}" aria-label="{{ T "AnchorText" }}{{ .Text | safeHTML }}" onclick="navigator.clipboard.writeText({{ print .Page.Permalink "#" .Anchor | safeURL }});"><svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 96 960 960" width="1em"><path d="M450 776H280q-83 0-141.5-58.5T80 576q0-83 58.5-141.5T280 376h170v60H280q-58.333 0-99.167 40.765-40.833 40.764-40.833 99Q140 634 180.833 675q40.834 41 99.167 41h170v60ZM325 606v-60h310v60H325Zm185 170v-60h170q58.333 0 99.167-40.765 40.833-40.764 40.833-99Q820 518 779.167 477 738.333 436 680 436H510v-60h170q83 0 141.5 58.5T880 576q0 83-58.5 141.5T680 776H510Z"/></svg></button></h{{ .Level }}>
button.anchor {
  fill: var(--main);
  display: inline-block;
}

@media (hover: hover) {
	button.anchor {
	  opacity: 0.6;
	}
  h2:hover button.anchor,
  h3:hover button.anchor,
  h4:hover button.anchor,
  h5:hover button.anchor,
  h6:hover button.anchor {
    opacity: 1;
    cursor: pointer;
  }
}

@media (hover: none) {
	button.anchor {
	  opacity: 1;
	}
}

By default, our anchor link inherits styling from the main anchor. When we replace it with a button we need to remove some default stylings so it will look the same (without button borders).

Added also cursor behaviour to a button to change into a pointer when hovered over, which is typical behaviour for links, but not necessarily for buttons. Thanks for the suggestion for Sid.

I did that by replacing button.anchor with the following:

button.anchor {
  fill: var(--main);
  background-color: inherit;
  border: none;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: inherit;
  padding: 0;
}

Let’s don’t forget to remove the button from printed pages

@media print {
	button.anchor { display: none; }
}

This way we got an anchor button that acts like an anchor. Browsers do not follow the link but only copy the link into the clipboard.

When we click on our new button, the problem is that we don’t have any prompt or visualisation that the copy to clipboard even happens. This is where we need to add something more so the user will know what happens unless they hover over the button for a couple of seconds to get the title= output showing (on desktop).

To add the visualisation we will get rid of the title= element, and replace it with something else.

Visual tooltip on button hover

My render-heading.html contains aria-label for accessibility and title for visual information when you hover over the link (on desktop).

The default behaviour of the title attribute is strictly dependent on the browser and appears after a while when hovering over the element with it.

If I want to use this for a visual tooltip I will resign from the title attribute in favour of the use of data-* attribute to embed custom data (in my case data-title).

In my code I replaced title= with data-title=. This will remove default browser behaviour, as it will be missing the title attribute, but this will not have an impact unless you browsing websites without styles, which probably will only happen on the next CSS Naked Day.

For this purpose, I will use the approach that I found at stackoverflow in the post Change how fast “title” attribute’s tooltip appears.

@media (hover: hover) {
  [data-title] {
  position: relative;
  }

  [data-title]:after {
  font-size: var(--h6);
  content: attr(data-title);
  position: absolute;
  left: 50%;
  top: 100%;
  color: var(--white);
  background-color: var(--black);
  width: max-content;
  opacity: 0;
  transition: opacity 0.5s ease-in-out;
  padding: 0.4rem 0.5rem;
  border-radius: 5px;
  }

  [data-title]:hover:after {
  opacity: 1;
  }
}

I have wrapped it in @media (hover: hover) as this is only relevant to devices where you can put a pointer over the element.

My variables in the above CSS are as follows:

:root {
 --h6: 0.67rem;
 --black: #222;
 --white: #fff;
}

This way I completed this part:

  • Tooltip when hovering over (on desktop);

Visual tooltip on click on the button

Now we need to sort out the last thing. When somebody will click on our anchor link (button), they need to have a prompt about what happens. This is especially important on touch devices where the previous point (hovering over) is not relevant.

For this purpose, I found a method how to incorporate it into the onclick action that was used to copy the link to the clipboard by injecting HTML into it.

this.insertAdjacentHTML('afterend', '<div class=link-copied>Link copied</div>');setTimeout(() => { document.querySelectorAll('.link-copied').forEach(el => el.remove()); }, 3000);
.link-copied {
  font-size: var(--h5);
  position: fixed;
  left: 1rem;
  bottom: 1rem;
  margin: 1rem;
  color: var(--white);
  background-color: var(--black);
  width: max-content;
  padding: 0.4rem 0.7rem;
  border-radius: 5px;
  z-index: 1;
}

My additional variable in CSS:

:root {
 --h5: 0.83rem;
}

This visual prompt has its timeout specified after which it will disappear.

This way my combined onclick action will look as follow:

onclick="navigator.clipboard.writeText({{ print .Page.Permalink "#" .Anchor | safeURL }});this.insertAdjacentHTML('afterend', '<div class=link-copied>{{ T "LinkCopied" }}</div>');setTimeout(() => { document.querySelectorAll('.link-copied').forEach(el => el.remove()); }, 3000);"

The {{ T "LinkCopied" }} is specified in i18n\en.toml file as:

[LinkCopied]
other = "Link copied to clipboard"

This way I completed this part:

  • Receive a visual prompt about copy to clipboard action.

The final code

Combining a simplified and advanced approach my code for render-heading.html with appropriate CSS styling will look as follow.

render-heading.html

<h{{ .Level }} {{- range $k, $v := .Attributes -}}{{- printf " %s=%q" $k $v | safeHTMLAttr -}}{{- end -}} id="{{ .Anchor | safeURL }}">{{ .Text | safeHTML }} <button type="button" class="anchor" data-title="{{ T "AnchorText" }}{{ .Text | safeHTML }}" aria-label="{{ T "AnchorText" }}{{ .Text | safeHTML }}" onclick="navigator.clipboard.writeText({{ print .Page.Permalink "#" .Anchor | safeURL }});this.insertAdjacentHTML('afterend', '<div class=link-copied>{{ T "LinkCopied" }}</div>');setTimeout(() => { document.querySelectorAll('.link-copied').forEach(el => el.remove()); }, 3000);"><svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 96 960 960" width="1em"><path d="M450 776H280q-83 0-141.5-58.5T80 576q0-83 58.5-141.5T280 376h170v60H280q-58.333 0-99.167 40.765-40.833 40.764-40.833 99Q140 634 180.833 675q40.834 41 99.167 41h170v60ZM325 606v-60h310v60H325Zm185 170v-60h170q58.333 0 99.167-40.765 40.833-40.764 40.833-99Q820 518 779.167 477 738.333 436 680 436H510v-60h170q83 0 141.5 58.5T880 576q0 83-58.5 141.5T680 776H510Z"/></svg></button></h{{ .Level }}>

i18n\en.toml

[AnchorText]
other = "Copy link to section: "

[LinkCopied]
other = "Link copied to clipboard"

style.css

button.anchor {
  fill: var(--main);
  background-color: inherit;
  border: none;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: inherit;
  padding: 0;
}

@media (hover: hover) {
	button.anchor {
	  opacity: 0.6;
	}
  h2:hover button.anchor,
  h3:hover button.anchor,
  h4:hover button.anchor,
  h5:hover button.anchor,
  h6:hover button.anchor {
    opacity: 1;
  }
}

@media (hover: none) {
	button.anchor {
	  opacity: 1;
	}
}

@media (hover: hover) {
  [data-title] {
  position: relative;
  }

  [data-title]:after {
  font-size: var(--h6);
  content: attr(data-title);
  position: absolute;
  left: 50%;
  top: 100%;
  color: var(--white);
  background-color: var(--black);
  width: max-content;
  opacity: 0;
  transition: opacity 0.5s ease-in-out;
  padding: 0.4rem 0.5rem;
  border-radius: 5px;
  }

  [data-title]:hover:after {
  opacity: 1;
  }
}

.link-copied {
  font-size: var(--h5);
  position: fixed;
  left: 1rem;
  bottom: 1rem;
  margin: 1rem;
  color: var(--white);
  background-color: var(--black);
  width: max-content;
  padding: 0.4rem 0.7rem;
  border-radius: 5px;
  z-index: 1;
}

Variables in style.css

:root {
	--main: #137faa;
	--h5: 0.83rem;
	--h6: 0.67rem;
	--black: #222;
	--white: #fff;
}

Job done.

The effect of this approach you can observe on this page as well as others with headings.

Comments
Categories