@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.
- package/lib/feeds/normalizer.js +22 -1
- package/lib/polling/processor.js +5 -0
- package/lib/storage/items.js +75 -10
- package/package.json +1 -1
- package/views/404.njk +17 -0
- package/views/item.njk +9 -9
- package/views/partials/item-card.njk +1 -1
package/lib/feeds/normalizer.js
CHANGED
|
@@ -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
|
|
package/lib/polling/processor.js
CHANGED
|
@@ -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,
|
package/lib/storage/items.js
CHANGED
|
@@ -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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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|
|
|
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
|
-
|
|
175
|
-
|
|
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
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
|
|
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
|
|
56
|
-
<p>{{ icon("reply") }} {{ __("Reply to") }}: <a href="{{ item
|
|
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
|
|
59
|
-
<p>{{ icon("like") }} {{ __("Liked") }}: <a href="{{ item
|
|
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
|
|
62
|
-
<p>{{ icon("repost") }} {{ __("Reposted") }}: <a href="{{ item
|
|
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
|
|
65
|
-
<p>{{ icon("bookmark") }} {{ __("Bookmarked") }}: <a href="{{ item
|
|
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.
|
|
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 %}
|