@rmdes/indiekit-endpoint-blogroll 1.0.15 → 1.0.17

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
@@ -130,16 +130,6 @@
130
130
  gap: var(--space-s);
131
131
  }
132
132
 
133
- .blogroll-filter-select {
134
- appearance: none;
135
- background-color: var(--color-background);
136
- border: 1px solid var(--color-border);
137
- border-radius: var(--radius-s);
138
- font-size: var(--step--1);
139
- min-inline-size: 150px;
140
- padding: var(--space-2xs) var(--space-s);
141
- }
142
-
143
133
  /* Form fields */
144
134
  .blogroll-form {
145
135
  max-inline-size: 600px;
@@ -152,39 +142,10 @@
152
142
  margin-block-end: var(--space-m);
153
143
  }
154
144
 
155
- .blogroll-field label {
156
- font-weight: var(--font-weight-semibold);
157
- }
158
-
159
- .blogroll-field-hint {
160
- color: var(--color-text-secondary);
161
- font-size: var(--step--1);
162
- }
163
-
164
- .blogroll-field input,
165
- .blogroll-field select,
166
- .blogroll-field textarea {
167
- appearance: none;
168
- background-color: var(--color-background);
169
- border: 1px solid var(--color-border);
170
- border-radius: var(--radius-s);
171
- font-size: var(--step--1);
172
- padding: var(--space-2xs) var(--space-s);
173
- width: 100%;
174
- }
175
-
176
- .blogroll-field textarea {
145
+ .blogroll-field .textarea {
177
146
  min-block-size: 100px;
178
147
  }
179
148
 
180
- .blogroll-field input:focus,
181
- .blogroll-field select:focus,
182
- .blogroll-field textarea:focus {
183
- border-color: var(--color-accent);
184
- outline: 2px solid var(--color-accent);
185
- outline-offset: 1px;
186
- }
187
-
188
149
  .blogroll-field--inline {
189
150
  align-items: center;
190
151
  flex-direction: row;
@@ -256,21 +217,6 @@
256
217
  margin-block-end: var(--space-s);
257
218
  }
258
219
 
259
- .blogroll-discover__input {
260
- display: flex;
261
- gap: var(--space-s);
262
- }
263
-
264
- .blogroll-discover__input input {
265
- appearance: none;
266
- background-color: var(--color-background);
267
- border: 1px solid var(--color-border);
268
- border-radius: var(--radius-s);
269
- flex: 1;
270
- font-size: var(--step--1);
271
- padding: var(--space-2xs) var(--space-s);
272
- }
273
-
274
220
  .blogroll-discover__result {
275
221
  background: var(--color-background);
276
222
  border-radius: var(--radius-s);
@@ -86,7 +86,7 @@ export async function getBlogByFeedUrl(application, feedUrl) {
86
86
  */
87
87
  export async function createBlog(application, data) {
88
88
  const collection = getCollection(application);
89
- const now = new Date();
89
+ const now = new Date().toISOString();
90
90
 
91
91
  const blog = {
92
92
  sourceId: data.sourceId ? new ObjectId(data.sourceId) : null,
@@ -127,7 +127,7 @@ export async function updateBlog(application, id, data) {
127
127
 
128
128
  const update = {
129
129
  ...data,
130
- updatedAt: new Date(),
130
+ updatedAt: new Date().toISOString(),
131
131
  };
132
132
 
133
133
  // Remove fields that shouldn't be updated directly
@@ -162,8 +162,8 @@ export async function deleteBlog(application, id) {
162
162
  $set: {
163
163
  status: "deleted",
164
164
  hidden: true,
165
- deletedAt: new Date(),
166
- updatedAt: new Date(),
165
+ deletedAt: new Date().toISOString(),
166
+ updatedAt: new Date().toISOString(),
167
167
  },
168
168
  }
169
169
  );
@@ -181,12 +181,12 @@ export async function updateBlogStatus(application, id, status) {
181
181
  const objectId = typeof id === "string" ? new ObjectId(id) : id;
182
182
 
183
183
  const update = {
184
- updatedAt: new Date(),
184
+ updatedAt: new Date().toISOString(),
185
185
  };
186
186
 
187
187
  if (status.success) {
188
188
  update.status = "active";
189
- update.lastFetchAt = new Date();
189
+ update.lastFetchAt = new Date().toISOString();
190
190
  update.lastError = null;
191
191
  if (status.itemCount !== undefined) {
192
192
  update.itemCount = status.itemCount;
@@ -246,7 +246,7 @@ export async function getCategories(application) {
246
246
  */
247
247
  export async function upsertBlog(application, data) {
248
248
  const collection = getCollection(application);
249
- const now = new Date();
249
+ const now = new Date().toISOString();
250
250
 
251
251
  // Skip if a blog with this feedUrl was soft-deleted
252
252
  const deleted = await collection.findOne({
@@ -206,7 +206,7 @@ export async function countItems(application, options = {}) {
206
206
  */
207
207
  export async function upsertItem(application, data) {
208
208
  const collection = getCollection(application);
209
- const now = new Date();
209
+ const now = new Date().toISOString();
210
210
 
211
211
  const result = await collection.updateOne(
212
212
  { blogId: new ObjectId(data.blogId), uid: data.uid },
@@ -45,7 +45,7 @@ export async function getSource(application, id) {
45
45
  */
46
46
  export async function createSource(application, data) {
47
47
  const collection = getCollection(application);
48
- const now = new Date();
48
+ const now = new Date().toISOString();
49
49
 
50
50
  const source = {
51
51
  type: data.type, // "opml_url" | "opml_file" | "manual" | "json_feed" | "microsub"
@@ -80,7 +80,7 @@ export async function updateSource(application, id, data) {
80
80
 
81
81
  const update = {
82
82
  ...data,
83
- updatedAt: new Date(),
83
+ updatedAt: new Date().toISOString(),
84
84
  };
85
85
 
86
86
  // Remove fields that shouldn't be updated directly
@@ -135,11 +135,11 @@ export async function updateSourceSyncStatus(application, id, status) {
135
135
  const objectId = typeof id === "string" ? new ObjectId(id) : id;
136
136
 
137
137
  const update = {
138
- updatedAt: new Date(),
138
+ updatedAt: new Date().toISOString(),
139
139
  };
140
140
 
141
141
  if (status.success) {
142
- update.lastSyncAt = new Date();
142
+ update.lastSyncAt = new Date().toISOString();
143
143
  update.lastSyncError = null;
144
144
  } else {
145
145
  update.lastSyncError = status.error;
package/lib/sync/feed.js CHANGED
@@ -142,8 +142,8 @@ function parseJsonFeed(content, feedUrl, maxItems) {
142
142
  text: item.content_text,
143
143
  },
144
144
  summary: decodeEntities(item.summary) || truncateText(item.content_text, 300),
145
- published: item.date_published ? new Date(item.date_published) : new Date(),
146
- updated: item.date_modified ? new Date(item.date_modified) : undefined,
145
+ published: item.date_published ? new Date(item.date_published).toISOString() : new Date().toISOString(),
146
+ updated: item.date_modified ? new Date(item.date_modified).toISOString() : undefined,
147
147
  author: item.author || (item.authors?.[0]),
148
148
  photo: item.image ? [item.image] : undefined,
149
149
  categories: item.tags || [],
@@ -168,6 +168,10 @@ function parseJsonFeed(content, feedUrl, maxItems) {
168
168
  function normalizeItem(item, feedUrl) {
169
169
  const description = item.description || item.summary || "";
170
170
 
171
+ // Convert dates to ISO strings - feedparser returns Date objects
172
+ const published = item.pubdate || item.date;
173
+ const updated = item.date;
174
+
171
175
  return {
172
176
  uid: generateUid(feedUrl, item.guid || item.link),
173
177
  url: item.link || item.origlink,
@@ -177,8 +181,8 @@ function normalizeItem(item, feedUrl) {
177
181
  text: stripHtml(description),
178
182
  },
179
183
  summary: truncateText(stripHtml(item.summary || description), 300),
180
- published: item.pubdate || item.date || new Date(),
181
- updated: item.date,
184
+ published: published ? (published instanceof Date ? published.toISOString() : new Date(published).toISOString()) : new Date().toISOString(),
185
+ updated: updated ? (updated instanceof Date ? updated.toISOString() : new Date(updated).toISOString()) : undefined,
182
186
  author: item.author ? { name: item.author } : undefined,
183
187
  photo: extractPhotos(item),
184
188
  categories: item.categories || [],
@@ -100,8 +100,8 @@ export async function syncMicrosubSource(application, source) {
100
100
  $set: {
101
101
  status: "deleted",
102
102
  hidden: true,
103
- deletedAt: new Date(),
104
- updatedAt: new Date(),
103
+ deletedAt: new Date().toISOString(),
104
+ updatedAt: new Date().toISOString(),
105
105
  },
106
106
  }
107
107
  );
@@ -227,8 +227,8 @@ export async function handleMicrosubWebhook(application, data) {
227
227
  {
228
228
  $set: {
229
229
  status: "inactive",
230
- unsubscribedAt: new Date(),
231
- updatedAt: new Date(),
230
+ unsubscribedAt: new Date().toISOString(),
231
+ updatedAt: new Date().toISOString(),
232
232
  },
233
233
  }
234
234
  );
@@ -110,7 +110,7 @@ export async function runFullSync(application, options = {}) {
110
110
  {
111
111
  $set: {
112
112
  key: "syncStats",
113
- lastFullSync: new Date(),
113
+ lastFullSync: new Date().toISOString(),
114
114
  duration,
115
115
  sources: {
116
116
  total: enabledSources.length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-blogroll",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "Blogroll endpoint for Indiekit. Aggregates blog feeds from OPML, JSON feeds, or manual entry.",
5
5
  "keywords": [
6
6
  "indiekit",
@@ -5,12 +5,12 @@
5
5
  {% if isNew %}
6
6
  <div class="blogroll-discover">
7
7
  <div class="blogroll-field">
8
- <label for="discoverUrl">{{ __("blogroll.blogs.form.discoverUrl") }}</label>
9
- <div class="blogroll-discover__input">
10
- <input type="url" id="discoverUrl" placeholder="https://tantek.com">
8
+ <label class="label" for="discoverUrl">{{ __("blogroll.blogs.form.discoverUrl") }}</label>
9
+ <div class="input-button-group">
10
+ <input class="input" type="url" id="discoverUrl" placeholder="https://tantek.com">
11
11
  {{ button({ type: "button", text: __("blogroll.blogs.form.discover"), classes: "button--secondary", attributes: { id: "discoverBtn" } }) }}
12
12
  </div>
13
- <span class="blogroll-field-hint">{{ __("blogroll.blogs.form.discoverHint") }}</span>
13
+ <span class="hint">{{ __("blogroll.blogs.form.discoverHint") }}</span>
14
14
  </div>
15
15
  <div id="discoverResult" class="blogroll-discover__result" style="display: none;"></div>
16
16
  </div>
@@ -18,39 +18,39 @@
18
18
  {% endif %}
19
19
 
20
20
  <div class="blogroll-field">
21
- <label for="feedUrl">{{ __("blogroll.blogs.form.feedUrl") }}</label>
22
- <input type="url" id="feedUrl" name="feedUrl" value="{{ blog.feedUrl if blog else '' }}" required placeholder="https://example.com/feed.xml">
23
- <span class="blogroll-field-hint">{{ __("blogroll.blogs.form.feedUrlHint") }}</span>
21
+ <label class="label" for="feedUrl">{{ __("blogroll.blogs.form.feedUrl") }}</label>
22
+ <input class="input" type="url" id="feedUrl" name="feedUrl" value="{{ blog.feedUrl if blog else '' }}" required placeholder="https://example.com/feed.xml">
23
+ <span class="hint">{{ __("blogroll.blogs.form.feedUrlHint") }}</span>
24
24
  </div>
25
25
 
26
26
  <div class="blogroll-field">
27
- <label for="title">{{ __("blogroll.blogs.form.title") }}</label>
28
- <input type="text" id="title" name="title" value="{{ blog.title if blog else '' }}" placeholder="{{ __('blogroll.blogs.form.titlePlaceholder') }}">
29
- <span class="blogroll-field-hint">{{ __("blogroll.blogs.form.titleHint") }}</span>
27
+ <label class="label" for="title">{{ __("blogroll.blogs.form.title") }}</label>
28
+ <input class="input" type="text" id="title" name="title" value="{{ blog.title if blog else '' }}" placeholder="{{ __('blogroll.blogs.form.titlePlaceholder') }}">
29
+ <span class="hint">{{ __("blogroll.blogs.form.titleHint") }}</span>
30
30
  </div>
31
31
 
32
32
  <div class="blogroll-field">
33
- <label for="siteUrl">{{ __("blogroll.blogs.form.siteUrl") }}</label>
34
- <input type="url" id="siteUrl" name="siteUrl" value="{{ blog.siteUrl if blog else '' }}" placeholder="https://example.com">
35
- <span class="blogroll-field-hint">{{ __("blogroll.blogs.form.siteUrlHint") }}</span>
33
+ <label class="label" for="siteUrl">{{ __("blogroll.blogs.form.siteUrl") }}</label>
34
+ <input class="input" type="url" id="siteUrl" name="siteUrl" value="{{ blog.siteUrl if blog else '' }}" placeholder="https://example.com">
35
+ <span class="hint">{{ __("blogroll.blogs.form.siteUrlHint") }}</span>
36
36
  </div>
37
37
 
38
38
  <div class="blogroll-field">
39
- <label for="category">{{ __("blogroll.blogs.form.category") }}</label>
40
- <input type="text" id="category" name="category" value="{{ blog.category if blog else '' }}" placeholder="Technology">
41
- <span class="blogroll-field-hint">{{ __("blogroll.blogs.form.categoryHint") }}</span>
39
+ <label class="label" for="category">{{ __("blogroll.blogs.form.category") }}</label>
40
+ <input class="input" type="text" id="category" name="category" value="{{ blog.category if blog else '' }}" placeholder="Technology">
41
+ <span class="hint">{{ __("blogroll.blogs.form.categoryHint") }}</span>
42
42
  </div>
43
43
 
44
44
  <div class="blogroll-field">
45
- <label for="tags">{{ __("blogroll.blogs.form.tags") }}</label>
46
- <input type="text" id="tags" name="tags" value="{{ blog.tags | join(', ') if blog and blog.tags else '' }}" placeholder="indie, personal, tech">
47
- <span class="blogroll-field-hint">{{ __("blogroll.blogs.form.tagsHint") }}</span>
45
+ <label class="label" for="tags">{{ __("blogroll.blogs.form.tags") }}</label>
46
+ <input class="input" type="text" id="tags" name="tags" value="{{ blog.tags | join(', ') if blog and blog.tags else '' }}" placeholder="indie, personal, tech">
47
+ <span class="hint">{{ __("blogroll.blogs.form.tagsHint") }}</span>
48
48
  </div>
49
49
 
50
50
  <div class="blogroll-field">
51
- <label for="notes">{{ __("blogroll.blogs.form.notes") }}</label>
52
- <textarea id="notes" name="notes" placeholder="{{ __('blogroll.blogs.form.notesPlaceholder') }}">{{ blog.notes if blog else '' }}</textarea>
53
- <span class="blogroll-field-hint">{{ __("blogroll.blogs.form.notesHint") }}</span>
51
+ <label class="label" for="notes">{{ __("blogroll.blogs.form.notes") }}</label>
52
+ <textarea class="textarea" id="notes" name="notes" placeholder="{{ __('blogroll.blogs.form.notesPlaceholder') }}">{{ blog.notes if blog else '' }}</textarea>
53
+ <span class="hint">{{ __("blogroll.blogs.form.notesHint") }}</span>
54
54
  </div>
55
55
 
56
56
  <div class="blogroll-field blogroll-field--inline">
@@ -3,13 +3,13 @@
3
3
  {% block blogroll %}
4
4
  <div class="blogroll-filters">
5
5
  <form method="get" action="{{ baseUrl }}/blogs" style="display: flex; gap: var(--space-s); flex-wrap: wrap; align-items: center;">
6
- <select name="category" class="blogroll-filter-select" onchange="this.form.submit()">
6
+ <select name="category" class="select" onchange="this.form.submit()">
7
7
  <option value="">{{ __("blogroll.blogs.allCategories") }}</option>
8
8
  {% for cat in categories %}
9
9
  <option value="{{ cat }}" {% if filterCategory == cat %}selected{% endif %}>{{ cat }}</option>
10
10
  {% endfor %}
11
11
  </select>
12
- <select name="status" class="blogroll-filter-select" onchange="this.form.submit()">
12
+ <select name="status" class="select" onchange="this.form.submit()">
13
13
  <option value="">{{ __("blogroll.blogs.allStatuses") }}</option>
14
14
  <option value="active" {% if filterStatus == 'active' %}selected{% endif %}>{{ __("blogroll.blogs.statusActive") }}</option>
15
15
  <option value="error" {% if filterStatus == 'error' %}selected{% endif %}>{{ __("blogroll.blogs.statusError") }}</option>
@@ -3,54 +3,54 @@
3
3
  {% block blogroll %}
4
4
  <form method="post" action="{% if isNew %}{{ baseUrl }}/sources{% else %}{{ baseUrl }}/sources/{{ source._id }}{% endif %}" class="blogroll-form">
5
5
  <div class="blogroll-field">
6
- <label for="name">{{ __("blogroll.sources.form.name") }}</label>
7
- <input type="text" id="name" name="name" value="{{ source.name if source else '' }}" required>
6
+ <label class="label" for="name">{{ __("blogroll.sources.form.name") }}</label>
7
+ <input class="input" type="text" id="name" name="name" value="{{ source.name if source else '' }}" required>
8
8
  </div>
9
9
 
10
10
  <div class="blogroll-field">
11
- <label for="type">{{ __("blogroll.sources.form.type") }}</label>
12
- <select id="type" name="type" required onchange="toggleTypeFields()">
11
+ <label class="label" for="type">{{ __("blogroll.sources.form.type") }}</label>
12
+ <select class="select" id="type" name="type" required onchange="toggleTypeFields()">
13
13
  <option value="opml_url" {% if source.type == 'opml_url' %}selected{% endif %}>OPML URL (auto-sync)</option>
14
14
  <option value="opml_file" {% if source.type == 'opml_file' %}selected{% endif %}>OPML File (one-time import)</option>
15
15
  {% if microsubAvailable %}
16
16
  <option value="microsub" {% if source.type == 'microsub' %}selected{% endif %}>Microsub Subscriptions</option>
17
17
  {% endif %}
18
18
  </select>
19
- <span class="blogroll-field-hint">{{ __("blogroll.sources.form.typeHint") }}</span>
19
+ <span class="hint">{{ __("blogroll.sources.form.typeHint") }}</span>
20
20
  </div>
21
21
 
22
22
  <div class="blogroll-field" id="urlField">
23
- <label for="url">{{ __("blogroll.sources.form.url") }}</label>
24
- <input type="url" id="url" name="url" value="{{ source.url if source else '' }}" placeholder="https://...">
25
- <span class="blogroll-field-hint">{{ __("blogroll.sources.form.urlHint") }}</span>
23
+ <label class="label" for="url">{{ __("blogroll.sources.form.url") }}</label>
24
+ <input class="input" type="url" id="url" name="url" value="{{ source.url if source else '' }}" placeholder="https://...">
25
+ <span class="hint">{{ __("blogroll.sources.form.urlHint") }}</span>
26
26
  </div>
27
27
 
28
28
  <div class="blogroll-field" id="opmlContentField" style="display: none;">
29
- <label for="opmlContent">{{ __("blogroll.sources.form.opmlContent") }}</label>
30
- <textarea id="opmlContent" name="opmlContent" placeholder="<?xml version=&quot;1.0&quot;?>...">{{ source.opmlContent if source else '' }}</textarea>
31
- <span class="blogroll-field-hint">{{ __("blogroll.sources.form.opmlContentHint") }}</span>
29
+ <label class="label" for="opmlContent">{{ __("blogroll.sources.form.opmlContent") }}</label>
30
+ <textarea class="textarea" id="opmlContent" name="opmlContent" placeholder="<?xml version=&quot;1.0&quot;?>...">{{ source.opmlContent if source else '' }}</textarea>
31
+ <span class="hint">{{ __("blogroll.sources.form.opmlContentHint") }}</span>
32
32
  </div>
33
33
 
34
34
  <div class="blogroll-field" id="microsubChannelField" style="display: none;">
35
- <label for="channelFilter">{{ __("blogroll.sources.form.microsubChannel") | default("Microsub Channel") }}</label>
36
- <select id="channelFilter" name="channelFilter">
35
+ <label class="label" for="channelFilter">{{ __("blogroll.sources.form.microsubChannel") | default("Microsub Channel") }}</label>
36
+ <select class="select" id="channelFilter" name="channelFilter">
37
37
  <option value="">All channels</option>
38
38
  {% for channel in microsubChannels %}
39
39
  <option value="{{ channel.uid }}" {% if source.channelFilter == channel.uid %}selected{% endif %}>{{ channel.name }}</option>
40
40
  {% endfor %}
41
41
  </select>
42
- <span class="blogroll-field-hint">{{ __("blogroll.sources.form.microsubChannelHint") | default("Sync feeds from a specific channel, or all channels") }}</span>
42
+ <span class="hint">{{ __("blogroll.sources.form.microsubChannelHint") | default("Sync feeds from a specific channel, or all channels") }}</span>
43
43
  </div>
44
44
 
45
45
  <div class="blogroll-field" id="categoryPrefixField" style="display: none;">
46
- <label for="categoryPrefix">{{ __("blogroll.sources.form.categoryPrefix") | default("Category Prefix") }}</label>
47
- <input type="text" id="categoryPrefix" name="categoryPrefix" value="{{ source.categoryPrefix if source else '' }}" placeholder="e.g., Microsub: ">
48
- <span class="blogroll-field-hint">{{ __("blogroll.sources.form.categoryPrefixHint") | default("Optional prefix for blog categories (e.g., 'Following: ')") }}</span>
46
+ <label class="label" for="categoryPrefix">{{ __("blogroll.sources.form.categoryPrefix") | default("Category Prefix") }}</label>
47
+ <input class="input" type="text" id="categoryPrefix" name="categoryPrefix" value="{{ source.categoryPrefix if source else '' }}" placeholder="e.g., Microsub: ">
48
+ <span class="hint">{{ __("blogroll.sources.form.categoryPrefixHint") | default("Optional prefix for blog categories (e.g., 'Following: ')") }}</span>
49
49
  </div>
50
50
 
51
51
  <div class="blogroll-field">
52
- <label for="syncInterval">{{ __("blogroll.sources.form.syncInterval") }}</label>
53
- <select id="syncInterval" name="syncInterval">
52
+ <label class="label" for="syncInterval">{{ __("blogroll.sources.form.syncInterval") }}</label>
53
+ <select class="select" id="syncInterval" name="syncInterval">
54
54
  <option value="30" {% if source.syncInterval == 30 %}selected{% endif %}>30 minutes</option>
55
55
  <option value="60" {% if not source or source.syncInterval == 60 %}selected{% endif %}>1 hour</option>
56
56
  <option value="180" {% if source.syncInterval == 180 %}selected{% endif %}>3 hours</option>