@rmdes/indiekit-endpoint-blogroll 1.0.8 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,6 +38,9 @@ async function list(request, response) {
38
38
  // Get unique categories for filter dropdown
39
39
  const categories = [...new Set(blogs.map((b) => b.category).filter(Boolean))];
40
40
 
41
+ // Extract flash messages for native Indiekit notification banner
42
+ const flash = consumeFlashMessage(request);
43
+
41
44
  response.render("blogroll-blogs", {
42
45
  title: request.__("blogroll.blogs.title"),
43
46
  blogs: filteredBlogs,
@@ -45,6 +48,7 @@ async function list(request, response) {
45
48
  filterCategory: category,
46
49
  filterStatus,
47
50
  baseUrl: request.baseUrl,
51
+ ...flash,
48
52
  });
49
53
  } catch (error) {
50
54
  console.error("[Blogroll] Blogs list error:", error);
@@ -157,7 +161,17 @@ async function edit(request, response) {
157
161
  return response.status(404).render("404");
158
162
  }
159
163
 
160
- const items = await getItemsForBlog(application, blog._id, 10);
164
+ const rawItems = await getItemsForBlog(application, blog._id, 10);
165
+ const items = rawItems.map((item) => ({
166
+ ...item,
167
+ published:
168
+ item.published instanceof Date
169
+ ? item.published.toISOString()
170
+ : item.published,
171
+ }));
172
+
173
+ // Extract flash messages for native Indiekit notification banner
174
+ const flash = consumeFlashMessage(request);
161
175
 
162
176
  response.render("blogroll-blog-edit", {
163
177
  title: request.__("blogroll.blogs.edit"),
@@ -165,6 +179,7 @@ async function edit(request, response) {
165
179
  items,
166
180
  isNew: false,
167
181
  baseUrl: request.baseUrl,
182
+ ...flash,
168
183
  });
169
184
  } catch (error) {
170
185
  console.error("[Blogroll] Edit blog error:", error);
@@ -287,6 +302,21 @@ async function refresh(request, response) {
287
302
  }
288
303
  }
289
304
 
305
+ /**
306
+ * Extract and clear flash messages from session
307
+ * Returns { success, error } for Indiekit's native notificationBanner
308
+ */
309
+ function consumeFlashMessage(request) {
310
+ const result = {};
311
+ if (request.session?.messages?.length) {
312
+ const msg = request.session.messages[0];
313
+ if (msg.type === "success") result.success = msg.content;
314
+ else if (msg.type === "error" || msg.type === "warning") result.error = msg.content;
315
+ request.session.messages = null;
316
+ }
317
+ return result;
318
+ }
319
+
290
320
  export const blogsController = {
291
321
  list,
292
322
  newForm,
@@ -25,7 +25,7 @@ export async function getBlogs(application, options = {}) {
25
25
  const collection = getCollection(application);
26
26
  const { category, sourceId, includeHidden = false, limit = 100, offset = 0 } = options;
27
27
 
28
- const query = {};
28
+ const query = { status: { $ne: "deleted" } };
29
29
  if (!includeHidden) query.hidden = { $ne: true };
30
30
  if (category) query.category = category;
31
31
  if (sourceId) query.sourceId = new ObjectId(sourceId);
@@ -48,7 +48,7 @@ export async function countBlogs(application, options = {}) {
48
48
  const collection = getCollection(application);
49
49
  const { category, includeHidden = false } = options;
50
50
 
51
- const query = {};
51
+ const query = { status: { $ne: "deleted" } };
52
52
  if (!includeHidden) query.hidden = { $ne: true };
53
53
  if (category) query.category = category;
54
54
 
@@ -75,7 +75,7 @@ export async function getBlog(application, id) {
75
75
  */
76
76
  export async function getBlogByFeedUrl(application, feedUrl) {
77
77
  const collection = getCollection(application);
78
- return collection.findOne({ feedUrl });
78
+ return collection.findOne({ feedUrl, status: { $ne: "deleted" } });
79
79
  }
80
80
 
81
81
  /**
@@ -142,7 +142,8 @@ export async function updateBlog(application, id, data) {
142
142
  }
143
143
 
144
144
  /**
145
- * Delete a blog and its items
145
+ * Delete a blog and its items (soft delete)
146
+ * Marks blog as deleted so sync won't recreate it.
146
147
  * @param {object} application - Application instance
147
148
  * @param {string|ObjectId} id - Blog ID
148
149
  * @returns {Promise<boolean>} Success
@@ -154,9 +155,19 @@ export async function deleteBlog(application, id) {
154
155
  // Delete items for this blog
155
156
  await db.collection("blogrollItems").deleteMany({ blogId: objectId });
156
157
 
157
- // Delete the blog
158
- const result = await db.collection("blogrollBlogs").deleteOne({ _id: objectId });
159
- return result.deletedCount > 0;
158
+ // Soft delete: mark as deleted so upsertBlog won't recreate it
159
+ const result = await db.collection("blogrollBlogs").updateOne(
160
+ { _id: objectId },
161
+ {
162
+ $set: {
163
+ status: "deleted",
164
+ hidden: true,
165
+ deletedAt: new Date(),
166
+ updatedAt: new Date(),
167
+ },
168
+ }
169
+ );
170
+ return result.modifiedCount > 0;
160
171
  }
161
172
 
162
173
  /**
@@ -204,6 +215,7 @@ export async function getBlogsDueForRefresh(application, maxAge = 60) {
204
215
  return collection
205
216
  .find({
206
217
  hidden: { $ne: true },
218
+ status: { $ne: "deleted" },
207
219
  $or: [{ lastFetchAt: null }, { lastFetchAt: { $lt: cutoff } }],
208
220
  })
209
221
  .toArray();
@@ -219,7 +231,7 @@ export async function getCategories(application) {
219
231
 
220
232
  return collection
221
233
  .aggregate([
222
- { $match: { hidden: { $ne: true }, category: { $ne: "" } } },
234
+ { $match: { hidden: { $ne: true }, status: { $ne: "deleted" }, category: { $ne: "" } } },
223
235
  { $group: { _id: "$category", count: { $sum: 1 } } },
224
236
  { $sort: { _id: 1 } },
225
237
  ])
@@ -236,6 +248,15 @@ export async function upsertBlog(application, data) {
236
248
  const collection = getCollection(application);
237
249
  const now = new Date();
238
250
 
251
+ // Skip if a blog with this feedUrl was soft-deleted
252
+ const deleted = await collection.findOne({
253
+ feedUrl: data.feedUrl,
254
+ status: "deleted",
255
+ });
256
+ if (deleted) {
257
+ return { upserted: false, modified: false, skippedDeleted: true };
258
+ }
259
+
239
260
  const filter = { feedUrl: data.feedUrl };
240
261
  if (data.sourceId) {
241
262
  filter.sourceId = new ObjectId(data.sourceId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-blogroll",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "Blogroll endpoint for Indiekit. Aggregates blog feeds from OPML, JSON feeds, or manual entry.",
5
5
  "keywords": [
6
6
  "indiekit",
@@ -203,11 +203,7 @@
203
203
  <h1 class="page-header__title">{{ title }}</h1>
204
204
  </header>
205
205
 
206
- {% for message in request.session.messages %}
207
- <div class="notice notice--{{ message.type }}">
208
- <p>{{ message.content }}</p>
209
- </div>
210
- {% endfor %}
206
+ {# Flash messages rendered by Indiekit's native notificationBanner via success/error template vars #}
211
207
 
212
208
  <form method="post" action="{% if isNew %}{{ baseUrl }}/blogs{% else %}{{ baseUrl }}/blogs/{{ blog._id }}{% endif %}" class="br-form">
213
209
  {% if isNew %}
@@ -112,10 +112,7 @@
112
112
  <h1 class="page-header__title">{{ __("blogroll.blogs.title") }}</h1>
113
113
  </header>
114
114
 
115
- {% for message in request.session.messages %}
116
- <div class="notice notice--{{ message.type }}">
117
- <p>{{ message.content }}</p>
118
- </div>
115
+ {# Flash messages are now rendered by Indiekit's native notificationBanner via success/error template vars #}
119
116
  {% endfor %}
120
117
 
121
118
  <div class="br-blogs">