@rmdes/indiekit-endpoint-rss 1.1.1 → 1.1.2

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/assets/styles.css CHANGED
@@ -83,19 +83,28 @@
83
83
  }
84
84
 
85
85
  .rss-feed-item {
86
- align-items: center;
86
+ align-items: flex-start;
87
87
  border-block-end: 1px solid var(--color-border);
88
88
  display: flex;
89
- flex-wrap: wrap;
90
89
  gap: var(--space-s);
91
90
  padding: var(--space-s) 0;
92
91
  }
93
92
 
94
- /* The publish disclosure gets a row of its own. As a plain flex sibling of the
95
- icon, info and actions it was squeezed into the same line and overlapped the
96
- feed metadata. */
93
+ /* The publish disclosure lives inside the feed's own column. As a full-width
94
+ flex row it turned every feed into three rows and put a bar wider than the
95
+ feed between each entry, which made the list unscannable. Its summary stays
96
+ quiet so the feed title remains what the eye lands on. */
97
97
  .rss-publish {
98
- flex-basis: 100%;
98
+ margin-block-start: var(--space-2xs);
99
+ }
100
+
101
+ .rss-publish .details__summary {
102
+ color: var(--color-text-secondary);
103
+ font-size: var(--step--1);
104
+ }
105
+
106
+ .rss-publish .details__main {
107
+ padding-block-start: var(--space-2xs);
99
108
  }
100
109
 
