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:
1 parent
d299c8891d
commit
eb12f21894
19 files changed
+592
-30
No files matched your search
@@ -1,12 +1,13 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import "../test/setup";
|
||||
import { createMockEnv, MockR2 } from "../test/setup";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { createMockEnv, MockR2, server } from "../test/setup";
|
||||
import {
|
||||
processEmail,
|
||||
ProcessEmailInput,
|
||||
RawAttachment,
|
||||
} from "./email-processor";
|
||||
import { getCounters } from "../utils/stats";
|
||||
import { iconKey } from "../utils/storage";
|
||||
|
||||
const VALID_FEED_ID = "apple.mountain.42";
|
||||
const VALID_TO = `${VALID_FEED_ID}@test.getmynews.app`;
|
||||
@@ -496,3 +497,54 @@ describe("processEmail — monitoring counters", () => {
|
||||
expect(counters.emails_received).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("processEmail — feed icon", () => {
|
||||
let env: ReturnType<typeof createMockEnv>;
|
||||
|
||||
beforeEach(async () => {
|
||||
env = createMockEnv();
|
||||
await env.EMAIL_STORAGE.put(
|
||||
`feed:${VALID_FEED_ID}:config`,
|
||||
JSON.stringify({}),
|
||||
);
|
||||
});
|
||||
|
||||
it("persists the latest sender domain on the feed metadata", async () => {
|
||||
await processEmail(
|
||||
makeInput({ from: "News <[email protected]>" }),
|
||||
env as any,
|
||||
);
|
||||
|
||||
const metadata = (await env.EMAIL_STORAGE.get(
|
||||
`feed:${VALID_FEED_ID}:metadata`,
|
||||
"json",
|
||||
)) as { iconDomain?: string };
|
||||
expect(metadata.iconDomain).toBe("github.com");
|
||||
});
|
||||
|
||||
it("triggers a background favicon fetch via ctx.waitUntil", async () => {
|
||||
let fetched = false;
|
||||
server.use(
|
||||
http.get("https://github.com/favicon.ico", () => {
|
||||
fetched = true;
|
||||
return new HttpResponse(new Uint8Array([1, 2, 3]), {
|
||||
headers: { "Content-Type": "image/png" },
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const pending: Promise<unknown>[] = [];
|
||||
const ctx = {
|
||||
waitUntil: (p: Promise<unknown>) => pending.push(p),
|
||||
passThroughOnException: () => {},
|
||||
} as unknown as ExecutionContext;
|
||||
|
||||
await processEmail(makeInput({ from: "[email protected]" }), env as any, ctx);
|
||||
await Promise.all(pending);
|
||||
|
||||
expect(fetched).toBe(true);
|
||||
expect(
|
||||
await env.EMAIL_STORAGE.get(iconKey("github.com"), "json"),
|
||||
).toMatchObject({ contentType: "image/png" });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user