From 4d56d587039f8d1bfd6c9f4355be332125c82111 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Tue, 17 Feb 2026 17:43:44 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20add=20lastItemAt=20field=20=E2=80=94=20?= =?UTF-8?q?tracks=20newest=20item=20publish=20date=20per=20blog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stores the most recent item's published date on the blog document during feed sync. Exposed in API response alongside lastFetchAt. Enables sorting/displaying blogs by content freshness rather than last fetch time. --- lib/controllers/api.js | 1 + lib/storage/blogs.js | 4 ++++ lib/sync/feed.js | 9 +++++++++ package.json | 2 +- 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/controllers/api.js b/lib/controllers/api.js index 4a64e38..36075ef 100644 --- a/lib/controllers/api.js +++ b/lib/controllers/api.js @@ -234,6 +234,7 @@ function sanitizeBlog(blog) { itemCount: blog.itemCount, pinned: blog.pinned, lastFetchAt: blog.lastFetchAt, + lastItemAt: blog.lastItemAt || null, source: blog.source || null, }; diff --git a/lib/storage/blogs.js b/lib/storage/blogs.js index 7a2a597..117e621 100644 --- a/lib/storage/blogs.js +++ b/lib/storage/blogs.js @@ -110,6 +110,7 @@ export async function createBlog(application, data) { author: data.author || null, status: "active", lastFetchAt: null, + lastItemAt: null, lastError: null, itemCount: 0, pinned: data.pinned || false, @@ -200,6 +201,7 @@ export async function updateBlogStatus(application, id, status) { if (status.itemCount !== undefined) { update.itemCount = status.itemCount; } + if (status.lastItemAt) update.lastItemAt = status.lastItemAt; if (status.title) update.title = status.title; if (status.photo) update.photo = status.photo; if (status.siteUrl) update.siteUrl = status.siteUrl; @@ -289,6 +291,7 @@ export async function upsertBlog(application, data) { if (data.skipItemFetch !== undefined) setFields.skipItemFetch = data.skipItemFetch; if (data.photo !== undefined) setFields.photo = data.photo; if (data.lastFetchAt !== undefined) setFields.lastFetchAt = data.lastFetchAt; + if (data.lastItemAt !== undefined) setFields.lastItemAt = data.lastItemAt; if (data.status !== undefined) setFields.status = data.status; // $setOnInsert only for fields NOT already in $set (avoids MongoDB path conflicts) @@ -312,6 +315,7 @@ export async function upsertBlog(application, data) { if (!("skipItemFetch" in setFields)) insertDefaults.skipItemFetch = false; if (!("photo" in setFields)) insertDefaults.photo = null; if (!("lastFetchAt" in setFields)) insertDefaults.lastFetchAt = null; + if (!("lastItemAt" in setFields)) insertDefaults.lastItemAt = null; if (!("status" in setFields)) insertDefaults.status = "active"; const result = await collection.updateOne( diff --git a/lib/sync/feed.js b/lib/sync/feed.js index b7803cc..75f896c 100644 --- a/lib/sync/feed.js +++ b/lib/sync/feed.js @@ -307,10 +307,19 @@ export async function syncBlogItems(application, blog, options = {}) { if (result.upserted) added++; } + // Compute newest item publish date + let newestDate = null; + for (const item of feed.items) { + if (item.published && (!newestDate || item.published > newestDate)) { + newestDate = item.published; + } + } + // Update blog metadata const updateData = { success: true, itemCount: feed.items.length, + lastItemAt: newestDate, }; // Update title if not manually set (still has feedUrl as title) diff --git a/package.json b/package.json index 39daaa7..f8460eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmdes/indiekit-endpoint-blogroll", - "version": "1.0.22", + "version": "1.0.23", "description": "Blogroll endpoint for Indiekit. Aggregates blog feeds from OPML, JSON feeds, or manual entry.", "keywords": [ "indiekit",