Customizing external links with Eleventy
2023-12-26
Background
I've recently written a couple of posts about the different ways I've customized the Eleventy, the static site generator used for this site. In the previous post I described modifying the HTML rendered by Eleventy to maintain working internal links between Markdown files. This modification makes Eleventy treat local links the way I believe it should by default. Next on my improvement wishlist was addressing the lack of visual distinction between internal and external links in my posts. With my successful link-transformation effort fresh in mind, I immediately set out to add some additional HTML post-processing.
Post-processing
In the post linked above I introduced a small class that iterates over all links in every page, modifying the internal ones, so in my case it was very straightforward to add another snippet to the same iteration. Here’s a standalone version of the code dealing only with the external links, which you can include in your .eleventy.js
file:
const cheerio = require("cheerio");
const externalLinkSvg = '<svg>...</svg>';
module.exports = function (eleventyConfig) {
eleventyConfig.addTransform("updateLinks", function (content, outputPath) {
// Only apply this to HTML files
if (!outputPath || !outputPath.endsWith(".html")) {
return content;
}
const $ = cheerio.load(content);
$('a[href^="http"]')
.filter((_, a) => $(a).text().trim() != '')
.each((_, a) => {
$(a)
.append(externalLinkSvg)
.addClass('external')
.attr('target', '_blank')
.attr('rel', 'noopener');
})
return $.html();
});
// Further configuration ...
return eleventyConfig;
};
The code utilizes Cheerio to parse the HTML content of the generated post. It specifically searches for all a
elements with a href
attribute beginning with http
, as a crude way of identifying external links. Since I just want to modify text links (e.g. not image links, like the contact icons in the page footer), the matching a
elements are then filtered to exclude those without text content. The remaining collection is iterated over using each()
, and within this loop any desired customizations can be made. In this example, I append a small snippet of SVG right into the link content, add a CSS class, set the target
attribute to _blank
and rel
to noopener
just for good measure.
At the end, the modified DOM is returned in the form of HTML, and the work of the link transformer is complete.
An alternate approach
Instead of modifying the HTML produced by Eleventy, Jeff Sheets suggested I could use a plugin for the Markdown parser used by Eleventy, markdown-it, to generate the links the way I want them right from the start. This feels like a more refined solution, but I haven't been able to achieve the result I want using this approach.
There are multiple plugins modifying links in various ways, some (like markdown-it-external-link and markdown-it-external-links) are even specifically made to add additional attributes to external links. However, I haven't found any plugin that can both add attributes and append content, like the inline SVG icon I'm using, to the links.
There are several plugins that can add CSS classes to external links, and using CSS I could instead insert my SVG icon into the external links using the ::after
pseudo-element like this:
a.external::after {
content: url(/assets/external.svg);
}
But unfortunately this still won't work the way I want. The problem is that this site has a light and a dark mode, and the link color varies slightly depending on the mode. To ensure the external link icon matches the color of the link itself, I insert it into the link content as inline SVG. This in combination with the property fill="currentColor"
in the SVG code makes the icon inherit the color of the parent element. Sadly this doesn't work when the SVG is inserted using the ::after
pseudo-element. In fact, I haven't found any way to set the color of the SVG inserted this way using CSS, it seems like the only option is to set the color in the SVG file.
I'm tempted to add support for modifying the link content to one of the plugins mentioned above, but I'd have to check if the authors are interested in this first. Another option would be to build my own plugin, but this would merely move my custom code to a different file, and I don't feel that bad about my post-processing approach.
Conclusion
While modifying the output of the Markdown processing itself would be a better solution, adding transforms to modify the HTML produced by Eleventy is a good way to ensure your site comes out just the way you want. Using Cheerio for this task does bring back some memories of ... erm, less elegant jQuery hacks from the past, but it feels much better to have this type of code as part of a build step rather than live on a site.
For those who want to follow along on my Eleventy adventure, I've got a page listing all my Eleventy posts. I'm still learning Eleventy myself, but hopefully I can share some insights and lessons learned from setting up this site.
Happy tinkering!