@rmdes/indiekit-endpoint-rss 1.1.2 → 1.1.4

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.
package/index.js CHANGED
@@ -74,6 +74,19 @@ export default class RssEndpoint {
74
74
  // Clear items and re-sync
75
75
  router.post("/clear-resync", dashboardController.clearResync);
76
76
 
77
+ // Dashboard forms: plain POST and redirect, so results land in
78
+ // Indiekit's own notification banner rather than a browser alert.
79
+ router.post(
80
+ "/feeds/:id/settings",
81
+ express.urlencoded({ extended: false }),
82
+ dashboardController.saveFeedSettings,
83
+ );
84
+ router.post(
85
+ "/feeds/:id/backfill",
86
+ express.urlencoded({ extended: false }),
87
+ dashboardController.backfillFeed,
88
+ );
89
+
77
90
  // Feed management (protected - requires auth)
78
91
  router.post("/api/feeds", express.json(), feedsController.add);
79
92
  router.delete("/api/feeds/:id", feedsController.remove);
@@ -1,3 +1,4 @@
1
+ import { applyFeedSettings, rewindWatermark } from "../feed-settings.js";
1
2
  import { getSyncState, runSync } from "../sync.js";
2
3
  import { formatFeed, formatItem } from "../utils.js";
3
4
 
@@ -17,6 +18,32 @@ function consumeFlashMessage(request) {
17
18
  return result;
18
19
  }
19
20
 
21
+
22
+ /**
23
+ * Publish context for a request, or null when the site is not configured.
24
+ * @param {object} request - Request
25
+ * @returns {object|null} Micropub endpoint and publication URL
26
+ */
27
+ function publishContextFrom(request) {
28
+ const { application, publication } = request.app.locals;
29
+
30
+ return application?.micropubEndpoint && publication?.me
31
+ ? { micropubEndpoint: application.micropubEndpoint, me: publication.me }
32
+ : null;
33
+ }
34
+
35
+ /**
36
+ * Record a flash message and return to the dashboard.
37
+ * @param {object} request - Request
38
+ * @param {object} response - Response
39
+ * @param {string} type - success, error or warning
40
+ * @param {string} content - Message
41
+ */
42
+ function flashBack(request, response, type, content) {
43
+ request.session.messages = [{ type, content }];
44
+ response.redirect(request.baseUrl);
45
+ }
46
+
20
47
  export const dashboardController = {
21
48
  /**
22
49
  * Render admin dashboard
@@ -81,6 +108,112 @@ export const dashboardController = {
81
108
  }
82
109
  },
83
110
 
111
+
112
+ /**
113
+ * Save a feed's settings from the dashboard form
114
+ * POST /feeds/:id/settings
115
+ */
116
+ async saveFeedSettings(request, response) {
117
+ try {
118
+ const db = request.app.locals.application.getRssDb?.();
119
+ if (!db) {
120
+ return flashBack(request, response, "error", "Database not available");
121
+ }
122
+
123
+ const body = {
124
+ url: request.body.url,
125
+ publish: {
126
+ // An unchecked checkbox is omitted from a form post entirely, so
127
+ // presence is the signal.
128
+ enabled: request.body.enabled !== undefined,
129
+ postType: request.body.postType,
130
+ content: request.body.content,
131
+ linkProperty: request.body.linkProperty || null,
132
+ status: request.body.status,
133
+ },
134
+ };
135
+
136
+ const result = await applyFeedSettings(
137
+ db,
138
+ request.params.id,
139
+ body,
140
+ request.app.locals.publication,
141
+ );
142
+
143
+ if (result.error) {
144
+ return flashBack(request, response, "error", result.error);
145
+ }
146
+
147
+ // Saying "nothing is waiting" beats silence: a feed can be correctly
148
+ // configured and still publish nothing, because the watermark only
149
+ // admits items dated after publishing was switched on.
150
+ const message =
151
+ result.eligible === 0
152
+ ? "Settings saved. No items are waiting — only items published after you enabled this will post. Use Publish existing items to include the ones already cached."
153
+ : result.eligible > 0
154
+ ? `Settings saved. ${result.eligible} item(s) waiting to publish.`
155
+ : "Settings saved.";
156
+
157
+ flashBack(request, response, "success", message);
158
+ } catch (error) {
159
+ console.error("[RSS] Error saving feed settings:", error.message);
160
+ flashBack(request, response, "error", error.message);
161
+ }
162
+ },
163
+
164
+ /**
165
+ * Rewind a feed's watermark and publish straight away
166
+ * POST /feeds/:id/backfill
167
+ */
168
+ async backfillFeed(request, response) {
169
+ try {
170
+ const { rssConfig, getRssDb } = request.app.locals.application;
171
+ const db = getRssDb?.();
172
+ if (!db) {
173
+ return flashBack(request, response, "error", "Database not available");
174
+ }
175
+
176
+ const result = await rewindWatermark(db, request.params.id, {
177
+ last: Number(request.body.last),
178
+ });
179
+
180
+ if (result.error) {
181
+ return flashBack(request, response, "error", result.error);
182
+ }
183
+
184
+ if (result.eligible === 0) {
185
+ return flashBack(
186
+ request,
187
+ response,
188
+ "warning",
189
+ "Nothing to publish: those items are already published or were skipped after repeated failures.",
190
+ );
191
+ }
192
+
193
+ // Run the sync now rather than leaving the user to wait a cycle and
194
+ // guess. This is the only way the answer can be "3 published" instead
195
+ // of "queued, come back later".
196
+ const sync = await runSync(db, rssConfig, publishContextFrom(request));
197
+
198
+ if (sync.error) {
199
+ return flashBack(request, response, "error", sync.error);
200
+ }
201
+
202
+ const published = sync.itemsPublished || 0;
203
+ flashBack(
204
+ request,
205
+ response,
206
+ published > 0 ? "success" : "warning",
207
+ published > 0
208
+ ? `Published ${published} item(s) from the cache.`
209
+ : `${result.eligible} item(s) became eligible but none published — check the logs for the reason.`,
210
+ );
211
+ } catch (error) {
212
+ console.error("[RSS] Error running backfill:", error.message);
213
+ flashBack(request, response, "error", error.message);
214
+ }
215
+ },
216
+
84
217
  /**
85
218
  * Clear all items and re-sync
86
219
  * POST /clear-resync
@@ -1,7 +1,6 @@
1
1
  import { ObjectId } from "mongodb";
2
2
  import { RssClient } from "../rss-client.js";
3
- import { defaultContent, defaultLinkProperty } from "../jf2-builder.js";
4
- import { watermarkFor } from "../publisher.js";
3
+ import { applyFeedSettings, rewindWatermark } from "../feed-settings.js";
5
4
  import { formatFeed, isValidUrl, normalizeUrl } from "../utils.js";
6
5
 
7
6
  export const feedsController = {
@@ -149,128 +148,31 @@ export const feedsController = {
149
148
  },
150
149
 
151
150
  /**
152
- * Update a feed: enable/disable, or change its publish configuration
151
+ * Update a feed: enable/disable, change its url, or its publish config
153
152
  * PATCH /api/feeds/:id
154
- * Body: { enabled?: boolean, publish?: object }
155
153
  */
156
154
  async toggle(request, response) {
157
155
  try {
158
- const { id } = request.params;
159
- const { enabled, publish, url } = request.body;
160
-
161
- if (!ObjectId.isValid(id)) {
162
- return response.status(400).json({ error: "Invalid feed ID" });
163
- }
164
-
165
- if (enabled === undefined && publish === undefined && url === undefined) {
166
- return response.status(400).json({ error: "Nothing to update" });
167
- }
168
-
169
- if (enabled !== undefined && typeof enabled !== "boolean") {
170
- return response.status(400).json({ error: "enabled must be boolean" });
171
- }
172
-
173
156
  const db = request.app.locals.application.getRssDb?.();
174
157
  if (!db) {
175
158
  return response.status(500).json({ error: "Database not available" });
176
159
  }
177
160
 
178
- const feedsCollection = db.collection("rssFeeds");
179
- const feedId = new ObjectId(id);
180
- const feed = await feedsCollection.findOne({ _id: feedId });
181
-
182
- if (!feed) {
183
- return response.status(404).json({
184
- error: response.locals.__("rss.error.feedNotFound"),
185
- });
186
- }
187
-
188
- const update = {};
189
-
190
- if (enabled !== undefined) {
191
- update.enabled = enabled;
192
- }
193
-
194
- if (url !== undefined) {
195
- if (!isValidUrl(url)) {
196
- return response.status(400).json({
197
- error: response.locals.__("rss.error.invalidUrl"),
198
- });
199
- }
200
-
201
- const normalizedUrl = normalizeUrl(url);
202
- const clash = await feedsCollection.findOne({
203
- url: normalizedUrl,
204
- _id: { $ne: feedId },
205
- });
206
-
207
- if (clash) {
208
- return response.status(409).json({
209
- error: response.locals.__("rss.error.feedExists"),
210
- });
211
- }
212
-
213
- update.url = normalizedUrl;
214
- // Items key on feedId, so they survive the change. Any error from the
215
- // old URL is stale the moment it is corrected.
216
- update.lastError = null;
217
- }
218
-
219
- if (publish !== undefined) {
220
- const { postTypes } = request.app.locals.publication || {};
221
-
222
- if (publish.enabled) {
223
- if (!postTypes) {
224
- return response.status(500).json({
225
- error: "Post type configuration unavailable",
226
- });
227
- }
228
-
229
- // postData.create() throws notImplemented for an unconfigured type,
230
- // and the failure would be buried deep in the sync loop. Sites
231
- // differ: chardonsbleus has audio, event, jam and rsvp disabled.
232
- if (!Object.hasOwn(postTypes, publish.postType)) {
233
- return response.status(400).json({
234
- error: `Post type "${publish.postType}" is not enabled on this site`,
235
- });
236
- }
237
- }
238
-
239
- const merged = { ...feed.publish, ...publish };
161
+ const result = await applyFeedSettings(
162
+ db,
163
+ request.params.id,
164
+ request.body,
165
+ request.app.locals.publication,
166
+ );
240
167
 
241
- update.publish = {
242
- ...merged,
243
- // Pre-fill from the post type when the caller left them out, so a
244
- // minimal PATCH of { enabled, postType } yields a working config.
245
- // An explicit null is respected: it means "no link property".
246
- content: merged.content || defaultContent(merged.postType),
247
- linkProperty:
248
- publish.linkProperty === undefined
249
- ? (merged.linkProperty ?? defaultLinkProperty(merged.postType))
250
- : publish.linkProperty,
251
- // Stamp the watermark the first time publishing is switched on, so
252
- // enabling a feed does not publish its entire cached backlog.
253
- since:
254
- publish.enabled && !feed.publish?.since
255
- ? new Date().toISOString()
256
- : (feed.publish?.since ?? null),
257
- };
168
+ if (result.error) {
169
+ return response.status(result.status).json({ error: result.error });
258
170
  }
259
171
 
260
- const result = await feedsCollection.findOneAndUpdate(
261
- { _id: feedId },
262
- { $set: update },
263
- { returnDocument: "after" },
264
- );
265
-
266
172
  response.json({
267
- message:
268
- enabled === undefined
269
- ? response.locals.__("rss.success.feedUpdated")
270
- : enabled
271
- ? response.locals.__("rss.success.feedEnabled")
272
- : response.locals.__("rss.success.feedDisabled"),
273
- feed: formatFeed(result),
173
+ eligible: result.eligible,
174
+ message: response.locals.__("rss.success.feedUpdated"),
175
+ feed: formatFeed(result.feed),
274
176
  });
275
177
  } catch (error) {
276
178
  console.error("[RSS] Error updating feed:", error.message);
@@ -281,70 +183,24 @@ export const feedsController = {
281
183
  /**
282
184
  * Rewind a feed's watermark so past items get published
283
185
  * POST /api/feeds/:id/backfill
284
- * Body: { since?: string } or { last?: number }
285
186
  */
286
187
  async backfill(request, response) {
287
188
  try {
288
- const { id } = request.params;
289
- const { since, last } = request.body;
290
-
291
- if (!ObjectId.isValid(id)) {
292
- return response.status(400).json({ error: "Invalid feed ID" });
293
- }
294
-
295
189
  const db = request.app.locals.application.getRssDb?.();
296
190
  if (!db) {
297
191
  return response.status(500).json({ error: "Database not available" });
298
192
  }
299
193
 
300
- const feedsCollection = db.collection("rssFeeds");
301
- const itemsCollection = db.collection("rssItems");
302
- const feedId = new ObjectId(id);
303
- const feed = await feedsCollection.findOne({ _id: feedId });
304
-
305
- if (!feed) {
306
- return response.status(404).json({
307
- error: response.locals.__("rss.error.feedNotFound"),
308
- });
309
- }
310
-
311
- if (!feed.publish?.enabled) {
312
- return response.status(400).json({
313
- error: "Enable publishing on this feed first",
314
- });
315
- }
194
+ const result = await rewindWatermark(db, request.params.id, request.body);
316
195
 
317
- let oldestPubDate = null;
318
- if (last) {
319
- const items = await itemsCollection
320
- .find({ feedId })
321
- .sort({ pubDate: -1 })
322
- .limit(Number(last))
323
- .toArray();
324
- oldestPubDate = items.at(-1)?.pubDate || null;
196
+ if (result.error) {
197
+ return response.status(result.status).json({ error: result.error });
325
198
  }
326
199
 
327
- let watermark;
328
- try {
329
- watermark = watermarkFor({ since, last }, oldestPubDate);
330
- } catch (error) {
331
- return response.status(400).json({ error: error.message });
332
- }
333
-
334
- await feedsCollection.updateOne(
335
- { _id: feedId },
336
- {
337
- $set: {
338
- "publish.since": watermark,
339
- // Imported history is always drafts, whatever the feed is set to.
340
- "publish.status": "draft",
341
- },
342
- },
343
- );
344
-
345
200
  response.json({
201
+ eligible: result.eligible,
346
202
  message: response.locals.__("rss.success.backfillQueued"),
347
- since: watermark,
203
+ since: result.since,
348
204
  });
349
205
  } catch (error) {
350
206
  console.error("[RSS] Error queueing backfill:", error.message);
@@ -0,0 +1,183 @@
1
+ import { ObjectId } from "mongodb";
2
+
3
+ import { defaultContent, defaultLinkProperty } from "./jf2-builder.js";
4
+ import { pendingQuery, watermarkFor } from "./publisher.js";
5
+ import { isValidUrl, normalizeUrl } from "./utils.js";
6
+
7
+ /**
8
+ * Apply a settings change to one feed.
9
+ *
10
+ * Shared by the JSON API and the dashboard form so the two cannot drift: two
11
+ * copies of this validation would eventually disagree, and the symptom would
12
+ * be a form that accepts what the API rejects.
13
+ * @param {object} db - Database
14
+ * @param {string} id - Feed id
15
+ * @param {object} body - Fields to change: enabled, url, publish
16
+ * @param {object} [publication] - Publication configuration
17
+ * @returns {Promise<object>} Result carrying status, error, feed, eligible
18
+ */
19
+ export async function applyFeedSettings(db, id, body, publication = {}) {
20
+ const { enabled, publish, url } = body;
21
+
22
+ if (!ObjectId.isValid(id)) {
23
+ return { status: 400, error: "Invalid feed ID" };
24
+ }
25
+
26
+ if (enabled === undefined && publish === undefined && url === undefined) {
27
+ return { status: 400, error: "Nothing to update" };
28
+ }
29
+
30
+ if (enabled !== undefined && typeof enabled !== "boolean") {
31
+ return { status: 400, error: "enabled must be boolean" };
32
+ }
33
+
34
+ const feedsCollection = db.collection("rssFeeds");
35
+ const feedId = new ObjectId(id);
36
+ const feed = await feedsCollection.findOne({ _id: feedId });
37
+
38
+ if (!feed) {
39
+ return { status: 404, error: "Feed not found" };
40
+ }
41
+
42
+ const update = {};
43
+
44
+ if (enabled !== undefined) {
45
+ update.enabled = enabled;
46
+ }
47
+
48
+ if (url !== undefined) {
49
+ if (!isValidUrl(url)) {
50
+ return { status: 400, error: "Invalid feed URL" };
51
+ }
52
+
53
+ const normalizedUrl = normalizeUrl(url);
54
+ const clash = await feedsCollection.findOne({
55
+ url: normalizedUrl,
56
+ _id: { $ne: feedId },
57
+ });
58
+
59
+ if (clash) {
60
+ return { status: 409, error: "Another feed already uses that URL" };
61
+ }
62
+
63
+ update.url = normalizedUrl;
64
+ // Items key on feedId, so they survive the change. Any error from the old
65
+ // URL is stale the moment it is corrected.
66
+ update.lastError = null;
67
+ }
68
+
69
+ if (publish !== undefined) {
70
+ const { postTypes } = publication;
71
+
72
+ if (publish.enabled) {
73
+ if (!postTypes) {
74
+ return { status: 500, error: "Post type configuration unavailable" };
75
+ }
76
+
77
+ // postData.create() throws notImplemented for an unconfigured type, and
78
+ // the failure would be buried deep in the sync loop. Sites differ:
79
+ // chardonsbleus has audio, event, jam and rsvp disabled.
80
+ if (!Object.hasOwn(postTypes, publish.postType)) {
81
+ return {
82
+ status: 400,
83
+ error: `Post type "${publish.postType}" is not enabled on this site`,
84
+ };
85
+ }
86
+ }
87
+
88
+ const merged = { ...feed.publish, ...publish };
89
+
90
+ update.publish = {
91
+ ...merged,
92
+ // Pre-fill from the post type when the caller left them out, so a
93
+ // minimal update of enabled plus postType yields a working config. An
94
+ // explicit null is respected: it means "no link property".
95
+ content: merged.content || defaultContent(merged.postType),
96
+ linkProperty:
97
+ publish.linkProperty === undefined
98
+ ? (merged.linkProperty ?? defaultLinkProperty(merged.postType))
99
+ : publish.linkProperty,
100
+ // Stamp the watermark the first time publishing is switched on, so
101
+ // enabling a feed does not publish its entire cached backlog.
102
+ since:
103
+ publish.enabled && !feed.publish?.since
104
+ ? new Date().toISOString()
105
+ : (feed.publish?.since ?? null),
106
+ };
107
+ }
108
+
109
+ const result = await feedsCollection.findOneAndUpdate(
110
+ { _id: feedId },
111
+ { $set: update },
112
+ { returnDocument: "after" },
113
+ );
114
+
115
+ // A feed can be correctly configured and still have nothing to publish, so
116
+ // report the count rather than leaving the user to guess.
117
+ let eligible;
118
+ if (result.publish?.enabled) {
119
+ eligible = await db
120
+ .collection("rssItems")
121
+ .countDocuments(pendingQuery(result));
122
+ }
123
+
124
+ return { status: 200, feed: result, eligible };
125
+ }
126
+
127
+ /**
128
+ * Move a feed's watermark back so already-cached items become eligible.
129
+ * @param {object} db - Database
130
+ * @param {string} id - Feed id
131
+ * @param {object} body - Either a since date or a last item count
132
+ * @returns {Promise<object>} Result carrying status, error, since, eligible
133
+ */
134
+ export async function rewindWatermark(db, id, body) {
135
+ const { since, last } = body;
136
+
137
+ if (!ObjectId.isValid(id)) {
138
+ return { status: 400, error: "Invalid feed ID" };
139
+ }
140
+
141
+ const feedsCollection = db.collection("rssFeeds");
142
+ const itemsCollection = db.collection("rssItems");
143
+ const feedId = new ObjectId(id);
144
+ const feed = await feedsCollection.findOne({ _id: feedId });
145
+
146
+ if (!feed) {
147
+ return { status: 404, error: "Feed not found" };
148
+ }
149
+
150
+ if (!feed.publish?.enabled) {
151
+ return { status: 400, error: "Enable publishing on this feed first" };
152
+ }
153
+
154
+ let oldestPubDate = null;
155
+ if (last) {
156
+ const items = await itemsCollection
157
+ .find({ feedId })
158
+ .sort({ pubDate: -1 })
159
+ .limit(Number(last))
160
+ .toArray();
161
+ oldestPubDate = items.at(-1)?.pubDate || null;
162
+ }
163
+
164
+ let watermark;
165
+ try {
166
+ watermark = watermarkFor({ since, last }, oldestPubDate);
167
+ } catch (error) {
168
+ return { status: 400, error: error.message };
169
+ }
170
+
171
+ // The feed's own post status is respected: silently forcing drafts would
172
+ // override a deliberate choice. The guard is the count the user asked for,
173
+ // and maxPostsPerCycle pacing the catch-up.
174
+ const updated = await feedsCollection.findOneAndUpdate(
175
+ { _id: feedId },
176
+ { $set: { "publish.since": watermark } },
177
+ { returnDocument: "after" },
178
+ );
179
+
180
+ const eligible = await itemsCollection.countDocuments(pendingQuery(updated));
181
+
182
+ return { status: 200, since: watermark, eligible };
183
+ }
package/lib/publisher.js CHANGED
@@ -102,6 +102,44 @@ export function resolvePublishContext(application = {}, publication = {}) {
102
102
 
103
103
  const MAX_ATTEMPTS = 3;
104
104
 
105
+ /**
106
+ * Query selecting a feed's items that are still waiting to be published.
107
+ *
108
+ * Exported so the dashboard can count them with exactly the same rule the
109
+ * publish loop applies. Two copies of this would drift, and the symptom would
110
+ * be a count that disagrees with what actually publishes.
111
+ * @param {object} feed - Feed document
112
+ * @returns {object} MongoDB query
113
+ */
114
+ export function pendingQuery(feed) {
115
+ const query = {
116
+ feedId: feed._id,
117
+ postedAt: { $exists: false },
118
+ postSkipped: { $ne: true },
119
+ };
120
+
121
+ if (feed.publish?.since) {
122
+ const since = feed.publish.since;
123
+
124
+ // One watermark, two operand types, because the two fields are stored
125
+ // differently: pubDate is a BSON Date (rss-client builds real Dates and
126
+ // pruneOldItems compares it against one), while fetchedAt — like every
127
+ // other date here — is an ISO string. MongoDB orders by BSON type before
128
+ // value, so a Date is never $gt a String: passing the ISO string to
129
+ // pubDate matches nothing at all, with no error to show for it.
130
+ //
131
+ // An item with no parsable date would also be excluded forever by a bare
132
+ // comparison, because null sorts below every Date. fetchedAt is always set
133
+ // on insert and stands in when pubDate is missing.
134
+ query.$or = [
135
+ { pubDate: { $gt: new Date(since) } },
136
+ { pubDate: null, fetchedAt: { $gt: since } },
137
+ ];
138
+ }
139
+
140
+ return query;
141
+ }
142
+
105
143
  /**
106
144
  * Publish the pending items of one feed.
107
145
  *
@@ -131,22 +169,7 @@ export async function publishPending(feed, itemsCollection, options) {
131
169
  mintImpl = mintToken,
132
170
  } = options;
133
171
 
134
- const query = {
135
- feedId: feed._id,
136
- postedAt: { $exists: false },
137
- postSkipped: { $ne: true },
138
- };
139
-
140
- if (feed.publish.since) {
141
- // An item with no parsable date would be excluded forever by a bare
142
- // `pubDate: { $gt: since }`, because null sorts below every Date in BSON —
143
- // a silent permanent no-op. fetchedAt is always set on insert and is
144
- // monotonic with cache entry, so it stands in when pubDate is missing.
145
- query.$or = [
146
- { pubDate: { $gt: feed.publish.since } },
147
- { pubDate: null, fetchedAt: { $gt: feed.publish.since } },
148
- ];
149
- }
172
+ const query = pendingQuery(feed);
150
173
 
151
174
  const items = await itemsCollection
152
175
  .find(query)
package/locales/en.json CHANGED
@@ -42,7 +42,7 @@
42
42
  "feedUpdated": "Feed updated",
43
43
  "feedEnabled": "Feed enabled",
44
44
  "feedDisabled": "Feed disabled",
45
- "backfillQueued": "Backfill queued; items will be published as drafts over the next sync cycles",
45
+ "backfillQueued": "Backfill queued: the newest cached items will be published over the next sync cycles, using this feed's post status.",
46
46
  "syncComplete": "Sync complete",
47
47
  "clearResync": "Items cleared and re-synced"
48
48
  },
@@ -68,6 +68,11 @@
68
68
  "title": "Feed settings",
69
69
  "url": "Feed URL",
70
70
  "urlHint": "Correcting this keeps the cached items; removing and re-adding the feed would lose them."
71
+ },
72
+ "backfill": {
73
+ "label": "Publish existing items",
74
+ "hint": "How many of the newest cached items to publish. They are created with this feed's post status, a few per sync cycle.",
75
+ "submit": "Publish existing items"
71
76
  }
72
77
  }
73
78
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-rss",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "RSS feed reader endpoint for Indiekit. Aggregates multiple feeds, caches in MongoDB, displays on frontend.",
5
5
  "keywords": [
6
6
  "indiekit",
package/views/rss.njk CHANGED
@@ -113,7 +113,7 @@
113
113
  classes: "rss-publish",
114
114
  summary: publishSummary
115
115
  }) %}
116
- <form data-publish-feed="{{ feed.id }}">
116
+ <form action="{{ mountPath }}/feeds/{{ feed.id }}/settings" method="post">
117
117
  {{ input({
118
118
  id: "feed-url-" + feed.id,
119
119
  name: "url",
@@ -180,6 +180,24 @@
180
180
  text: __("rss.publish.save")
181
181
  }) }}
182
182
  </form>
183
+
184
+ {% if feed.publish.enabled %}
185
+ <form class="rss-backfill" action="{{ mountPath }}/feeds/{{ feed.id }}/backfill" method="post">
186
+ {{ input({
187
+ id: "backfill-last-" + feed.id,
188
+ name: "last",
189
+ type: "number",
190
+ label: __("rss.backfill.label"),
191
+ hint: __("rss.backfill.hint"),
192
+ value: "10"
193
+ }) }}
194
+ {{ button({
195
+ classes: "button--secondary",
196
+ type: "submit",
197
+ text: __("rss.backfill.submit")
198
+ }) }}
199
+ </form>
200
+ {% endif %}
183
201
  {% endcall %}
184
202
  </div>
185
203
  <div class="rss-feed-actions">
@@ -362,40 +380,5 @@
362
380
  });
363
381
  });
364
382
 
365
- // Handle publish configuration
366
- document.querySelectorAll('[data-publish-feed]').forEach(form => {
367
- form.addEventListener('submit', async (e) => {
368
- e.preventDefault();
369
- const feedId = e.target.dataset.publishFeed;
370
- const data = new FormData(e.target);
371
-
372
- const publish = {
373
- // The checkboxes component sets value="true"; an unchecked box is
374
- // omitted from FormData entirely, so presence is the signal.
375
- enabled: data.get('enabled') !== null,
376
- postType: data.get('postType'),
377
- content: data.get('content'),
378
- linkProperty: data.get('linkProperty') || null,
379
- status: data.get('status'),
380
- };
381
-
382
- try {
383
- const response = await fetch(`{{ mountPath }}/api/feeds/${feedId}`, {
384
- method: 'PATCH',
385
- headers: { 'Content-Type': 'application/json' },
386
- body: JSON.stringify({ url: data.get('url'), publish })
387
- });
388
-
389
- if (response.ok) {
390
- location.reload();
391
- } else {
392
- const body = await response.json();
393
- alert(body.error || 'Failed to save publish settings');
394
- }
395
- } catch (err) {
396
- alert('Failed to save publish settings: ' + err.message);
397
- }
398
- });
399
- });
400
383
  </script>
401
384
  {% endblock %}