@rmdes/indiekit-endpoint-rss 1.1.0 → 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 +18 -1
- package/lib/controllers/feeds.js +27 -2
- package/lib/sync.js +16 -1
- package/locales/en.json +11 -2
- package/package.json +1 -1
- package/views/rss.njk +85 -35
package/assets/styles.css
CHANGED
|
@@ -83,13 +83,30 @@
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
.rss-feed-item {
|
|
86
|
-
align-items:
|
|
86
|
+
align-items: flex-start;
|
|
87
87
|
border-block-end: 1px solid var(--color-border);
|
|
88
88
|
display: flex;
|
|
89
89
|
gap: var(--space-s);
|
|
90
90
|
padding: var(--space-s) 0;
|
|
91
91
|
}
|
|
92
92
|
|
|
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
|
+
.rss-publish {
|
|
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);
|
|
108
|
+
}
|
|
109
|
+
|
|
93
110
|
.rss-feed-item:last-child {
|
|
94
111
|
border-block-end: none;
|
|
95
112
|
}
|
package/lib/controllers/feeds.js
CHANGED
|
@@ -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
|
|
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
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"feedAdded": "Feed added successfully",
|
|
41
41
|
"feedRemoved": "Feed removed",
|
|
42
42
|
"feedUpdated": "Feed updated",
|
|
43
|
-
|
|
43
|
+
"feedEnabled": "Feed enabled",
|
|
44
44
|
"feedDisabled": "Feed disabled",
|
|
45
45
|
"backfillQueued": "Backfill queued; items will be published as drafts over the next sync cycles",
|
|
46
46
|
"syncComplete": "Sync complete",
|
|
@@ -56,9 +56,18 @@
|
|
|
56
56
|
"enabled": "Create posts from this feed",
|
|
57
57
|
"postType": "Post type",
|
|
58
58
|
"content": "Content template",
|
|
59
|
+
"contentHint": "Available: title, link, description, content, author, sourceTitle. Write each one in double braces, as shown below.",
|
|
59
60
|
"linkProperty": "Link property",
|
|
61
|
+
"linkPropertyHint": "Discovery property carrying the item URL, e.g. bookmark-of. Leave empty for a plain note or article.",
|
|
60
62
|
"status": "Post status",
|
|
61
|
-
"
|
|
63
|
+
"draft": "Draft",
|
|
64
|
+
"published": "Published",
|
|
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."
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
73
|
}
|
package/package.json
CHANGED
package/views/rss.njk
CHANGED
|
@@ -100,40 +100,88 @@
|
|
|
100
100
|
{% if feed.lastError %}
|
|
101
101
|
<div class="rss-feed-error">{{ feed.lastError }}</div>
|
|
102
102
|
{% endif %}
|
|
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 %} • {{ 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
|
+
}) }}
|
|
135
|
+
|
|
136
|
+
{{ select({
|
|
137
|
+
id: "publish-post-type-" + feed.id,
|
|
138
|
+
name: "postType",
|
|
139
|
+
label: __("rss.publish.postType"),
|
|
140
|
+
items: typeItems
|
|
141
|
+
}) }}
|
|
142
|
+
|
|
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
|
+
}) }}
|
|
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
|
+
}) }}
|
|
159
|
+
|
|
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
|
+
}) }}
|
|
177
|
+
|
|
178
|
+
{{ button({
|
|
179
|
+
type: "submit",
|
|
180
|
+
text: __("rss.publish.save")
|
|
181
|
+
}) }}
|
|
182
|
+
</form>
|
|
183
|
+
{% endcall %}
|
|
103
184
|
</div>
|
|
104
|
-
<details class="rss-publish">
|
|
105
|
-
<summary>{{ __("rss.publish.title") }}{% if feed.publish.enabled %} • {{ feed.publish.postType }}{% endif %}</summary>
|
|
106
|
-
<form data-publish-feed="{{ feed.id }}">
|
|
107
|
-
<label>
|
|
108
|
-
<input type="checkbox" name="enabled" {{ "checked" if feed.publish.enabled }}>
|
|
109
|
-
{{ __("rss.publish.enabled") }}
|
|
110
|
-
</label>
|
|
111
|
-
<label>
|
|
112
|
-
{{ __("rss.publish.postType") }}
|
|
113
|
-
<select name="postType">
|
|
114
|
-
{% for type in postTypes %}
|
|
115
|
-
<option value="{{ type }}" {{ "selected" if feed.publish.postType == type }}>{{ type }}</option>
|
|
116
|
-
{% endfor %}
|
|
117
|
-
</select>
|
|
118
|
-
</label>
|
|
119
|
-
<label>
|
|
120
|
-
{{ __("rss.publish.content") }}
|
|
121
|
-
<input class="input" type="text" name="content" value="{{ feed.publish.content or '{{description}}' }}">
|
|
122
|
-
</label>
|
|
123
|
-
<label>
|
|
124
|
-
{{ __("rss.publish.linkProperty") }}
|
|
125
|
-
<input class="input" type="text" name="linkProperty" value="{{ feed.publish.linkProperty or '' }}">
|
|
126
|
-
</label>
|
|
127
|
-
<label>
|
|
128
|
-
{{ __("rss.publish.status") }}
|
|
129
|
-
<select name="status">
|
|
130
|
-
<option value="draft" {{ "selected" if feed.publish.status != "published" }}>draft</option>
|
|
131
|
-
<option value="published" {{ "selected" if feed.publish.status == "published" }}>published</option>
|
|
132
|
-
</select>
|
|
133
|
-
</label>
|
|
134
|
-
{{ button({ type: "submit", text: __("rss.publish.save") }) }}
|
|
135
|
-
</form>
|
|
136
|
-
</details>
|
|
137
185
|
<div class="rss-feed-actions">
|
|
138
186
|
<label class="rss-toggle">
|
|
139
187
|
<input
|
|
@@ -322,7 +370,9 @@
|
|
|
322
370
|
const data = new FormData(e.target);
|
|
323
371
|
|
|
324
372
|
const publish = {
|
|
325
|
-
|
|
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,
|
|
326
376
|
postType: data.get('postType'),
|
|
327
377
|
content: data.get('content'),
|
|
328
378
|
linkProperty: data.get('linkProperty') || null,
|
|
@@ -333,7 +383,7 @@
|
|
|
333
383
|
const response = await fetch(`{{ mountPath }}/api/feeds/${feedId}`, {
|
|
334
384
|
method: 'PATCH',
|
|
335
385
|
headers: { 'Content-Type': 'application/json' },
|
|
336
|
-
body: JSON.stringify({ publish })
|
|
386
|
+
body: JSON.stringify({ url: data.get('url'), publish })
|
|
337
387
|
});
|
|
338
388
|
|
|
339
389
|
if (response.ok) {
|