diff --git a/lib/controllers/blogs.js b/lib/controllers/blogs.js index d123571..f5be016 100644 --- a/lib/controllers/blogs.js +++ b/lib/controllers/blogs.js @@ -38,6 +38,9 @@ async function list(request, response) { // Get unique categories for filter dropdown const categories = [...new Set(blogs.map((b) => b.category).filter(Boolean))]; + // Extract flash messages for native Indiekit notification banner + const flash = consumeFlashMessage(request); + response.render("blogroll-blogs", { title: request.__("blogroll.blogs.title"), blogs: filteredBlogs, @@ -45,6 +48,7 @@ async function list(request, response) { filterCategory: category, filterStatus, baseUrl: request.baseUrl, + ...flash, }); } catch (error) { console.error("[Blogroll] Blogs list error:", error); @@ -166,12 +170,16 @@ async function edit(request, response) { : item.published, })); + // Extract flash messages for native Indiekit notification banner + const flash = consumeFlashMessage(request); + response.render("blogroll-blog-edit", { title: request.__("blogroll.blogs.edit"), blog, items, isNew: false, baseUrl: request.baseUrl, + ...flash, }); } catch (error) { console.error("[Blogroll] Edit blog error:", error); @@ -294,6 +302,21 @@ async function refresh(request, response) { } } +/** + * Extract and clear flash messages from session + * Returns { success, error } for Indiekit's native notificationBanner + */ +function consumeFlashMessage(request) { + const result = {}; + if (request.session?.messages?.length) { + const msg = request.session.messages[0]; + if (msg.type === "success") result.success = msg.content; + else if (msg.type === "error" || msg.type === "warning") result.error = msg.content; + request.session.messages = null; + } + return result; +} + export const blogsController = { list, newForm, diff --git a/lib/storage/blogs.js b/lib/storage/blogs.js index ad7bc96..a5b7770 100644 --- a/lib/storage/blogs.js +++ b/lib/storage/blogs.js @@ -25,7 +25,7 @@ export async function getBlogs(application, options = {}) { const collection = getCollection(application); const { category, sourceId, includeHidden = false, limit = 100, offset = 0 } = options; - const query = {}; + const query = { status: { $ne: "deleted" } }; if (!includeHidden) query.hidden = { $ne: true }; if (category) query.category = category; if (sourceId) query.sourceId = new ObjectId(sourceId); @@ -48,7 +48,7 @@ export async function countBlogs(application, options = {}) { const collection = getCollection(application); const { category, includeHidden = false } = options; - const query = {}; + const query = { status: { $ne: "deleted" } }; if (!includeHidden) query.hidden = { $ne: true }; if (category) query.category = category; @@ -75,7 +75,7 @@ export async function getBlog(application, id) { */ export async function getBlogByFeedUrl(application, feedUrl) { const collection = getCollection(application); - return collection.findOne({ feedUrl }); + return collection.findOne({ feedUrl, status: { $ne: "deleted" } }); } /** @@ -142,7 +142,8 @@ export async function updateBlog(application, id, data) { } /** - * Delete a blog and its items + * Delete a blog and its items (soft delete) + * Marks blog as deleted so sync won't recreate it. * @param {object} application - Application instance * @param {string|ObjectId} id - Blog ID * @returns {Promise} Success @@ -154,9 +155,19 @@ export async function deleteBlog(application, id) { // Delete items for this blog await db.collection("blogrollItems").deleteMany({ blogId: objectId }); - // Delete the blog - const result = await db.collection("blogrollBlogs").deleteOne({ _id: objectId }); - return result.deletedCount > 0; + // Soft delete: mark as deleted so upsertBlog won't recreate it + const result = await db.collection("blogrollBlogs").updateOne( + { _id: objectId }, + { + $set: { + status: "deleted", + hidden: true, + deletedAt: new Date(), + updatedAt: new Date(), + }, + } + ); + return result.modifiedCount > 0; } /** @@ -204,6 +215,7 @@ export async function getBlogsDueForRefresh(application, maxAge = 60) { return collection .find({ hidden: { $ne: true }, + status: { $ne: "deleted" }, $or: [{ lastFetchAt: null }, { lastFetchAt: { $lt: cutoff } }], }) .toArray(); @@ -219,7 +231,7 @@ export async function getCategories(application) { return collection .aggregate([ - { $match: { hidden: { $ne: true }, category: { $ne: "" } } }, + { $match: { hidden: { $ne: true }, status: { $ne: "deleted" }, category: { $ne: "" } } }, { $group: { _id: "$category", count: { $sum: 1 } } }, { $sort: { _id: 1 } }, ]) @@ -236,6 +248,15 @@ export async function upsertBlog(application, data) { const collection = getCollection(application); const now = new Date(); + // Skip if a blog with this feedUrl was soft-deleted + const deleted = await collection.findOne({ + feedUrl: data.feedUrl, + status: "deleted", + }); + if (deleted) { + return { upserted: false, modified: false, skippedDeleted: true }; + } + const filter = { feedUrl: data.feedUrl }; if (data.sourceId) { filter.sourceId = new ObjectId(data.sourceId); diff --git a/package.json b/package.json index c9bae83..ba56d88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmdes/indiekit-endpoint-blogroll", - "version": "1.0.9", + "version": "1.0.10", "description": "Blogroll endpoint for Indiekit. Aggregates blog feeds from OPML, JSON feeds, or manual entry.", "keywords": [ "indiekit", diff --git a/views/blogroll-blog-edit.njk b/views/blogroll-blog-edit.njk index 21f24fa..ac153df 100644 --- a/views/blogroll-blog-edit.njk +++ b/views/blogroll-blog-edit.njk @@ -203,11 +203,7 @@

{{ title }}

-{% for message in request.session.messages %} -
-

{{ message.content }}

-
-{% endfor %} +{# Flash messages rendered by Indiekit's native notificationBanner via success/error template vars #}
{% if isNew %} diff --git a/views/blogroll-blogs.njk b/views/blogroll-blogs.njk index f572c3a..957cfa4 100644 --- a/views/blogroll-blogs.njk +++ b/views/blogroll-blogs.njk @@ -112,10 +112,7 @@

{{ __("blogroll.blogs.title") }}

-{% for message in request.session.messages %} -
-

{{ message.content }}

-
+{# Flash messages are now rendered by Indiekit's native notificationBanner via success/error template vars #} {% endfor %}