101
110
  .rss-feed-item:last-child {
@@ -156,13 +156,13 @@ export const feedsController = {
156
156
  async toggle(request, response) {
157
157
  try {
158
158
  const { id } = request.params;
159
- const { enabled, publish } = request.body;
159
+ const { enabled, publish, url } = request.body;
160
160
 
161
161
  if (!ObjectId.isValid(id)) {
162
162
  return response.status(400).json({ error: "Invalid feed ID" });
163
163
  }
164
164
 
165
- if (enabled === undefined && publish === undefined) {
165
+ if (enabled === undefined && publish === undefined && url === undefined) {
166
166
  return response.status(400).json({ error: "Nothing to update" });
167
167
  }
168
168
 
@@ -191,6 +191,31 @@ export const feedsController = {
191
191
  update.enabled = enabled;
192
192
  }
193
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
+
194
219
  if (publish !== undefined) {
195
220
  const { postTypes } = request.app.locals.publication || {};
196
221
 
package/lib/sync.js CHANGED
@@ -173,7 +173,7 @@ export async function runSync(dbOrIndiekit, options, publishContext) {
173
173
  itemsCollection,
174
174
  feedsCollection,
175
175
  retentionDays,
176
- options.minItemsPerFeed ?? 10,
176
+ retentionFloor(options),
177
177
  );
178
178
 
179
179
  syncState.lastSync = new Date().toISOString();
@@ -310,6 +310,21 @@ async function createIndexes(feedsCollection, itemsCollection) {
310
310
  await itemsCollection.createIndex({ fetchedAt: -1 });
311
311
  }
312
312
 
313
+ /**
314
+ * Number of newest items per feed that pruning must never touch.
315
+ *
316
+ * Pruning an item the feed still serves is a no-op by construction — the next
317
+ * sync re-inserts it. A sync stores at most `maxItemsPerFeed` items per feed,
318
+ * so age-based retention below that threshold can only churn: insert, delete,
319
+ * refetch, forever. The floor is therefore derived from what a sync can store,
320
+ * not set as an independent constant.
321
+ * @param {object} options - Plugin options
322
+ * @returns {number} Items to keep per feed regardless of age
323
+ */
324
+ export function retentionFloor(options = {}) {
325
+ return Math.max(options.minItemsPerFeed ?? 10, options.maxItemsPerFeed ?? 50);
326
+ }
327
+
313
328
  /**
314
329
  * Rewrite legacy BSON Date values of `addedAt` as ISO 8601 strings.
315
330
  *
package/locales/en.json CHANGED
@@ -56,13 +56,18 @@
56
56
  "enabled": "Create posts from this feed",
57
57
  "postType": "Post type",
58
58
  "content": "Content template",
59
- "contentHint": "Placeholders: {{title}} {{link}} {{description}} {{content}} {{author}} {{sourceTitle}}",
59
+ "contentHint": "Available: title, link, description, content, author, sourceTitle. Write each one in double braces, as shown below.",
60
60
  "linkProperty": "Link property",
61
61
  "linkPropertyHint": "Discovery property carrying the item URL, e.g. bookmark-of. Leave empty for a plain note or article.",
62
62
  "status": "Post status",
63
63
  "draft": "Draft",
64
64
  "published": "Published",
65
- "save": "Save publish settings"
65
+ "save": "Save settings"
66
+ },
67
+ "settings": {
68
+ "title": "Feed settings",
69
+ "url": "Feed URL",
70
+ "urlHint": "Correcting this keeps the cached items; removing and re-adding the feed would lose them."
66
71
  }
67
72
  }
68
73
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-rss",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
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
@@ -100,79 +100,88 @@
100
100
  {% if feed.lastError %}
101
101
  <div class="rss-feed-error">{{ feed.lastError }}</div>
102
102
  {% endif %}
103
- </div>
104
- {% set typeItems = [] %}
105
- {% for type in postTypes %}
106
- {% set typeItems = (typeItems.push({
107
- text: type,
108
- value: type,
109
- selected: feed.publish.postType == type
110
- }), typeItems) %}
111
- {% endfor %}
112
- {% set publishSummary %}{{ __("rss.publish.title") }}{% if feed.publish.enabled %} &bull; {{ feed.publish.postType }}{% endif %}{% endset %}
113
- {% call details({
114
- classes: "rss-publish",
115
- summary: publishSummary
116
- }) %}
117
- <form data-publish-feed="{{ feed.id }}">
118
- {{ checkboxes({
119
- idPrefix: "publish-enabled-" + feed.id,
120
- name: "enabled",
121
- items: [{
122
- value: "true",
123
- text: __("rss.publish.enabled"),
124
- checked: feed.publish.enabled
125
- }]
126
- }) }}
103
+ {% set typeItems = [] %}
104
+ {% for type in postTypes %}
105
+ {% set typeItems = (typeItems.push({
106
+ text: type,
107
+ value: type,
108
+ selected: feed.publish.postType == type
109
+ }), typeItems) %}
110
+ {% endfor %}
111
+ {% set publishSummary %}{{ __("rss.settings.title") }}{% if feed.publish.enabled %} &bull; {{ feed.publish.postType }}{% endif %}{% endset %}
112
+ {% call details({
113
+ classes: "rss-publish",
114
+ summary: publishSummary
115
+ }) %}
116
+ <form data-publish-feed="{{ feed.id }}">
117
+ {{ input({
118
+ id: "feed-url-" + feed.id,
119
+ name: "url",
120
+ type: "url",
121
+ label: __("rss.settings.url"),
122
+ hint: __("rss.settings.urlHint"),
123
+ value: feed.url
124
+ }) }}
125
+
126
+ {{ checkboxes({
127
+ idPrefix: "publish-enabled-" + feed.id,
128
+ name: "enabled",
129
+ items: [{
130
+ value: "true",
131
+ label: __("rss.publish.enabled"),
132
+ checked: feed.publish.enabled
133
+ }]
134
+ }) }}
127
135
 
128
- {{ select({
129
- id: "publish-post-type-" + feed.id,
130
- name: "postType",
131
- label: __("rss.publish.postType"),
132
- items: typeItems
133
- }) }}
136
+ {{ select({
137
+ id: "publish-post-type-" + feed.id,
138
+ name: "postType",
139
+ label: __("rss.publish.postType"),
140
+ items: typeItems
141
+ }) }}
134
142
 
135
- {{ input({
136
- id: "publish-content-" + feed.id,
137
- name: "content",
138
- label: __("rss.publish.content"),
139
- hint: __("rss.publish.contentHint"),
140
- value: feed.publish.content or "{{description}}"
141
- }) }}
143
+ {{ input({
144
+ id: "publish-content-" + feed.id,
145
+ name: "content",
146
+ label: __("rss.publish.content"),
147
+ hint: __("rss.publish.contentHint"),
148
+ value: feed.publish.content or "{{description}}"
149
+ }) }}
142
150
 
143
- {{ input({
144
- id: "publish-link-property-" + feed.id,
145
- name: "linkProperty",
146
- label: __("rss.publish.linkProperty"),
147
- hint: __("rss.publish.linkPropertyHint"),
148
- optional: true,
149
- value: feed.publish.linkProperty
150
- }) }}
151
+ {{ input({
152
+ id: "publish-link-property-" + feed.id,
153
+ name: "linkProperty",
154
+ label: __("rss.publish.linkProperty"),
155
+ hint: __("rss.publish.linkPropertyHint"),
156
+ optional: true,
157
+ value: feed.publish.linkProperty
158
+ }) }}
151
159
 
152
- {{ select({
153
- id: "publish-status-" + feed.id,
154
- name: "status",
155
- label: __("rss.publish.status"),
156
- items: [
157
- {
158
- text: __("rss.publish.draft"),
159
- value: "draft",
160
- selected: feed.publish.status != "published"
161
- },
162
- {
163
- text: __("rss.publish.published"),
164
- value: "published",
165
- selected: feed.publish.status == "published"
166
- }
167
- ]
168
- }) }}
160
+ {{ select({
161
+ id: "publish-status-" + feed.id,
162
+ name: "status",
163
+ label: __("rss.publish.status"),
164
+ items: [
165
+ {
166
+ text: __("rss.publish.draft"),
167
+ value: "draft",
168
+ selected: feed.publish.status != "published"
169
+ },
170
+ {
171
+ text: __("rss.publish.published"),
172
+ value: "published",
173
+ selected: feed.publish.status == "published"
174
+ }
175
+ ]
176
+ }) }}
169
177
 
170
- {{ button({
171
- type: "submit",
172
- text: __("rss.publish.save")
173
- }) }}
174
- </form>
175
- {% endcall %}
178
+ {{ button({
179
+ type: "submit",
180
+ text: __("rss.publish.save")
181
+ }) }}
182
+ </form>
183
+ {% endcall %}
184
+ </div>
176
185
  <div class="rss-feed-actions">
177
186
  <label class="rss-toggle">
178
187
  <input
@@ -374,7 +383,7 @@
374
383
  const response = await fetch(`{{ mountPath }}/api/feeds/${feedId}`, {
375
384
  method: 'PATCH',
376
385
  headers: { 'Content-Type': 'application/json' },
377
- body: JSON.stringify({ publish })
386
+ body: JSON.stringify({ url: data.get('url'), publish })
378
387
  });
379
388
 
380
389
  if (response.ok) {