feat: add breadcrumb navigation across all ActivityPub UI pages

Document.njk pages (followers, following, activities, featured, tags,
profile, migrate) get parent breadcrumbs via the upstream heading
component. Reader pages (explore, notifications, compose, moderation,
tag timeline, post detail, remote profile, my profile) get a new
breadcrumb nav bar in ap-reader.njk layout.
This commit is contained in:
Ricardo
2026-02-27 12:10:31 +01:00
parent 4aff8895c0
commit 25513c7ea5
17 changed files with 65 additions and 0 deletions
+31
View File
@@ -4,6 +4,37 @@
* Uses Indiekit CSS custom properties for automatic dark mode support * Uses Indiekit CSS custom properties for automatic dark mode support
*/ */
/* ==========================================================================
Breadcrumb Navigation
========================================================================== */
.ap-breadcrumb {
display: flex;
align-items: center;
gap: var(--space-xs);
margin-bottom: var(--space-m);
font-size: var(--font-size-s);
color: var(--color-on-offset);
}
.ap-breadcrumb a {
color: var(--color-accent);
text-decoration: none;
}
.ap-breadcrumb a:hover {
text-decoration: underline;
}
.ap-breadcrumb__separator {
color: var(--color-on-offset);
}
.ap-breadcrumb__current {
color: var(--color-on-background);
font-weight: var(--font-weight-bold);
}
/* ========================================================================== /* ==========================================================================
Fediverse Lookup Fediverse Lookup
========================================================================== */ ========================================================================== */
+2
View File
@@ -12,6 +12,7 @@ export function activitiesController(mountPath) {
if (!collection) { if (!collection) {
return response.render("activitypub-activities", { return response.render("activitypub-activities", {
title: response.locals.__("activitypub.activities"), title: response.locals.__("activitypub.activities"),
parent: { href: mountPath, text: response.locals.__("activitypub.title") },
activities: [], activities: [],
mountPath, mountPath,
}); });
@@ -32,6 +33,7 @@ export function activitiesController(mountPath) {
response.render("activitypub-activities", { response.render("activitypub-activities", {
title: response.locals.__("activitypub.activities"), title: response.locals.__("activitypub.activities"),
parent: { href: mountPath, text: response.locals.__("activitypub.title") },
activities, activities,
mountPath, mountPath,
cursor, cursor,
+1
View File
@@ -141,6 +141,7 @@ export function composeController(mountPath, plugin) {
response.render("activitypub-compose", { response.render("activitypub-compose", {
title: response.locals.__("activitypub.compose.title"), title: response.locals.__("activitypub.compose.title"),
readerParent: { href: `${mountPath}/admin/reader`, text: response.locals.__("activitypub.reader.title") },
replyTo, replyTo,
replyContext, replyContext,
syndicationTargets, syndicationTargets,
+5
View File
@@ -141,10 +141,13 @@ export function exploreController(mountPath) {
const csrfToken = getToken(request.session); const csrfToken = getToken(request.session);
const deckCount = decks.length; const deckCount = decks.length;
const readerParent = { href: `${mountPath}/admin/reader`, text: response.locals.__("activitypub.reader.title") };
// No instance specified — render clean initial page (no error) // No instance specified — render clean initial page (no error)
if (!rawInstance.trim()) { if (!rawInstance.trim()) {
return response.render("activitypub-explore", { return response.render("activitypub-explore", {
title: response.locals.__("activitypub.reader.explore.title"), title: response.locals.__("activitypub.reader.explore.title"),
readerParent,
instance: "", instance: "",
scope, scope,
items: [], items: [],
@@ -163,6 +166,7 @@ export function exploreController(mountPath) {
if (!instance) { if (!instance) {
return response.render("activitypub-explore", { return response.render("activitypub-explore", {
title: response.locals.__("activitypub.reader.explore.title"), title: response.locals.__("activitypub.reader.explore.title"),
readerParent,
instance: rawInstance, instance: rawInstance,
scope, scope,
items: [], items: [],
@@ -228,6 +232,7 @@ export function exploreController(mountPath) {
response.render("activitypub-explore", { response.render("activitypub-explore", {
title: response.locals.__("activitypub.reader.explore.title"), title: response.locals.__("activitypub.reader.explore.title"),
readerParent,
instance, instance,
scope, scope,
items, items,
+1
View File
@@ -15,6 +15,7 @@ export function featuredTagsGetController(mountPath) {
response.render("activitypub-featured-tags", { response.render("activitypub-featured-tags", {
title: title:
response.locals.__("activitypub.featuredTags") || "Featured Tags", response.locals.__("activitypub.featuredTags") || "Featured Tags",
parent: { href: mountPath, text: response.locals.__("activitypub.title") },
tags, tags,
mountPath, mountPath,
}); });
+1
View File
@@ -57,6 +57,7 @@ export function featuredGetController(mountPath) {
response.render("activitypub-featured", { response.render("activitypub-featured", {
title: response.locals.__("activitypub.featured") || "Pinned Posts", title: response.locals.__("activitypub.featured") || "Pinned Posts",
parent: { href: mountPath, text: response.locals.__("activitypub.title") },
pinned, pinned,
availablePosts, availablePosts,
maxPins: MAX_PINS, maxPins: MAX_PINS,
+2
View File
@@ -12,6 +12,7 @@ export function followersController(mountPath) {
if (!collection) { if (!collection) {
return response.render("activitypub-followers", { return response.render("activitypub-followers", {
title: response.locals.__("activitypub.followers"), title: response.locals.__("activitypub.followers"),
parent: { href: mountPath, text: response.locals.__("activitypub.title") },
followers: [], followers: [],
followerCount: 0, followerCount: 0,
mountPath, mountPath,
@@ -33,6 +34,7 @@ export function followersController(mountPath) {
response.render("activitypub-followers", { response.render("activitypub-followers", {
title: `${totalCount} ${response.locals.__("activitypub.followers")}`, title: `${totalCount} ${response.locals.__("activitypub.followers")}`,
parent: { href: mountPath, text: response.locals.__("activitypub.title") },
followers, followers,
followerCount: totalCount, followerCount: totalCount,
mountPath, mountPath,
+2
View File
@@ -12,6 +12,7 @@ export function followingController(mountPath) {
if (!collection) { if (!collection) {
return response.render("activitypub-following", { return response.render("activitypub-following", {
title: response.locals.__("activitypub.following"), title: response.locals.__("activitypub.following"),
parent: { href: mountPath, text: response.locals.__("activitypub.title") },
following: [], following: [],
followingCount: 0, followingCount: 0,
mountPath, mountPath,
@@ -33,6 +34,7 @@ export function followingController(mountPath) {
response.render("activitypub-following", { response.render("activitypub-following", {
title: `${totalCount} ${response.locals.__("activitypub.following")}`, title: `${totalCount} ${response.locals.__("activitypub.following")}`,
parent: { href: mountPath, text: response.locals.__("activitypub.title") },
following, following,
followingCount: totalCount, followingCount: totalCount,
mountPath, mountPath,
+2
View File
@@ -24,6 +24,7 @@ export function migrateGetController(mountPath, pluginOptions) {
response.render("activitypub-migrate", { response.render("activitypub-migrate", {
title: response.locals.__("activitypub.migrate.title"), title: response.locals.__("activitypub.migrate.title"),
parent: { href: mountPath, text: response.locals.__("activitypub.title") },
mountPath, mountPath,
currentAlias, currentAlias,
result: null, result: null,
@@ -61,6 +62,7 @@ export function migratePostController(mountPath, pluginOptions) {
response.render("activitypub-migrate", { response.render("activitypub-migrate", {
title: response.locals.__("activitypub.migrate.title"), title: response.locals.__("activitypub.migrate.title"),
parent: { href: mountPath, text: response.locals.__("activitypub.title") },
mountPath, mountPath,
currentAlias, currentAlias,
result, result,
+1
View File
@@ -301,6 +301,7 @@ export function moderationController(mountPath) {
response.render("activitypub-moderation", { response.render("activitypub-moderation", {
title: response.locals.__("activitypub.moderation.title"), title: response.locals.__("activitypub.moderation.title"),
readerParent: { href: `${mountPath}/admin/reader`, text: response.locals.__("activitypub.reader.title") },
muted, muted,
blocked, blocked,
mutedActors, mutedActors,
+1
View File
@@ -219,6 +219,7 @@ export function myProfileController(plugin) {
response.render("activitypub-my-profile", { response.render("activitypub-my-profile", {
title: response.locals.__("activitypub.myProfile.title"), title: response.locals.__("activitypub.myProfile.title"),
readerParent: { href: mountPath, text: response.locals.__("activitypub.title") },
profile: profile || {}, profile: profile || {},
handle, handle,
domain, domain,
+2
View File
@@ -197,6 +197,7 @@ export function postDetailController(mountPath, plugin) {
// Truly not found (no local item either) // Truly not found (no local item either)
return response.status(404).render("activitypub-post-detail", { return response.status(404).render("activitypub-post-detail", {
title: response.locals.__("activitypub.reader.post.title"), title: response.locals.__("activitypub.reader.post.title"),
readerParent: { href: `${mountPath}/admin/reader`, text: response.locals.__("activitypub.reader.title") },
notFound: true, objectUrl, mountPath, notFound: true, objectUrl, mountPath,
item: null, interactionMap: {}, csrfToken: null, item: null, interactionMap: {}, csrfToken: null,
parentPosts: [], replyPosts: [], parentPosts: [], replyPosts: [],
@@ -318,6 +319,7 @@ export function postDetailController(mountPath, plugin) {
response.render("activitypub-post-detail", { response.render("activitypub-post-detail", {
title: response.locals.__("activitypub.reader.post.title"), title: response.locals.__("activitypub.reader.post.title"),
readerParent: { href: `${mountPath}/admin/reader`, text: response.locals.__("activitypub.reader.title") },
item: timelineItem, item: timelineItem,
interactionMap, interactionMap,
csrfToken, csrfToken,
+2
View File
@@ -18,6 +18,7 @@ export function profileGetController(mountPath) {
response.render("activitypub-profile", { response.render("activitypub-profile", {
title: response.locals.__("activitypub.profile.title"), title: response.locals.__("activitypub.profile.title"),
parent: { href: mountPath, text: response.locals.__("activitypub.title") },
mountPath, mountPath,
profile, profile,
actorTypes: ACTOR_TYPES, actorTypes: ACTOR_TYPES,
@@ -96,6 +97,7 @@ export function profilePostController(mountPath, plugin) {
response.render("activitypub-profile", { response.render("activitypub-profile", {
title: response.locals.__("activitypub.profile.title"), title: response.locals.__("activitypub.profile.title"),
parent: { href: mountPath, text: response.locals.__("activitypub.title") },
mountPath, mountPath,
profile, profile,
actorTypes: ACTOR_TYPES, actorTypes: ACTOR_TYPES,
+1
View File
@@ -126,6 +126,7 @@ export function remoteProfileController(mountPath, plugin) {
response.render("activitypub-remote-profile", { response.render("activitypub-remote-profile", {
title: name, title: name,
readerParent: { href: `${mountPath}/admin/reader`, text: response.locals.__("activitypub.reader.title") },
actorUrl, actorUrl,
name, name,
actorHandle, actorHandle,
+2
View File
@@ -203,6 +203,7 @@ export function readerController(mountPath) {
response.render("activitypub-reader", { response.render("activitypub-reader", {
title: response.locals.__("activitypub.reader.title"), title: response.locals.__("activitypub.reader.title"),
readerParent: { href: mountPath, text: response.locals.__("activitypub.title") },
items, items,
tab, tab,
before: result.before, before: result.before,
@@ -253,6 +254,7 @@ export function notificationsController(mountPath) {
response.render("activitypub-notifications", { response.render("activitypub-notifications", {
title: response.locals.__("activitypub.notifications.title"), title: response.locals.__("activitypub.notifications.title"),
readerParent: { href: `${mountPath}/admin/reader`, text: response.locals.__("activitypub.reader.title") },
items: result.items, items: result.items,
before: result.before, before: result.before,
tab, tab,
+1
View File
@@ -131,6 +131,7 @@ export function tagTimelineController(mountPath) {
response.render("activitypub-tag-timeline", { response.render("activitypub-tag-timeline", {
title: `#${tag}`, title: `#${tag}`,
readerParent: { href: `${mountPath}/admin/reader`, text: response.locals.__("activitypub.reader.title") },
tag, tag,
items, items,
before: result.before, before: result.before,
+8
View File
@@ -18,6 +18,14 @@
{# AP link interception for internal navigation #} {# AP link interception for internal navigation #}
<script defer src="/assets/@rmdes-indiekit-endpoint-activitypub/reader-links.js"></script> <script defer src="/assets/@rmdes-indiekit-endpoint-activitypub/reader-links.js"></script>
{% if readerParent %}
<nav class="ap-breadcrumb" aria-label="Breadcrumb">
<a href="{{ readerParent.href }}">{{ readerParent.text }}</a>
<span class="ap-breadcrumb__separator" aria-hidden="true">/</span>
<span class="ap-breadcrumb__current" aria-current="page">{{ title }}</span>
</nav>
{% endif %}
{% block readercontent %} {% block readercontent %}
{% endblock %} {% endblock %}
{% endblock %} {% endblock %}