feat(favicon): per-feed icon from the last sender's domain

Resolve each feed's most recent sender domain and serve its favicon at
GET /favicon/:feedId, falling back to the project icon. Icons are fetched
in the background on ingestion (direct /favicon.ico then a DuckDuckGo
fallback), cached base64 in KV keyed by domain with a 1-week TTL so the
fetch only fires when absent. Exposed via RSS <image> / Atom <icon>/<logo>
and rendered in the admin feed list, plus a landing-page feature card.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Julien HerrandClaude Opus 4.7 committed 2026-05-23 14:05:14 +02:00
1 parent d299c8891d
commit eb12f21894
19 files changed
+592 -30

No files matched your search

+13
View File
@@ -8,6 +8,10 @@ import {
} from "../types";
import { notifySubscribers } from "../utils/websub";
import { bumpCounters } from "../utils/stats";
import {
cacheFaviconForDomain,
extractEmailDomain,
} from "../utils/favicon-fetcher";
import { logger } from "./logger";
import { FEED_MAX_BYTES } from "../config/constants";
@@ -213,6 +217,12 @@ export async function storeEmail(
};
feedMetadata.emails.unshift(newEntry);
// Track the latest sender's domain so the feed icon follows the source.
const iconDomain = extractEmailDomain(input.from);
if (iconDomain) {
feedMetadata.iconDomain = iconDomain;
}
let totalSize = feedMetadata.emails.reduce(
(sum, e) => sum + (e.size ?? 0),
0,
@@ -240,6 +250,9 @@ export async function storeEmail(
logger.info("Email processed", { feedId });
if (ctx) {
ctx.waitUntil(notifySubscribers(feedId, env));
if (iconDomain) {
ctx.waitUntil(cacheFaviconForDomain(iconDomain, env));
}
}
}