Show favicons for external links

This commit is contained in:
2026-01-28 16:41:08 +01:00
parent e6d6ad3c7b
commit a5b33c1b8d
4 changed files with 154 additions and 0 deletions

116
js/linkfavicon.js Normal file
View File

@@ -0,0 +1,116 @@
/* global document */
/**
* Link Favicon Module
*
* Displays favicons for external links using DuckDuckGo's favicon service.
* Based on the linkfavicon plugin by Shao Yanmin.
*
* Note: DDG returns a grey placeholder icon for domains without favicons.
* Detecting this placeholder client-side is not reliably possible due to
* CORS restrictions preventing canvas pixel inspection.
*/
(function () {
'use strict';
var Luxtools = window.Luxtools || (window.Luxtools = {});
// Cache states for favicon URLs
var ICON_NOT_FOUND = -1;
var ICON_LOADING = 0;
var ICON_LOADED = 1;
var faviconCache = {};
/**
* Preload an image to verify it loads.
* @param {string} src - Image URL to load
* @returns {Promise} Resolves on load, rejects on error
*/
function loadImage(src) {
return new Promise(function (resolve, reject) {
var image = new Image();
image.addEventListener('load', resolve);
image.addEventListener('error', reject);
image.src = src;
});
}
/**
* Apply the favicon as background image to all matching links.
* @param {string} faviconUrl - The favicon URL to apply
*/
function applyFavicon(faviconUrl) {
if (faviconCache[faviconUrl] !== ICON_LOADED) return;
var links = document.querySelectorAll('[data-linkfavicon="' + faviconUrl + '"]');
for (var i = 0; i < links.length; i++) {
links[i].classList.add('linkfavicon');
links[i].style.backgroundImage = 'url(' + faviconUrl + ')';
}
}
/**
* Get domain from URL.
* @param {string} url - Full URL
* @returns {string|null} Domain or null if invalid
*/
function getDomain(url) {
try {
var parsed = new URL(url);
return parsed.hostname;
} catch (e) {
return null;
}
}
/**
* Initialize favicons for all external links on the page.
*/
function init() {
// Find all external links (DokuWiki marks them with class 'urlextern')
var links = document.querySelectorAll('a.urlextern');
for (var i = 0; i < links.length; i++) {
var link = links[i];
var href = link.getAttribute('href');
if (!href) continue;
var domain = getDomain(href);
if (!domain) continue;
// DuckDuckGo favicon service URL
var faviconUrl = 'https://icons.duckduckgo.com/ip3/' + domain + '.ico';
// Mark the link with the favicon URL for later reference
link.setAttribute('data-linkfavicon', faviconUrl);
// Load and cache the favicon if not already processed
if (faviconCache[faviconUrl] === undefined) {
faviconCache[faviconUrl] = ICON_LOADING;
(function (url) {
loadImage(url)
.then(function () {
faviconCache[url] = ICON_LOADED;
applyFavicon(url);
})
.catch(function () {
faviconCache[url] = ICON_NOT_FOUND;
});
})(faviconUrl);
} else if (faviconCache[faviconUrl] === ICON_LOADED) {
// Already loaded, apply immediately
link.classList.add('linkfavicon');
link.style.backgroundImage = 'url(' + faviconUrl + ')';
}
}
}
// Export for potential external use
Luxtools.LinkFavicon = {
init: init
};
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', init, false);
})();