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

View File

@@ -275,6 +275,19 @@ Behaviour:
Scratchpads render the referenced file as wikitext and (when you have edit rights on the host page) provide an inline editor that saves directly to the backing file. Scratchpads render the referenced file as wikitext and (when you have edit rights on the host page) provide an inline editor that saves directly to the backing file.
### 7) Link Favicons (automatic)
External links automatically display the favicon of the linked website. This feature:
- Uses DuckDuckGo's favicon service (`icons.duckduckgo.com`)
- Works on all external links (class `urlextern`)
- Shows grayscale icons that become colored on hover
- Browser handles caching; no server-side storage needed
No configuration required. The feature is enabled by default for all external links.
Based on the [linkfavicon plugin](https://github.com/shaoyanmin/linkfavicon) by Shao Yanmin.
## Inline options reference (directory/images) ## Inline options reference (directory/images)

View File

@@ -48,6 +48,7 @@ class action_plugin_luxtools extends ActionPlugin
"gallery-thumbnails.js", "gallery-thumbnails.js",
"open-service.js", "open-service.js",
"scratchpads.js", "scratchpads.js",
"linkfavicon.js",
"main.js", "main.js",
]; ];

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);
})();

View File

@@ -470,3 +470,27 @@ html.luxtools-noscroll body {
padding: 3px; padding: 3px;
text-align: left; text-align: left;
} }
/* ============================================================
* Link Favicon
* Display favicons for external links.
* ============================================================ */
/* External links with favicon loaded */
.dokuwiki a.urlextern.linkfavicon {
/* Reserve space for 16x16 favicon + small gap */
padding-left: 20px;
background-repeat: no-repeat;
background-position: left center;
background-size: 16px 16px;
/* Muted grayscale effect until hover */
background-color: #fff;
background-blend-mode: luminosity;
}
/* Show full color on hover */
.dokuwiki a.urlextern.linkfavicon:hover {
background-color: transparent;
background-blend-mode: normal;
transition: all 0.2s ease-in-out;
}