mirror of
https://github.com/svemagie/indiekit-endpoint-blogroll.git
synced 2026-04-02 15:34:59 +02:00
fix: soft delete blogs to prevent sync from recreating deleted entries
Deleted blogs are now marked with status: "deleted" instead of being removed from MongoDB. The upsertBlog function skips deleted feedUrls, preventing OPML/Microsub sync from recreating them. All queries exclude deleted blogs. Flash messages now use Indiekit's native notificationBanner. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<boolean>} 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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -203,11 +203,7 @@
|
||||
<h1 class="page-header__title">{{ title }}</h1>
|
||||
</header>
|
||||
|
||||
{% for message in request.session.messages %}
|
||||
<div class="notice notice--{{ message.type }}">
|
||||
<p>{{ message.content }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{# Flash messages rendered by Indiekit's native notificationBanner via success/error template vars #}
|
||||
|
||||
<form method="post" action="{% if isNew %}{{ baseUrl }}/blogs{% else %}{{ baseUrl }}/blogs/{{ blog._id }}{% endif %}" class="br-form">
|
||||
{% if isNew %}
|
||||
|
||||
@@ -112,10 +112,7 @@
|
||||
<h1 class="page-header__title">{{ __("blogroll.blogs.title") }}</h1>
|
||||
</header>
|
||||
|
||||
{% for message in request.session.messages %}
|
||||
<div class="notice notice--{{ message.type }}">
|
||||
<p>{{ message.content }}</p>
|
||||
</div>
|
||||
{# Flash messages are now rendered by Indiekit's native notificationBanner via success/error template vars #}
|
||||
{% endfor %}
|
||||
|
||||
<div class="br-blogs">
|
||||
|
||||
Reference in New Issue
Block a user