feat: self-hosted favicon resolver via /favicon/<domain>

Adds a Kafka-hosted favicon proxy at /favicon/<domain>:
- Fetches favicon.ico from the target domain
- In-memory cache with 1-hour TTL and ETag support (304 Not Modified)
- Max 64KB per favicon to prevent memory abuse
- Privacy: user browser talks to Kafka, not Google/DuckDuckGo

New "Self (Kafka)" option in the favicon service selector.
Defaults to None. No third-party requests when self is chosen.
This commit is contained in:
Franz Kafka 2026-03-23 14:35:19 +00:00
parent d0efcb0309
commit 352264509c
4 changed files with 124 additions and 4 deletions

View file

@ -48,7 +48,8 @@ function applyTheme(theme) {
function applyFavicon(service) {
var faviconMap = {
google: function(domain) { return 'https://www.google.com/s2/favicons?domain=' + encodeURIComponent(domain) + '&sz=32'; },
duckduckgo: function(domain) { return 'https://icons.duckduckgo.com/ip3/' + encodeURIComponent(domain) + '.ico'; }
duckduckgo: function(domain) { return 'https://icons.duckduckgo.com/ip3/' + encodeURIComponent(domain) + '.ico'; },
self: function(domain) { return '/favicon/' + encodeURIComponent(domain); }
};
var imgs = document.querySelectorAll('.result-favicon');
imgs.forEach(function(img) {
@ -56,7 +57,7 @@ function applyFavicon(service) {
if (!domain) return;
if (service === 'none') {
img.style.display = 'none';
} else {
} else if (faviconMap[service]) {
img.style.display = '';
img.src = faviconMap[service](domain);
}
@ -125,8 +126,8 @@ function renderPanel(prefs) {
var faviconOptions = '';
['none', 'google', 'duckduckgo'].forEach(function(src) {
var labels = { none: 'None', google: 'Google', duckduckgo: 'DuckDuckGo' };
['none', 'google', 'duckduckgo', 'self'].forEach(function(src) {
var labels = { none: 'None', google: 'Google', duckduckgo: 'DuckDuckGo', self: 'Self (Kafka)' };
var selected = prefs.favicon === src ? ' selected' : '';
faviconOptions += '<option value="' + src + '"' + selected + '>' + labels[src] + '</option>';
});

View file

@ -78,6 +78,7 @@
<option value="none" selected>None</option>
<option value="google">Google</option>
<option value="duckduckgo">DuckDuckGo</option>
<option value="self">Self (Kafka)</option>
</select>
</div>
</section>