fix: reader UI — navigation, Alpine.js loading, avatar fallback, Temporal dates

- Return multiple navigation items (ActivityPub, Reader, Notifications, Moderation)
  so all AP sub-pages are accessible from the sidebar
- Fix Alpine.js not loading: `{% block head %}` was silently discarded because
  the parent template chain has no such block — moved script/css into content block
- Pin Alpine.js to exact version 3.14.9 to prevent CDN resolution issues
- Add fallback avatar (first letter) when author photo is missing
- Guard empty author URLs to prevent broken links
- Fix Temporal.Instant TypeError: use String() instead of new Date() for
  Fedify published timestamps in inbox-listeners and timeline-store
- Link author names to remote profile view instead of raw AP URLs
- Bump to 1.1.3
This commit is contained in:
Ricardo
2026-02-21 13:31:52 +01:00
parent 55e9311c4a
commit 3ad86ffb39
12 changed files with 71 additions and 29 deletions
+10
View File
@@ -123,6 +123,16 @@
width: 40px; width: 40px;
} }
.ap-card__avatar--default {
align-items: center;
background: var(--color-offset);
color: var(--color-text-muted);
display: inline-flex;
font-size: 1.1em;
font-weight: 600;
justify-content: center;
}
.ap-card__author-info { .ap-card__author-info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+22 -5
View File
@@ -95,11 +95,28 @@ export default class ActivityPubEndpoint {
} }
get navigationItems() { get navigationItems() {
return { return [
href: `${this.options.mountPath}/admin/reader`, {
text: "activitypub.reader.title", href: this.options.mountPath,
requiresDatabase: true, text: "activitypub.title",
}; requiresDatabase: true,
},
{
href: `${this.options.mountPath}/admin/reader`,
text: "activitypub.reader.title",
requiresDatabase: true,
},
{
href: `${this.options.mountPath}/admin/reader/notifications`,
text: "activitypub.notifications.title",
requiresDatabase: true,
},
{
href: `${this.options.mountPath}/admin/reader/moderation`,
text: "activitypub.moderation.title",
requiresDatabase: true,
},
];
} }
/** /**
+6 -6
View File
@@ -96,7 +96,7 @@ export function registerInboxListeners(inboxChain, options) {
actorName: followerInfo.name, actorName: followerInfo.name,
actorPhoto: followerInfo.photo, actorPhoto: followerInfo.photo,
actorHandle: followerInfo.handle, actorHandle: followerInfo.handle,
published: follow.published ? new Date(follow.published) : new Date(), published: follow.published ? String(follow.published) : new Date().toISOString(),
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
}); });
}) })
@@ -258,7 +258,7 @@ export function registerInboxListeners(inboxChain, options) {
actorHandle: actorInfo.handle, actorHandle: actorInfo.handle,
targetUrl: objectId, targetUrl: objectId,
targetName: "", // Could fetch post title, but not critical targetName: "", // Could fetch post title, but not critical
published: like.published ? new Date(like.published) : new Date(), published: like.published ? String(like.published) : new Date().toISOString(),
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
}); });
}) })
@@ -306,7 +306,7 @@ export function registerInboxListeners(inboxChain, options) {
actorHandle: actorInfo.handle, actorHandle: actorInfo.handle,
targetUrl: objectId, targetUrl: objectId,
targetName: "", // Could fetch post title, but not critical targetName: "", // Could fetch post title, but not critical
published: announce.published ? new Date(announce.published).toISOString() : new Date().toISOString(), published: announce.published ? String(announce.published) : new Date().toISOString(),
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
}); });
@@ -328,7 +328,7 @@ export function registerInboxListeners(inboxChain, options) {
// Extract and store with boost metadata // Extract and store with boost metadata
const timelineItem = await extractObjectData(object, { const timelineItem = await extractObjectData(object, {
boostedBy: boosterInfo, boostedBy: boosterInfo,
boostedAt: announce.published ? new Date(announce.published).toISOString() : new Date().toISOString(), boostedAt: announce.published ? String(announce.published) : new Date().toISOString(),
}); });
await addTimelineItem(collections, timelineItem); await addTimelineItem(collections, timelineItem);
@@ -404,7 +404,7 @@ export function registerInboxListeners(inboxChain, options) {
text: contentText, text: contentText,
html: contentHtml, html: contentHtml,
}, },
published: object.published ? new Date(object.published).toISOString() : new Date().toISOString(), published: object.published ? String(object.published) : new Date().toISOString(),
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
}); });
} }
@@ -433,7 +433,7 @@ export function registerInboxListeners(inboxChain, options) {
text: contentText, text: contentText,
html: mentionHtml, html: mentionHtml,
}, },
published: object.published ? new Date(object.published).toISOString() : new Date().toISOString(), published: object.published ? String(object.published) : new Date().toISOString(),
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
}); });
+1 -1
View File
@@ -124,7 +124,7 @@ export async function extractObjectData(object, options = {}) {
// Published date — store as ISO string per Indiekit convention // Published date — store as ISO string per Indiekit convention
const published = object.published const published = object.published
? new Date(object.published).toISOString() ? String(object.published)
: new Date().toISOString(); : new Date().toISOString();
// Extract author — use async getAttributedTo() for Fedify objects // Extract author — use async getAttributedTo() for Fedify objects
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@rmdes/indiekit-endpoint-activitypub", "name": "@rmdes/indiekit-endpoint-activitypub",
"version": "1.1.1", "version": "1.1.3",
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.", "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
"keywords": [ "keywords": [
"indiekit", "indiekit",
+1 -1
View File
@@ -2,7 +2,7 @@
{% from "heading/macro.njk" import heading with context %} {% from "heading/macro.njk" import heading with context %}
{% block content %} {% block readercontent %}
{{ heading({ {{ heading({
text: title, text: title,
level: 1, level: 1,
+1 -1
View File
@@ -3,7 +3,7 @@
{% from "heading/macro.njk" import heading with context %} {% from "heading/macro.njk" import heading with context %}
{% from "prose/macro.njk" import prose with context %} {% from "prose/macro.njk" import prose with context %}
{% block content %} {% block readercontent %}
{{ heading({ {{ heading({
text: title, text: title,
level: 1, level: 1,
+1 -1
View File
@@ -3,7 +3,7 @@
{% from "heading/macro.njk" import heading with context %} {% from "heading/macro.njk" import heading with context %}
{% from "prose/macro.njk" import prose with context %} {% from "prose/macro.njk" import prose with context %}
{% block content %} {% block readercontent %}
{{ heading({ {{ heading({
text: __("activitypub.notifications.title"), text: __("activitypub.notifications.title"),
level: 1, level: 1,
+1 -1
View File
@@ -3,7 +3,7 @@
{% from "heading/macro.njk" import heading with context %} {% from "heading/macro.njk" import heading with context %}
{% from "prose/macro.njk" import prose with context %} {% from "prose/macro.njk" import prose with context %}
{% block content %} {% block readercontent %}
{{ heading({ {{ heading({
text: __("activitypub.reader.title"), text: __("activitypub.reader.title"),
level: 1, level: 1,
+1 -1
View File
@@ -3,7 +3,7 @@
{% from "heading/macro.njk" import heading with context %} {% from "heading/macro.njk" import heading with context %}
{% from "prose/macro.njk" import prose with context %} {% from "prose/macro.njk" import prose with context %}
{% block content %} {% block readercontent %}
{{ heading({ {{ heading({
text: title, text: title,
level: 1, level: 1,
+7 -4
View File
@@ -1,9 +1,12 @@
{% extends "document.njk" %} {% extends "document.njk" %}
{% block head %} {% block content %}
{# Alpine.js for client-side reactivity #} {# Alpine.js for client-side reactivity (CW toggles, interaction buttons) #}
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14/dist/cdn.min.js"></script> <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.9/dist/cdn.min.js"></script>
{# Reader stylesheet #} {# Reader stylesheet — loaded in body is fine for modern browsers #}
<link rel="stylesheet" href="/assets/@rmdes-indiekit-endpoint-activitypub/reader.css"> <link rel="stylesheet" href="/assets/@rmdes-indiekit-endpoint-activitypub/reader.css">
{% block readercontent %}
{% endblock %}
{% endblock %} {% endblock %}
+19 -7
View File
@@ -4,7 +4,7 @@
{# Boost header if this is a boosted post #} {# Boost header if this is a boosted post #}
{% if item.type == "boost" and item.boostedBy %} {% if item.type == "boost" and item.boostedBy %}
<div class="ap-card__boost"> <div class="ap-card__boost">
🔁 <a href="{{ item.boostedBy.url }}">{{ item.boostedBy.name }}</a> {{ __("activitypub.reader.boosted") }} 🔁 {% if item.boostedBy.url %}<a href="{{ mountPath }}/admin/reader/profile?url={{ item.boostedBy.url | urlencode }}">{{ item.boostedBy.name or "Someone" }}</a>{% else %}{{ item.boostedBy.name or "Someone" }}{% endif %} {{ __("activitypub.reader.boosted") }}
</div> </div>
{% endif %} {% endif %}
@@ -17,16 +17,28 @@
{# Author header #} {# Author header #}
<header class="ap-card__author"> <header class="ap-card__author">
<img src="{{ item.author.photo }}" alt="{{ item.author.name }}" class="ap-card__avatar"> {% if item.author.photo %}
<img src="{{ item.author.photo }}" alt="{{ item.author.name }}" class="ap-card__avatar" loading="lazy">
{% else %}
<span class="ap-card__avatar ap-card__avatar--default" aria-hidden="true">{{ item.author.name[0] | upper if item.author.name else "?" }}</span>
{% endif %}
<div class="ap-card__author-info"> <div class="ap-card__author-info">
<div class="ap-card__author-name"> <div class="ap-card__author-name">
<a href="{{ item.author.url }}">{{ item.author.name }}</a> {% if item.author.url %}
<a href="{{ mountPath }}/admin/reader/profile?url={{ item.author.url | urlencode }}">{{ item.author.name or "Unknown" }}</a>
{% else %}
<span>{{ item.author.name or "Unknown" }}</span>
{% endif %}
</div> </div>
<div class="ap-card__author-handle">{{ item.author.handle }}</div> {% if item.author.handle %}
<div class="ap-card__author-handle">{{ item.author.handle }}</div>
{% endif %}
</div> </div>
<time datetime="{{ item.published }}" class="ap-card__timestamp"> {% if item.published %}
{{ item.published | date("PPp") }} <time datetime="{{ item.published }}" class="ap-card__timestamp">
</time> {{ item.published | date("PPp") }}
</time>
{% endif %}
</header> </header>
{# Post title (articles only) #} {# Post title (articles only) #}