It is kind of an informal industry standard to have a bookmark link in the headings of a page. The link text is typically a link icon (๐) or a hash symbol (#). The idea is that you can click this link and get an URL that points to that section of the page. It is a bit odd to click a link, have the page scroll down to the section exactly, and then copy the link from the address bar to share it with others. But that is what is done usually.
You can see how some websites have implemented the links in figure 1-0 below. GitHub only shows the link when you hover on the heading. CSS Tricks and Smashing Magazine always show the link, however the link text has a lower color contrast ratio than the rest of the text, but when you hover over it, it gets brighter. GitHub and CSS Tricks place the link at the very beginning of the heading, Smashing Magazine places it right at the end of the heading. Variations on the theme.
Today, I will show you how you can write some code to add these links to a page. And I will offer an alternative version, why not just add a button that will copy the URL to the system clipboard for you?
And now, there is a web specification that adds some query powers to text fragments, so you can reference any part of a webpage in an URL, and you don't have to rely on the page-author to do anything for you!
Let's explore these options.
The "standard" way - a bookmark link
N.B. Codepen runs code in a iframe
, so the bookmark links don't point to a valid external URL. If you run the same code in a page, the links are perfectly valid.
To create a bookmark, we add an unique ID to an element.
<h2 id="my-bookmark">How to create a bookmark</h2>
Remember that there are a few rules for a valid ID name:
- it must contain at least one character,
- it cannot start with a number, and
- must not contain whitespaces (spaces, tabs, etc.).
To create a link to that heading, the URL must contain a text fragment that matches our ID. A text fragment is specified by a hash.
<a href="#my-bookmark">Jump to the heading</a>
The above example is only valid within the same page. You must use an absolute URL if you want to share it with others e.g. https://www.roboleary.net/2022/01/13/copy-code-to-clipboard-blog.html/#my-bookmark.
So, to create bookmark links for all of our headings, we need to:
- Add unique IDs to all of our headings except
h1
- Insert a link into these headings, set the
href
to an absolute URL that includes the ID as a text fragment.
Let's write the code then!
We can get all of our headings with document.querySelectorAll("h2, h3, h4, h5, h6")
. We want to loop through each of these headings and add an id
. We must come up with a way to create an unique ID for each heading, a common way to do this is to use the text of the heading to generate a "slug" (that's what the cool kids call it). We will discuss the slugify
function in more detail below.
A slug is a human-readable, unique identifier, used to identify a resource instead of a less human-readable identifier like an id. You use a slug when you want to refer to an item while preserving the ability to see, at a glance, what the item is.
For each heading, we must create an anchor element (a
) and set its href
attribute to the current URL plus the slug as a text fragment. We use the global object window.location
to get the page's URL info. We build our own URL from the pieces rather than use window.location.href
. We do this because window.location.href
includes the text fragment, if someone were to follow a link with a text fragment to the page and we used window.location.href
in our code, we would create a bookmark link with 2 text fragments. Not the outcome we want! Once the link is created correctly, we append it to the heading.
let headings = document.querySelectorAll("h2, h3, h4, h5, h6");
// we construct this URL ourselves to exclude the text fragment
const currentURL = `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
headings.forEach((heading) => {
let slug = slugify(heading.innerText);
heading.setAttribute("id", slug);
const bookmarkLink = document.createElement("a");
bookmarkLink.innerText = "#";
bookmarkLink.setAttribute("href", `${currentURL}#${slug}`);
heading.append(bookmarkLink);
});
In our slugify
function, we want to generate a slug that has no whitespace, and does not have any unwanted punctuation characters. While all punctuation characters are allowed in an id
name, it is common practice to only include hyphens and underscores, probably for the sake of readability. We can use a regular expression (regex) in the replace() function to remove the unwanted charcters, and replace any spaces with hyphens. I will use something similar to GitHub's algorithm, which uses a weird-looking regex, but no doubt it has been battle-tested by now!
function slugify(text) {
// Everything except our "safe" characters
const PUNCTUATION_REGEXP = /[^\p{L}\p{M}\p{N}\p{Pc}\- ]/gu;
let slug = text.trim().toLowerCase();
slug = slug.replace(PUNCTUATION_REGEXP, "").replace(/ /g, "-");
return slug;
}
Here is a literal description of the PUNCTUATION_REGEXP
:
" Globally match a single character not present in the list below:
- \p{L}: any kind of letter from any language,
- \p{M}: a character intended to be combined with another character (e.g. accents, umlauts, enclosing boxes, etc.),
- \p{N}: any kind of numeric character in any script,
- \p{Pc}: a punctuation character such as an underscore that connects words,
- \-: a hyphen,
- and an empty space (which we replace later)."
We use the regex to remove anything that is not in our "character safe list". When you use a regex which contains unicode properties, any expression in the form of \p{}
, you must use the /u
flag also. We do a second replacement to replace spaces with a hyphen.
An alternative way - a "copy bookmark link to clipboard" button
My proposed alternative is to use a button instead of a link. The button copies the bookmark URL to the system clipboard. A snackbar message informs the user that the URL has been copied to the clipboard. I think this is a more convenient way of doings things.
N.B. Codepen runs code in a iframe
, so the bookmark links don't point to a valid external URL. If you run the same code in a page, the links are perfectly valid.
async function copyLink(event) {
const button = event.srcElement;
let text = button.getAttribute("data-href");
await navigator.clipboard.writeText(text);
showSnackbar();
}
We can asynchronously write to the system clipboard through the Clipboard API, using the writeText()
function. The browser support is excellent (for writing to the clipboard).
We show a snackbar message when the button is pressed. We use the Web Animations API to fade in and move the snackbar further into view. The Web Animations API is a cleaner of way of running a once-off animation, the alternative is to add a class that has an associated CSS animation, and then remove it via setTimeout()
a few seconds later. You can see the function showSnackbar()
for the details.
Text fragment directive specification
Text fragments can now include a text query. Upon clicking a link with a text query, the browser finds that text in the webpage, scrolls it into view, and highlights the matched text. This enables links to specify which portion of the page is being linked to, without relying on the page-author annotating the page with ID attributes.
The fragment format is: #:~:text=\[prefix-,]textStart[,textEnd\][,-suffix]
.
In its simplest form, the syntax is as follows: The hash symbol #
followed by :~:text=
and finally textStart
, which is the percent-encoded text I want to link to. Here is a simple example you can test in your browser to take you to the text "how do we get the text of the code element" from my last article:
https://www.roboleary.net/2022/01/13/copy-code-to-clipboard-blog.html#:~:text=how%20do%20we%20get%20the%20text%20of%20the%20code%20element?
You can check out the article, Boldly link where no one has linked before: Text Fragments, for further explanation and examples.
At the moment, this feature is only available in Edge and Chrome. It is still early days, but I think this should be something that we start to use wholesale.
Final word
Having the ability to cross-reference specific parts of other webpages is an often overlooked feature of the web that is of great benefit to readers. You are saving a reader from foraging through a page to find the right section themselves - maybe they want to read more of the passage of text, or maybe they want to verify the source of a quotation.
It does seem strange that we are still adding links to headings if the purpose is to provide someone with an URL to a section of a page. Why not add a button that will copy it to the clipboard instead, like I demonstrated? Or is there something am I missing? If there is, fill me in!
I hope that more browsers implement the text fragment directive soon. It would be great to break the dependence of the reader on the page-author to add IDs to headings to enable referencing of sections. And along with that, it would be great if the awareness of this feature grew too, so that people would start using it regularly. I hope this article will go a little way to raising awareness!
Top comments (1)