@rmdes/indiekit-endpoint-microsub 1.0.6 → 1.0.8
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/controllers/reader.js +33 -7
- package/lib/feeds/normalizer.js +35 -5
- package/package.json +1 -1
- package/views/compose.njk +4 -4
- package/views/partials/item-card.njk +15 -8
|
@@ -303,6 +303,18 @@ export async function item(request, response) {
|
|
|
303
303
|
});
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Ensure value is a string URL
|
|
308
|
+
* @param {string|object|undefined} value - Value to check
|
|
309
|
+
* @returns {string|undefined} String value or undefined
|
|
310
|
+
*/
|
|
311
|
+
function ensureString(value) {
|
|
312
|
+
if (!value) return;
|
|
313
|
+
if (typeof value === "string") return value;
|
|
314
|
+
if (typeof value === "object" && value.url) return value.url;
|
|
315
|
+
return String(value);
|
|
316
|
+
}
|
|
317
|
+
|
|
306
318
|
/**
|
|
307
319
|
* Compose response form
|
|
308
320
|
* @param {object} request - Express request
|
|
@@ -324,10 +336,10 @@ export async function compose(request, response) {
|
|
|
324
336
|
|
|
325
337
|
response.render("compose", {
|
|
326
338
|
title: request.__("microsub.compose.title"),
|
|
327
|
-
replyTo: replyTo || reply,
|
|
328
|
-
likeOf: likeOf || like,
|
|
329
|
-
repostOf: repostOf || repost,
|
|
330
|
-
bookmarkOf: bookmarkOf || bookmark,
|
|
339
|
+
replyTo: ensureString(replyTo || reply),
|
|
340
|
+
likeOf: ensureString(likeOf || like),
|
|
341
|
+
repostOf: ensureString(repostOf || repost),
|
|
342
|
+
bookmarkOf: ensureString(bookmarkOf || bookmark),
|
|
331
343
|
baseUrl: request.baseUrl,
|
|
332
344
|
});
|
|
333
345
|
}
|
|
@@ -364,7 +376,7 @@ export async function submitCompose(request, response) {
|
|
|
364
376
|
if (!micropubEndpoint) {
|
|
365
377
|
return response.status(500).render("error", {
|
|
366
378
|
title: "Error",
|
|
367
|
-
|
|
379
|
+
content: "Micropub endpoint not configured",
|
|
368
380
|
});
|
|
369
381
|
}
|
|
370
382
|
|
|
@@ -438,20 +450,34 @@ export async function submitCompose(request, response) {
|
|
|
438
450
|
|
|
439
451
|
// Handle error
|
|
440
452
|
const errorBody = await micropubResponse.text();
|
|
453
|
+
const statusText = micropubResponse.statusText || "Unknown error";
|
|
441
454
|
console.error(
|
|
442
455
|
`[Microsub] Micropub error: ${micropubResponse.status} ${errorBody}`,
|
|
443
456
|
);
|
|
444
457
|
|
|
458
|
+
// Parse error message from response body if JSON
|
|
459
|
+
let errorMessage = `Micropub error: ${statusText}`;
|
|
460
|
+
try {
|
|
461
|
+
const errorJson = JSON.parse(errorBody);
|
|
462
|
+
if (errorJson.error_description) {
|
|
463
|
+
errorMessage = String(errorJson.error_description);
|
|
464
|
+
} else if (errorJson.error) {
|
|
465
|
+
errorMessage = String(errorJson.error);
|
|
466
|
+
}
|
|
467
|
+
} catch {
|
|
468
|
+
// Not JSON, use status text
|
|
469
|
+
}
|
|
470
|
+
|
|
445
471
|
return response.status(micropubResponse.status).render("error", {
|
|
446
472
|
title: "Error",
|
|
447
|
-
|
|
473
|
+
content: errorMessage,
|
|
448
474
|
});
|
|
449
475
|
} catch (error) {
|
|
450
476
|
console.error(`[Microsub] Micropub request failed: ${error.message}`);
|
|
451
477
|
|
|
452
478
|
return response.status(500).render("error", {
|
|
453
479
|
title: "Error",
|
|
454
|
-
|
|
480
|
+
content: `Failed to create post: ${error.message}`,
|
|
455
481
|
});
|
|
456
482
|
}
|
|
457
483
|
}
|
package/lib/feeds/normalizer.js
CHANGED
|
@@ -536,18 +536,18 @@ export function normalizeHfeedItem(entry, feedUrl) {
|
|
|
536
536
|
);
|
|
537
537
|
}
|
|
538
538
|
|
|
539
|
-
// Interaction types
|
|
539
|
+
// Interaction types - normalize to string URLs
|
|
540
540
|
if (properties["like-of"]) {
|
|
541
|
-
normalized["like-of"] = properties["like-of"];
|
|
541
|
+
normalized["like-of"] = normalizeUrlArray(properties["like-of"]);
|
|
542
542
|
}
|
|
543
543
|
if (properties["repost-of"]) {
|
|
544
|
-
normalized["repost-of"] = properties["repost-of"];
|
|
544
|
+
normalized["repost-of"] = normalizeUrlArray(properties["repost-of"]);
|
|
545
545
|
}
|
|
546
546
|
if (properties["bookmark-of"]) {
|
|
547
|
-
normalized["bookmark-of"] = properties["bookmark-of"];
|
|
547
|
+
normalized["bookmark-of"] = normalizeUrlArray(properties["bookmark-of"]);
|
|
548
548
|
}
|
|
549
549
|
if (properties["in-reply-to"]) {
|
|
550
|
-
normalized["in-reply-to"] = properties["in-reply-to"];
|
|
550
|
+
normalized["in-reply-to"] = normalizeUrlArray(properties["in-reply-to"]);
|
|
551
551
|
}
|
|
552
552
|
|
|
553
553
|
// RSVP
|
|
@@ -617,6 +617,36 @@ function extractPhotoUrl(photo) {
|
|
|
617
617
|
return;
|
|
618
618
|
}
|
|
619
619
|
|
|
620
|
+
/**
|
|
621
|
+
* Extract URL string from a value that may be string or object
|
|
622
|
+
* @param {object|string} value - URL string or object with url/value property
|
|
623
|
+
* @returns {string|undefined} URL string
|
|
624
|
+
*/
|
|
625
|
+
function extractUrl(value) {
|
|
626
|
+
if (!value) {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (typeof value === "string") {
|
|
630
|
+
return value;
|
|
631
|
+
}
|
|
632
|
+
if (typeof value === "object") {
|
|
633
|
+
return value.value || value.url || value.href;
|
|
634
|
+
}
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Normalize an array of URLs that may contain strings or objects
|
|
640
|
+
* @param {Array} urls - Array of URL strings or objects
|
|
641
|
+
* @returns {Array<string>} Array of URL strings
|
|
642
|
+
*/
|
|
643
|
+
function normalizeUrlArray(urls) {
|
|
644
|
+
if (!urls || !Array.isArray(urls)) {
|
|
645
|
+
return [];
|
|
646
|
+
}
|
|
647
|
+
return urls.map((u) => extractUrl(u)).filter(Boolean);
|
|
648
|
+
}
|
|
649
|
+
|
|
620
650
|
/**
|
|
621
651
|
* Normalize h-card author
|
|
622
652
|
* @param {object|string} hcard - h-card or author name string
|
package/package.json
CHANGED
package/views/compose.njk
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
<h2>{{ __("microsub.compose.title") }}</h2>
|
|
10
10
|
|
|
11
|
-
{% if replyTo %}
|
|
11
|
+
{% if replyTo and replyTo is string %}
|
|
12
12
|
<div class="compose__context">
|
|
13
13
|
{{ icon("reply") }} {{ __("microsub.compose.replyTo") }}:
|
|
14
14
|
<a href="{{ replyTo }}" target="_blank" rel="noopener">
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
</div>
|
|
18
18
|
{% endif %}
|
|
19
19
|
|
|
20
|
-
{% if likeOf %}
|
|
20
|
+
{% if likeOf and likeOf is string %}
|
|
21
21
|
<div class="compose__context">
|
|
22
22
|
{{ icon("like") }} {{ __("microsub.compose.likeOf") }}:
|
|
23
23
|
<a href="{{ likeOf }}" target="_blank" rel="noopener">
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
</div>
|
|
27
27
|
{% endif %}
|
|
28
28
|
|
|
29
|
-
{% if repostOf %}
|
|
29
|
+
{% if repostOf and repostOf is string %}
|
|
30
30
|
<div class="compose__context">
|
|
31
31
|
{{ icon("repost") }} {{ __("microsub.compose.repostOf") }}:
|
|
32
32
|
<a href="{{ repostOf }}" target="_blank" rel="noopener">
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
</div>
|
|
36
36
|
{% endif %}
|
|
37
37
|
|
|
38
|
-
{% if bookmarkOf %}
|
|
38
|
+
{% if bookmarkOf and bookmarkOf is string %}
|
|
39
39
|
<div class="compose__context">
|
|
40
40
|
{{ icon("bookmark") }} {{ __("microsub.compose.bookmarkOf") }}:
|
|
41
41
|
<a href="{{ bookmarkOf }}" target="_blank" rel="noopener">
|
|
@@ -7,36 +7,43 @@
|
|
|
7
7
|
data-is-read="{{ item._is_read | default(false) }}">
|
|
8
8
|
|
|
9
9
|
{# Context bar for interactions (Aperture pattern) #}
|
|
10
|
+
{# Helper to extract URL from value that may be string or object #}
|
|
11
|
+
{% macro getUrl(val) %}{{ val.url or val.value or val if val is string else val }}{% endmacro %}
|
|
12
|
+
|
|
10
13
|
{% if item["like-of"] and item["like-of"].length > 0 %}
|
|
14
|
+
{% set contextUrl = item['like-of'][0].url or item['like-of'][0].value or item['like-of'][0] %}
|
|
11
15
|
<div class="item-card__context">
|
|
12
16
|
{{ icon("like") }}
|
|
13
17
|
<span>Liked</span>
|
|
14
|
-
<a href="{{
|
|
15
|
-
{{
|
|
18
|
+
<a href="{{ contextUrl }}" target="_blank" rel="noopener">
|
|
19
|
+
{{ contextUrl | replace("https://", "") | replace("http://", "") | truncate(50) }}
|
|
16
20
|
</a>
|
|
17
21
|
</div>
|
|
18
22
|
{% elif item["repost-of"] and item["repost-of"].length > 0 %}
|
|
23
|
+
{% set contextUrl = item['repost-of'][0].url or item['repost-of'][0].value or item['repost-of'][0] %}
|
|
19
24
|
<div class="item-card__context">
|
|
20
25
|
{{ icon("repost") }}
|
|
21
26
|
<span>Reposted</span>
|
|
22
|
-
<a href="{{
|
|
23
|
-
{{
|
|
27
|
+
<a href="{{ contextUrl }}" target="_blank" rel="noopener">
|
|
28
|
+
{{ contextUrl | replace("https://", "") | replace("http://", "") | truncate(50) }}
|
|
24
29
|
</a>
|
|
25
30
|
</div>
|
|
26
31
|
{% elif item["in-reply-to"] and item["in-reply-to"].length > 0 %}
|
|
32
|
+
{% set contextUrl = item['in-reply-to'][0].url or item['in-reply-to'][0].value or item['in-reply-to'][0] %}
|
|
27
33
|
<div class="item-card__context">
|
|
28
34
|
{{ icon("reply") }}
|
|
29
35
|
<span>Reply to</span>
|
|
30
|
-
<a href="{{
|
|
31
|
-
{{
|
|
36
|
+
<a href="{{ contextUrl }}" target="_blank" rel="noopener">
|
|
37
|
+
{{ contextUrl | replace("https://", "") | replace("http://", "") | truncate(50) }}
|
|
32
38
|
</a>
|
|
33
39
|
</div>
|
|
34
40
|
{% elif item["bookmark-of"] and item["bookmark-of"].length > 0 %}
|
|
41
|
+
{% set contextUrl = item['bookmark-of'][0].url or item['bookmark-of'][0].value or item['bookmark-of'][0] %}
|
|
35
42
|
<div class="item-card__context">
|
|
36
43
|
{{ icon("bookmark") }}
|
|
37
44
|
<span>Bookmarked</span>
|
|
38
|
-
<a href="{{
|
|
39
|
-
{{
|
|
45
|
+
<a href="{{ contextUrl }}" target="_blank" rel="noopener">
|
|
46
|
+
{{ contextUrl | replace("https://", "") | replace("http://", "") | truncate(50) }}
|
|
40
47
|
</a>
|
|
41
48
|
</div>
|
|
42
49
|
{% endif %}
|