mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
+43
-1
@@ -1,5 +1,7 @@
|
||||
import { Context } from "hono";
|
||||
import { Env } from "../types";
|
||||
import { getFeedMetadata } from "../utils/storage";
|
||||
import { cacheFaviconForDomain, getCachedIcon } from "../utils/favicon-fetcher";
|
||||
|
||||
export const FAVICON_PATH = "/favicon.svg";
|
||||
|
||||
@@ -13,7 +15,7 @@ export const FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0
|
||||
</g>
|
||||
</svg>`;
|
||||
|
||||
export function handle(_c: Context<{ Bindings: Env }>): Response {
|
||||
function projectFavicon(): Response {
|
||||
return new Response(FAVICON_SVG, {
|
||||
headers: {
|
||||
"Content-Type": "image/svg+xml; charset=utf-8",
|
||||
@@ -21,3 +23,43 @@ export function handle(_c: Context<{ Bindings: Env }>): Response {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function handle(_c: Context<{ Bindings: Env }>): Response {
|
||||
return projectFavicon();
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-feed favicon. Resolves the feed's most recent sender domain and serves
|
||||
* its cached icon; falls back to the project icon for any unresolved case
|
||||
* (no domain, cache miss, or negative cache entry).
|
||||
*/
|
||||
export async function handleFeedFavicon(
|
||||
c: Context<{ Bindings: Env }>,
|
||||
): Promise<Response> {
|
||||
const env = c.env;
|
||||
const feedId = c.req.param("feedId");
|
||||
if (!feedId) return projectFavicon();
|
||||
|
||||
const metadata = await getFeedMetadata(env.EMAIL_STORAGE, feedId);
|
||||
const domain = metadata?.iconDomain;
|
||||
if (!domain) return projectFavicon();
|
||||
|
||||
const icon = await getCachedIcon(domain, env);
|
||||
if (icon) {
|
||||
return new Response(icon.bytes, {
|
||||
headers: {
|
||||
"Content-Type": icon.contentType,
|
||||
"Cache-Control": "public, max-age=86400",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Known domain but nothing cached yet: warm the cache in the background and
|
||||
// serve the fallback for now.
|
||||
try {
|
||||
c.executionCtx.waitUntil(cacheFaviconForDomain(domain, env));
|
||||
} catch {
|
||||
// No ExecutionContext (e.g. tests) — fallback is served regardless.
|
||||
}
|
||||
return projectFavicon();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user