feat: add lastItemAt field — tracks newest item publish date per blog

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.
This commit is contained in:
Ricardo
2026-02-17 17:43:44 +01:00
parent f02d46e76e
commit 4d56d58703
4 changed files with 15 additions and 1 deletions

View File

@@ -234,6 +234,7 @@ function sanitizeBlog(blog) {
itemCount: blog.itemCount, itemCount: blog.itemCount,
pinned: blog.pinned, pinned: blog.pinned,
lastFetchAt: blog.lastFetchAt, lastFetchAt: blog.lastFetchAt,
lastItemAt: blog.lastItemAt || null,
source: blog.source || null, source: blog.source || null,
}; };

View File

@@ -110,6 +110,7 @@ export async function createBlog(application, data) {
author: data.author || null, author: data.author || null,
status: "active", status: "active",
lastFetchAt: null, lastFetchAt: null,
lastItemAt: null,
lastError: null, lastError: null,
itemCount: 0, itemCount: 0,
pinned: data.pinned || false, pinned: data.pinned || false,
@@ -200,6 +201,7 @@ export async function updateBlogStatus(application, id, status) {
if (status.itemCount !== undefined) { if (status.itemCount !== undefined) {
update.itemCount = status.itemCount; update.itemCount = status.itemCount;
} }
if (status.lastItemAt) update.lastItemAt = status.lastItemAt;
if (status.title) update.title = status.title; if (status.title) update.title = status.title;
if (status.photo) update.photo = status.photo; if (status.photo) update.photo = status.photo;
if (status.siteUrl) update.siteUrl = status.siteUrl; 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.skipItemFetch !== undefined) setFields.skipItemFetch = data.skipItemFetch;
if (data.photo !== undefined) setFields.photo = data.photo; if (data.photo !== undefined) setFields.photo = data.photo;
if (data.lastFetchAt !== undefined) setFields.lastFetchAt = data.lastFetchAt; if (data.lastFetchAt !== undefined) setFields.lastFetchAt = data.lastFetchAt;
if (data.lastItemAt !== undefined) setFields.lastItemAt = data.lastItemAt;
if (data.status !== undefined) setFields.status = data.status; if (data.status !== undefined) setFields.status = data.status;
// $setOnInsert only for fields NOT already in $set (avoids MongoDB path conflicts) // $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 (!("skipItemFetch" in setFields)) insertDefaults.skipItemFetch = false;
if (!("photo" in setFields)) insertDefaults.photo = null; if (!("photo" in setFields)) insertDefaults.photo = null;
if (!("lastFetchAt" in setFields)) insertDefaults.lastFetchAt = null; if (!("lastFetchAt" in setFields)) insertDefaults.lastFetchAt = null;
if (!("lastItemAt" in setFields)) insertDefaults.lastItemAt = null;
if (!("status" in setFields)) insertDefaults.status = "active"; if (!("status" in setFields)) insertDefaults.status = "active";
const result = await collection.updateOne( const result = await collection.updateOne(

View File

@@ -307,10 +307,19 @@ export async function syncBlogItems(application, blog, options = {}) {
if (result.upserted) added++; 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 // Update blog metadata
const updateData = { const updateData = {
success: true, success: true,
itemCount: feed.items.length, itemCount: feed.items.length,
lastItemAt: newestDate,
}; };
// Update title if not manually set (still has feedUrl as title) // Update title if not manually set (still has feedUrl as title)

View File

@@ -1,6 +1,6 @@
{ {
"name": "@rmdes/indiekit-endpoint-blogroll", "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.", "description": "Blogroll endpoint for Indiekit. Aggregates blog feeds from OPML, JSON feeds, or manual entry.",
"keywords": [ "keywords": [
"indiekit", "indiekit",