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;
}
.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 {
display: flex;
flex-direction: column;
+19 -2
View File
@@ -95,11 +95,28 @@ export default class ActivityPubEndpoint {
}
get navigationItems() {
return {
return [
{
href: this.options.mountPath,
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,
actorPhoto: followerInfo.photo,
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(),
});
})
@@ -258,7 +258,7 @@ export function registerInboxListeners(inboxChain, options) {
actorHandle: actorInfo.handle,
targetUrl: objectId,
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(),
});
})
@@ -306,7 +306,7 @@ export function registerInboxListeners(inboxChain, options) {
actorHandle: actorInfo.handle,
targetUrl: objectId,
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(),
});
@@ -328,7 +328,7 @@ export function registerInboxListeners(inboxChain, options) {
// Extract and store with boost metadata
const timelineItem = await extractObjectData(object, {
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);
@@ -404,7 +404,7 @@ export function registerInboxListeners(inboxChain, options) {
text: contentText,
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(),
});
}
@@ -433,7 +433,7 @@ export function registerInboxListeners(inboxChain, options) {
text: contentText,
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(),
});
+1 -1
View File
@@ -124,7 +124,7 @@ export async function extractObjectData(object, options = {}) {
// Published date — store as ISO string per Indiekit convention
const published = object.published
? new Date(object.published).toISOString()
? String(object.published)
: new Date().toISOString();
// Extract author — use async getAttributedTo() for Fedify objects
+1 -1
View File
@@ -1,6 +1,6 @@
{
"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.",
"keywords": [
"indiekit",
+1 -1
View File
@@ -2,7 +2,7 @@
{% from "heading/macro.njk" import heading with context %}
{% block content %}
{% block readercontent %}
{{ heading({
text: title,
level: 1,
+1 -1
View File
@@ -3,7 +3,7 @@
{% from "heading/macro.njk" import heading with context %}
{% from "prose/macro.njk" import prose with context %}
{% block content %}
{% block readercontent %}
{{ heading({
text: title,
level: 1,
+1 -1
View File
@@ -3,7 +3,7 @@
{% from "heading/macro.njk" import heading with context %}
{% from "prose/macro.njk" import prose with context %}
{% block content %}
{% block readercontent %}
{{ heading({
text: __("activitypub.notifications.title"),
level: 1,
+1 -1
View File
@@ -3,7 +3,7 @@
{% from "heading/macro.njk" import heading with context %}
{% from "prose/macro.njk" import prose with context %}
{% block content %}
{% block readercontent %}
{{ heading({
text: __("activitypub.reader.title"),
level: 1,
+1 -1
View File
@@ -3,7 +3,7 @@
{% from "heading/macro.njk" import heading with context %}
{% from "prose/macro.njk" import prose with context %}
{% block content %}
{% block readercontent %}
{{ heading({
text: title,
level: 1,
+7 -4
View File
@@ -1,9 +1,12 @@
{% extends "document.njk" %}
{% block head %}
{# Alpine.js for client-side reactivity #}
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14/dist/cdn.min.js"></script>
{% block content %}
{# Alpine.js for client-side reactivity (CW toggles, interaction buttons) #}
<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">
{% block readercontent %}
{% endblock %}
{% endblock %}
+15 -3
View File
@@ -4,7 +4,7 @@
{# Boost header if this is a boosted post #}
{% if item.type == "boost" and item.boostedBy %}
<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>
{% endif %}
@@ -17,16 +17,28 @@
{# Author header #}
<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-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>
{% if item.author.handle %}
<div class="ap-card__author-handle">{{ item.author.handle }}</div>
{% endif %}
</div>
{% if item.published %}
<time datetime="{{ item.published }}" class="ap-card__timestamp">
{{ item.published | date("PPp") }}
</time>
{% endif %}
</header>
{# Post title (articles only) #}