I wanted to make external links on my blog a bit more visually interesting. Rather than displaying just the link text, I thought it would be nice to show the website’s favicon alongside it.
It’s a small detail, but it makes links more recognisable, particularly when referencing GitHub, documentation or other websites throughout a post.
The blog is built with Nuxt, pulling its content from WordPress at build time. I could have written something to fetch the favicons in the browser, but that would mean making additional requests every time someone opened a page.
Instead, I decided to fetch the favicons during the build and store them locally alongside the rest of the site’s images.
Finding the favicon
Most websites declare their favicon in the HTML <head>, using a link element like this:
1<link rel="icon" href="/favicon.ico">
Some websites provide several icons at different sizes, while others use an Apple touch icon.
I created a FetchFavicon class which fetches the HTML of a website and uses Cheerio
1findRelIcons () { 2 const results = [] 3 const icons = this.$('link[rel~=\'icon\']') 4 5 for (const icon of icons) { 6 const sizesStr = icon.attribs.sizes 7 8 results.push({ 9 href: this.normalizePath(icon.attribs.href),10 size: sizesStr ? sizesStr.split('x') : [0, 0]11 })12 }13 14 return results.sort((a, b) => b.size[0] - a.size[0])15}
The idea was to collect the available icons and prioritise the larger ones, so I had a decent quality image to work with.
I also added a check for Apple touch icons, which takes priority over the standard favicon.
1async getFavicon () { 2 try { 3 await this.fetch() 4 } catch (e) { 5 return null 6 } 7 8 const appleTouchIconHref = this.findAppleTouchIcon() 9 10 if (appleTouchIconHref) {11 return appleTouchIconHref12 }13 14 const relIcons = this.findRelIcons()15 16 if (relIcons.length) {17 return relIcons[0].href18 }19 20 return null21}
One thing to account for is that icon URLs aren’t always absolute. A website might specify /favicon.png rather than a full URL.
I used JavaScript’s URL constructor to resolve relative paths against the website’s URL, or its canonical URL if one was available.
Adding the icons to WordPress content
Once I had a way to find the favicon, I needed to add it to the links within each blog post.
I was already processing the HTML returned by WordPress to handle images and videos, so I added another step for external links.
Using Cheerio, I could find every anchor element in the post.
1const $ = load(postContent, null, false) 2 3$('a').each((i, elem) => { 4 const url = $(elem).attr('href') 5 6 if (!url) { 7 return 8 } 9 10 const FaviconFetcher = new FetchFavicon(url)11 const parsedUrl = FaviconFetcher.isFullUrl(url)12 13 if (!parsedUrl) {14 return15 }16 17 // Fetch and process the favicon18})
Checking whether the URL is absolute means relative links within the website are ignored.
For each external link, the script finds the favicon and appends an image to the anchor element.
I also made external links open in a new tab.
Downloading and optimising the images
I didn’t want visitors downloading the favicons from third-party websites every time they loaded my blog.
The whole point of building the website statically is that I can do this sort of processing ahead of time.
I already had a utility for downloading and processing external images, using Sharp
The images are downloaded into a favicons directory, with the website’s hostname used as the filename.
1const width = 16 2const height = 16 3const outputDirectory = 'favicons' 4const outputFileName = parsedUrl.hostname 5 6const image = new HandleExternalImage( 7 externalImgUrl, 8 outputDirectory, 9 outputFileName,10 width,11 height,12 `${parsedUrl.hostname} favicon`13)14 15const pictureElement = await image.generatePictureElement()16$(elem).append(pictureElement)
The image utility handles resizing and generating AVIF and WebP versions.
It also checks whether the files already exist before downloading and processing them, avoiding unnecessary work on subsequent builds.
The result is a <picture> element containing the optimised favicon, which gets added to the link automatically.
Styling the links
With the images in place, the CSS is fairly simple.
1:any-link {2 display: inline-flex;3 gap: .2em;4 align-items: center;5 6 picture {7 display: inline-flex;8 }9}
Using inline-flex keeps the link text and favicon aligned, while gap adds a little space between them.
Because the images are only 16 pixels, they don’t overwhelm the text.
The nice thing is that I don’t have to do anything differently when writing a blog post. I just add a link in WordPress as normal, and the favicon is added during the build.
Was it worth it?
There’s quite a bit of processing involved for something as small as a favicon.
The script has to fetch each external website, parse its HTML, find an appropriate icon, download it and process the image.
Not every website declares its icons consistently, and some requests will fail. In those cases, the script simply leaves the link alone rather than failing the build.
But because all of this happens at build time, visitors don’t have to wait for those requests.
It’s a small visual improvement that makes the blog look a bit more polished, without adding any extra work when writing posts.