Here’s a two-second review. If an element has an ID, you can link to it with natural browser behavior. It’s great if headings have them, because it’s often useful to link directly to a specific section of content.
<h3 id="step-2">Step 2</a>
Should I be so inclined, I could link right to this heading, be it from an URL, like https://my-website.com/#step-2
, or an on-page link, like:
<a href="#step-2">Jump to Step 2</a>
So, it’s ideal if all headers have unique IDs.
I find it entirely too much work to manually add IDs to all my headings though. For years and years, I did it like this using jQuery on this very site (sue me):
// Adjust this for targetting the headers important to have IDs
const $headers = $(".article-content > h3");
$headers.each((i, el) => {
const $el = $(el);
// Probably a flexbox layout style page
if ($el.has("a").length != 0) {
return;
}
let idToLink = "";
if ($el.attr("id") === undefined) {
// give it ID
idToLink = "article-header-id-" + i;
$el.attr("id", idToLink);
} else {
// already has ID
idToLink = $el.attr("id");
}
const $headerLink = $("<a />", {
html: "#",
class: "article-headline-link",
href: "#" + idToLink
});
$el.addClass("has-header-link").prepend($headerLink);
});
That script goes one step further than just adding IDs (if it doesn’t already have one) by adding a #
link right inside the heading that links to that heading. The point of that is to demonstrate that the headers have IDs, and makes it easy to do stuff like right-click copy-link. Here’s that demo, if you care to see it.
Problem! All the sudden this stopped working.
Not the script itself, that works fine. But the native browser behavior that allows the browser to jump down to the heading when the page loads is what’s busted. I imagine it’s a race condition:
- The HTML arrives
- The page starts to render
- The browser is looking for the ID in the URL to scroll down to
- It doesn’t find it…
- Oh wait there it is!
- Scroll there.
The Oh wait there it is! step is from the script executing and putting that ID on the heading. I really don’t blame browsers for not jumping to dynamically-inserted links. I’m surprised this worked for as long as it did.
It’s much better to have the IDs on the headings by the time the HTML arrives. This site is WordPress, so I knew I could do it with some kind of content filter. Turns out I didn’t even have to bother because, of course, there is a plugin for that: Karolína Vyskočilová‘s Add Anchor Links. Works great for me. It’s technique is that it adds the ID on the anchor link itself, which is also totally fine. I guess that’s another way of avoiding messing with existing IDs.

If I didn’t have WordPress, I would have found some other way to process the HTML server-side to make sure there is some kind of heading link happening somehow. There is always a way. In fact, if it was too weird or cumbersome or whatever to do during the build process or in a server-side filter, I would look at doing it in a service worker. I’ve been having fun playing with Cloudflare’s HTMLRewriter, which is totally capable of this.
Worth noting the new Scroll-to-Text Fragment that Chrome has enabled by default: https://caniuse.com/#feat=url-scroll-to-text-fragment
Yeah I’ve been noticing that in clicks to pages from Google lately. I wonder if it will make its way to all browsers or not? I dunno, doesn’t seem like something that has to be a web standard. So far I’m finding it not very helpful, the way they have done it in search results. But I like that it exists, as it’s a way to make bespoke links to things despite what the page has available on it.
You could, probably improve the script a bit by refreshing the location hash on document ready?
Does it work? I would think you’d need a setTimeout in there or something otherwise the JavaScript engine might optimize that first hash setting away.
Or, to scroll to the position using
window.scrollTop
and the like on document ready only if URL hash exists and window page-Y offset is 0.Yeah! I was chatting with someone who told me that’s what they were doing with it (which also allows for a smooth scroll down). But I was kinda hoping to just keep browser default behavior. Write less code instead of more, ya know? But that’s totally an option. I was also curious if links-on-headers would be a benefit to SEO or the search page experience, but it seems like no.
You used to assign IDs like these to your headings:
#article-header-id-${index}
Now with the WordPress plugin, the IDs are generated based on the text in the heading, e.g.
#the-easy-way
.That means that all existing links to article headings are now broken.
Ugh yes, that’s true. I should have mentioned that. The URLs themselves still link to the right page, so I found it an acceptable tradeoff that the links aren’t actually broken and this new system will be easier to maintain going forward and relies on default browser behavior.
Adding ids with run-time javascript to a static page is not a proper solution, as those ids need to be in the html already. Which is why the solutions above are hacks, and run into issues with various browsers, and also waste CPU resources for no reason.
The best way is either to add the ids manually, but since you don’t want this, then the second best solution us to automatically modify/cook the html, for example with a very small python or node.js script, you can choose any language.
The script can work like this:
1) collect all ids in the page, using a regexp, store them in a dictionary.
2) collect all headers, again with a regexp or simple <h search, and all the missing ids, making sure they’re not in the aforementioned dictionary.
3) save the modified html, that’s it.
Right. That’s what I talk about in the article haha. If you want to share the Python script thought that would be helpful for future folks.
I love this. What I’d love more is if it only referred to HEADINGS instead of HEADERS since they’re two different things, no? It’s always a stop and think moment for me when saying
<head>
,<header>
or headings (<h1>
etc). #namingstuffishard :)I see we missed an instance in there, but it certainly was “headings” throughout the rest. Thanks for the catch! :)
Marc Jenkins:
I also use scrollTop for this, and I think adding an animation does provide a bit of context to the user. “Oh, it’s jumping me down to the content I want,” perhaps. Coincidentally, I recently developed a Drupal module for exactly this purpose that handles the generation of unique ids by finding all matching elements in the page and numbering duplicates. https://www.drupal.org/project/auto_anchors