From 619278dfaddecd20831f644866c43d49fb7cbc6c Mon Sep 17 00:00:00 2001 From: Ash Date: Fri, 10 Jul 2026 17:35:39 +0900 Subject: [PATCH] Prefer Daniel blog featured images --- scripts/community-feed-notifier.mjs | 4 +++- scripts/fetch-feeds.mjs | 11 ++++++---- scripts/lib/community-feed-reader.mjs | 7 ++++--- src/data/composite-feed.json | 5 ++++- src/data/member-feeds.json | 3 ++- src/pages/index.astro | 30 ++++++++++++++++++++------- test/community-feed-reader.test.mjs | 18 ++++++++++++++++ test/fetch-feeds.test.mjs | 19 +++++++++++++++++ 8 files changed, 79 insertions(+), 18 deletions(-) diff --git a/scripts/community-feed-notifier.mjs b/scripts/community-feed-notifier.mjs index 59db85a..db17367 100644 --- a/scripts/community-feed-notifier.mjs +++ b/scripts/community-feed-notifier.mjs @@ -231,7 +231,9 @@ async function main() { userAgent: USER_AGENT, }); const enrichedItems = await Promise.all( - items.map((item) => enrichNotifierItemWithLinkedPageImage(item)), + items.map((item) => enrichNotifierItemWithLinkedPageImage(item, { + force: item.source?.imageStrategy === "linked-page-featured", + })), ); allItems.push(...enrichedItems); } catch (error) { diff --git a/scripts/fetch-feeds.mjs b/scripts/fetch-feeds.mjs index 137d10d..221ec4d 100644 --- a/scripts/fetch-feeds.mjs +++ b/scripts/fetch-feeds.mjs @@ -429,6 +429,7 @@ function normalizeItem(rawItem, source) { link, publishedAt: publishedAt.toISOString(), source: { + ...(source.imageStrategy ? { imageStrategy: source.imageStrategy } : {}), name: source.name, siteUrl: source.siteUrl, feedUrl: source.feedUrl, @@ -441,10 +442,10 @@ function normalizeItem(rawItem, source) { export async function enrichItemWithLinkedPageImage( item, - { fetchTextFn = fetchText } = {}, + { fetchTextFn = fetchText, force = false } = {}, ) { const { inlineImage, ...publicItem } = item; - if (publicItem.image) return publicItem; + if (publicItem.image && !force) return publicItem; if ( !isHttpUrl(publicItem.link) ) { @@ -461,7 +462,7 @@ export async function enrichItemWithLinkedPageImage( const image = extractPageImage(html, publicItem.link); return { ...publicItem, - image: image ?? inlineImage ?? null, + image: image ?? publicItem.image ?? inlineImage ?? null, }; } catch { return { ...publicItem, image: inlineImage ?? null }; @@ -514,7 +515,9 @@ async function main() { }); const itemsWithLinkedPageImages = await Promise.all( - normalizedItems.map((item) => enrichItemWithLinkedPageImage(item)), + normalizedItems.map((item) => enrichItemWithLinkedPageImage(item, { + force: item.source?.imageStrategy === "linked-page-featured", + })), ); feedsWithItems.push({ diff --git a/scripts/lib/community-feed-reader.mjs b/scripts/lib/community-feed-reader.mjs index 32e66e3..c87b20b 100644 --- a/scripts/lib/community-feed-reader.mjs +++ b/scripts/lib/community-feed-reader.mjs @@ -38,6 +38,7 @@ export async function loadMemberFeeds(filePath = DEFAULT_MEMBER_FEEDS_PATH) { name: String(item.name), feedUrl: String(item.feedUrl), siteUrl: String(item.siteUrl), + ...(item.imageStrategy ? { imageStrategy: String(item.imageStrategy) } : {}), }; }); } @@ -318,15 +319,15 @@ export function normalizeNotifierItem(rawItem, source) { export async function enrichNotifierItemWithLinkedPageImage( item, - { fetchTextFn = fetchText } = {}, + { fetchTextFn = fetchText, force = false } = {}, ) { - if (item.imageUrl || !isHttpUrl(item.link)) return item; + if ((item.imageUrl && !force) || !isHttpUrl(item.link)) return item; try { const html = await fetchTextFn(item.link); return { ...item, - imageUrl: resolveHtmlImageUrl(html, item.link) || null, + imageUrl: resolveHtmlImageUrl(html, item.link) || item.imageUrl || null, }; } catch { return item; diff --git a/src/data/composite-feed.json b/src/data/composite-feed.json index a80b1d5..4b0818c 100644 --- a/src/data/composite-feed.json +++ b/src/data/composite-feed.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-07-10T08:13:47.643Z", + "generatedAt": "2026-07-10T08:34:57.128Z", "itemsPerFeed": 3, "feeds": [ { @@ -108,6 +108,7 @@ "link": "https://danielkishimoto.com/blog/2026/06/an-ai-roasted-my-brand/", "publishedAt": "2026-06-26T08:08:21.000Z", "source": { + "imageStrategy": "linked-page-featured", "name": "Daniel Kishimoto", "siteUrl": "https://danielkishimoto.com/", "feedUrl": "https://danielkishimoto.com/feed/" @@ -121,6 +122,7 @@ "link": "https://danielkishimoto.com/blog/2026/04/my-design-system-met-claude-design/", "publishedAt": "2026-04-22T14:46:18.000Z", "source": { + "imageStrategy": "linked-page-featured", "name": "Daniel Kishimoto", "siteUrl": "https://danielkishimoto.com/", "feedUrl": "https://danielkishimoto.com/feed/" @@ -134,6 +136,7 @@ "link": "https://danielkishimoto.com/blog/2026/02/2-ai-fluency-is-all-you-need/", "publishedAt": "2026-02-24T08:17:46.000Z", "source": { + "imageStrategy": "linked-page-featured", "name": "Daniel Kishimoto", "siteUrl": "https://danielkishimoto.com/", "feedUrl": "https://danielkishimoto.com/feed/" diff --git a/src/data/member-feeds.json b/src/data/member-feeds.json index 95a9cb8..d469362 100644 --- a/src/data/member-feeds.json +++ b/src/data/member-feeds.json @@ -15,7 +15,8 @@ "id": "daniel-kishimoto", "name": "Daniel Kishimoto", "siteUrl": "https://danielkishimoto.com/", - "feedUrl": "https://danielkishimoto.com/feed/" + "feedUrl": "https://danielkishimoto.com/feed/", + "imageStrategy": "linked-page-featured" }, { "id": "mere-mortal-dev", diff --git a/src/pages/index.astro b/src/pages/index.astro index 105276b..9a647cf 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -106,9 +106,11 @@ const calendarLegendItems = [ ] as const; type MemberFeed = { + id?: string; name: string; siteUrl: string; feedUrl: string; + imageStrategy?: string; }; type CompositeItem = { @@ -465,14 +467,26 @@ const formatDate = (value: string) => { {feed.items.map((item) => (
{item.image ? ( -
- +
+
) : null}
diff --git a/test/community-feed-reader.test.mjs b/test/community-feed-reader.test.mjs index 3a70bdd..59cb900 100644 --- a/test/community-feed-reader.test.mjs +++ b/test/community-feed-reader.test.mjs @@ -26,6 +26,7 @@ test("loadMemberFeeds normalizes valid source entries", async () => { name: "Example Author", feedUrl: "https://example.com/feed.xml", siteUrl: "https://example.com/", + imageStrategy: "linked-page-featured", }, ]), ); @@ -36,6 +37,7 @@ test("loadMemberFeeds normalizes valid source entries", async () => { name: "Example Author", feedUrl: "https://example.com/feed.xml", siteUrl: "https://example.com/", + imageStrategy: "linked-page-featured", }, ]); }); @@ -224,6 +226,22 @@ test("enrichNotifierItemWithLinkedPageImage finds WordPress featured images", as expect(enriched.imageUrl).toBe("https://example.com/featured.jpg"); }); +test("enrichNotifierItemWithLinkedPageImage can replace a feed logo", async () => { + const enriched = await enrichNotifierItemWithLinkedPageImage( + { + imageUrl: "https://example.com/logo.png", + link: "https://example.com/post-1", + }, + { + force: true, + fetchTextFn: async () => + '', + }, + ); + + expect(enriched.imageUrl).toBe("https://example.com/featured.jpg"); +}); + test("fetchFeedItems fetches, normalizes, dedupes, sorts, and limits items", async () => { const source = { id: "example-author", diff --git a/test/fetch-feeds.test.mjs b/test/fetch-feeds.test.mjs index 8682918..72190b3 100644 --- a/test/fetch-feeds.test.mjs +++ b/test/fetch-feeds.test.mjs @@ -121,6 +121,25 @@ describe("composite feed image selection", () => { expect(item).not.toHaveProperty("inlineImage"); }); + test("can prefer a linked WordPress featured image over a feed image", async () => { + const item = await enrichItemWithLinkedPageImage( + { + id: "post-1", + link: "https://example.com/post/", + image: "https://example.com/logo.png", + inlineImage: null, + }, + { + force: true, + fetchTextFn: vi.fn(async () => + '', + ), + }, + ); + + expect(item.image).toBe("https://example.com/featured.jpg"); + }); + test("retains an inline image when linked-page enrichment fails", async () => { const item = await enrichItemWithLinkedPageImage( {