@rmdes/indiekit-endpoint-microsub 1.0.1 → 1.0.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.
@@ -154,6 +154,7 @@ export function normalizeItem(item, feedUrl, feedType) {
154
154
  published: toISOStringSafe(item.pubdate),
155
155
  updated: toISOStringSafe(item.date),
156
156
  _source: {
157
+ url: feedUrl,
157
158
  feedUrl,
158
159
  feedType,
159
160
  originalId: item.guid,
@@ -310,6 +311,7 @@ export function normalizeJsonFeedItem(item, feedUrl) {
310
311
  ? new Date(item.date_modified).toISOString()
311
312
  : undefined,
312
313
  _source: {
314
+ url: feedUrl,
313
315
  feedUrl,
314
316
  feedType: "jsonfeed",
315
317
  originalId: item.id,
@@ -451,6 +453,7 @@ export function normalizeHfeedItem(entry, feedUrl) {
451
453
  uid,
452
454
  url,
453
455
  _source: {
456
+ url: feedUrl,
454
457
  feedUrl,
455
458
  feedType: "hfeed",
456
459
  originalId: getFirst(properties.uid),
@@ -596,6 +599,24 @@ export function normalizeHfeedMeta(hfeed, feedUrl) {
596
599
  return normalized;
597
600
  }
598
601
 
602
+ /**
603
+ * Extract URL string from a photo value
604
+ * @param {object|string} photo - Photo value (can be string URL or object with value/url)
605
+ * @returns {string|undefined} Photo URL string
606
+ */
607
+ function extractPhotoUrl(photo) {
608
+ if (!photo) {
609
+ return;
610
+ }
611
+ if (typeof photo === "string") {
612
+ return photo;
613
+ }
614
+ if (typeof photo === "object") {
615
+ return photo.value || photo.url || photo.src;
616
+ }
617
+ return;
618
+ }
619
+
599
620
  /**
600
621
  * Normalize h-card author
601
622
  * @param {object|string} hcard - h-card or author name string
@@ -616,7 +637,7 @@ function normalizeHcard(hcard) {
616
637
  type: "card",
617
638
  name: getFirst(properties.name),
618
639
  url: getFirst(properties.url),
619
- photo: getFirst(properties.photo),
640
+ photo: extractPhotoUrl(getFirst(properties.photo)),
620
641
  };
621
642
  }
622
643
 
@@ -73,6 +73,11 @@ export async function processFeed(application, feed) {
73
73
  continue;
74
74
  }
75
75
 
76
+ // Enrich item source with feed metadata
77
+ if (item._source) {
78
+ item._source.name = feed.title || parsed.name;
79
+ }
80
+
76
81
  // Store the item
77
82
  const stored = await addItem(application, {
78
83
  channelId: feed.channelId,
@@ -121,6 +121,50 @@ export async function getTimelineItems(application, channelId, options = {}) {
121
121
  };
122
122
  }
123
123
 
124
+ /**
125
+ * Extract URL string from a media value
126
+ * @param {object|string} media - Media value (can be string URL or object)
127
+ * @returns {string|undefined} URL string
128
+ */
129
+ function extractMediaUrl(media) {
130
+ if (!media) {
131
+ return;
132
+ }
133
+ if (typeof media === "string") {
134
+ return media;
135
+ }
136
+ if (typeof media === "object") {
137
+ return media.value || media.url || media.src;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Normalize media array to URL strings
143
+ * @param {Array} mediaArray - Array of media items
144
+ * @returns {Array} Array of URL strings
145
+ */
146
+ function normalizeMediaArray(mediaArray) {
147
+ if (!mediaArray || !Array.isArray(mediaArray)) {
148
+ return [];
149
+ }
150
+ return mediaArray.map((media) => extractMediaUrl(media)).filter(Boolean);
151
+ }
152
+
153
+ /**
154
+ * Normalize author object to ensure photo is a URL string
155
+ * @param {object} author - Author object
156
+ * @returns {object} Normalized author
157
+ */
158
+ function normalizeAuthor(author) {
159
+ if (!author) {
160
+ return;
161
+ }
162
+ return {
163
+ ...author,
164
+ photo: extractMediaUrl(author.photo),
165
+ };
166
+ }
167
+
124
168
  /**
125
169
  * Transform database item to jf2 format
126
170
  * @param {object} item - Database item
@@ -142,11 +186,17 @@ function transformToJf2(item, userId) {
142
186
  if (item.content) jf2.content = item.content;
143
187
  if (item.summary) jf2.summary = item.summary;
144
188
  if (item.updated) jf2.updated = item.updated.toISOString();
145
- if (item.author) jf2.author = item.author;
189
+ if (item.author) jf2.author = normalizeAuthor(item.author);
146
190
  if (item.category?.length > 0) jf2.category = item.category;
147
- if (item.photo?.length > 0) jf2.photo = item.photo;
148
- if (item.video?.length > 0) jf2.video = item.video;
149
- if (item.audio?.length > 0) jf2.audio = item.audio;
191
+
192
+ // Normalize media arrays to ensure they contain URL strings
193
+ const photos = normalizeMediaArray(item.photo);
194
+ const videos = normalizeMediaArray(item.video);
195
+ const audios = normalizeMediaArray(item.audio);
196
+
197
+ if (photos.length > 0) jf2.photo = photos;
198
+ if (videos.length > 0) jf2.video = videos;
199
+ if (audios.length > 0) jf2.audio = audios;
150
200
 
151
201
  // Interaction types
152
202
  if (item.likeOf?.length > 0) jf2["like-of"] = item.likeOf;
@@ -161,18 +211,33 @@ function transformToJf2(item, userId) {
161
211
  }
162
212
 
163
213
  /**
164
- * Get an item by ID
214
+ * Get an item by ID (MongoDB _id or uid)
165
215
  * @param {object} application - Indiekit application
166
- * @param {ObjectId|string} id - Item ObjectId
216
+ * @param {ObjectId|string} id - Item ObjectId or uid string
167
217
  * @param {string} [userId] - User ID for read state
168
- * @returns {Promise<object|null>} jf2 item or null
218
+ * @returns {Promise<object|undefined>} jf2 item or undefined
169
219
  */
170
220
  export async function getItemById(application, id, userId) {
171
221
  const collection = getCollection(application);
172
- const objectId = typeof id === "string" ? new ObjectId(id) : id;
173
222
 
174
- const item = await collection.findOne({ _id: objectId });
175
- if (!item) return;
223
+ let item;
224
+
225
+ // Try MongoDB ObjectId first
226
+ try {
227
+ const objectId = typeof id === "string" ? new ObjectId(id) : id;
228
+ item = await collection.findOne({ _id: objectId });
229
+ } catch {
230
+ // Invalid ObjectId format, will try uid lookup
231
+ }
232
+
233
+ // If not found by _id, try uid
234
+ if (!item) {
235
+ item = await collection.findOne({ uid: id });
236
+ }
237
+
238
+ if (!item) {
239
+ return;
240
+ }
176
241
 
177
242
  return transformToJf2(item, userId);
178
243
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-microsub",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the Microsub protocol.",
5
5
  "keywords": [
6
6
  "indiekit",
package/views/404.njk ADDED
@@ -0,0 +1,17 @@
1
+ {% extends "document.njk" %}
2
+
3
+ {% block main %}
4
+ <article class="main__container -!-container">
5
+ <header class="heading">
6
+ <h1 class="heading__title">
7
+ {{ __("microsub.error.notFound.title") | default("Not found") }}
8
+ </h1>
9
+ </header>
10
+ {{ prose({ text: __("microsub.error.notFound.description") | default("The item you're looking for could not be found.") }) }}
11
+ <p>
12
+ <a href="{{ baseUrl }}/channels" class="button button--secondary">
13
+ {{ __("microsub.reader.backToChannels") | default("Back to channels") }}
14
+ </a>
15
+ </p>
16
+ </article>
17
+ {% endblock %}
package/views/item.njk CHANGED
@@ -50,19 +50,19 @@
50
50
  </div>
51
51
  {% endif %}
52
52
 
53
- {% if item.inReplyTo or item.likeOf or item.repostOf or item.bookmarkOf %}
53
+ {% if item["in-reply-to"] or item["like-of"] or item["repost-of"] or item["bookmark-of"] %}
54
54
  <div class="item__context">
55
- {% if item.inReplyTo %}
56
- <p>{{ icon("reply") }} {{ __("Reply to") }}: <a href="{{ item.inReplyTo[0] }}">{{ item.inReplyTo[0] }}</a></p>
55
+ {% if item["in-reply-to"] %}
56
+ <p>{{ icon("reply") }} {{ __("Reply to") }}: <a href="{{ item["in-reply-to"][0] }}">{{ item["in-reply-to"][0] }}</a></p>
57
57
  {% endif %}
58
- {% if item.likeOf %}
59
- <p>{{ icon("like") }} {{ __("Liked") }}: <a href="{{ item.likeOf[0] }}">{{ item.likeOf[0] }}</a></p>
58
+ {% if item["like-of"] %}
59
+ <p>{{ icon("like") }} {{ __("Liked") }}: <a href="{{ item["like-of"][0] }}">{{ item["like-of"][0] }}</a></p>
60
60
  {% endif %}
61
- {% if item.repostOf %}
62
- <p>{{ icon("repost") }} {{ __("Reposted") }}: <a href="{{ item.repostOf[0] }}">{{ item.repostOf[0] }}</a></p>
61
+ {% if item["repost-of"] %}
62
+ <p>{{ icon("repost") }} {{ __("Reposted") }}: <a href="{{ item["repost-of"][0] }}">{{ item["repost-of"][0] }}</a></p>
63
63
  {% endif %}
64
- {% if item.bookmarkOf %}
65
- <p>{{ icon("bookmark") }} {{ __("Bookmarked") }}: <a href="{{ item.bookmarkOf[0] }}">{{ item.bookmarkOf[0] }}</a></p>
64
+ {% if item["bookmark-of"] %}
65
+ <p>{{ icon("bookmark") }} {{ __("Bookmarked") }}: <a href="{{ item["bookmark-of"][0] }}">{{ item["bookmark-of"][0] }}</a></p>
66
66
  {% endif %}
67
67
  </div>
68
68
  {% endif %}
@@ -1,6 +1,6 @@
1
1
  {# Item card for timeline display #}
2
2
  <article class="item-card{% if item._is_read %} item-card--read{% endif %}">
3
- <a href="{{ baseUrl }}/item/{{ item.uid }}" class="item-card__link">
3
+ <a href="{{ baseUrl }}/item/{{ item._id }}" class="item-card__link">
4
4
  {% if item.author %}
5
5
  <div class="item-card__author">
6
6
  {% if item.author.photo %}