From 25513c7ea57b963b4f9493260574f4988ad3803d Mon Sep 17 00:00:00 2001 From: Ricardo Date: Fri, 27 Feb 2026 12:10:31 +0100 Subject: [PATCH] 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. --- assets/reader.css | 31 +++++++++++++++++++++++++++++++ lib/controllers/activities.js | 2 ++ lib/controllers/compose.js | 1 + lib/controllers/explore.js | 5 +++++ lib/controllers/featured-tags.js | 1 + lib/controllers/featured.js | 1 + lib/controllers/followers.js | 2 ++ lib/controllers/following.js | 2 ++ lib/controllers/migrate.js | 2 ++ lib/controllers/moderation.js | 1 + lib/controllers/my-profile.js | 1 + lib/controllers/post-detail.js | 2 ++ lib/controllers/profile.js | 2 ++ lib/controllers/profile.remote.js | 1 + lib/controllers/reader.js | 2 ++ lib/controllers/tag-timeline.js | 1 + views/layouts/ap-reader.njk | 8 ++++++++ 17 files changed, 65 insertions(+) diff --git a/assets/reader.css b/assets/reader.css index b52ce3e..6ceb5e8 100644 --- a/assets/reader.css +++ b/assets/reader.css @@ -4,6 +4,37 @@ * 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 ========================================================================== */ diff --git a/lib/controllers/activities.js b/lib/controllers/activities.js index 7423b36..fcfe2d4 100644 --- a/lib/controllers/activities.js +++ b/lib/controllers/activities.js @@ -12,6 +12,7 @@ export function activitiesController(mountPath) { if (!collection) { return response.render("activitypub-activities", { title: response.locals.__("activitypub.activities"), + parent: { href: mountPath, text: response.locals.__("activitypub.title") }, activities: [], mountPath, }); @@ -32,6 +33,7 @@ export function activitiesController(mountPath) { response.render("activitypub-activities", { title: response.locals.__("activitypub.activities"), + parent: { href: mountPath, text: response.locals.__("activitypub.title") }, activities, mountPath, cursor, diff --git a/lib/controllers/compose.js b/lib/controllers/compose.js index 25a77fc..5f6d353 100644 --- a/lib/controllers/compose.js +++ b/lib/controllers/compose.js @@ -141,6 +141,7 @@ export function composeController(mountPath, plugin) { response.render("activitypub-compose", { title: response.locals.__("activitypub.compose.title"), + readerParent: { href: `${mountPath}/admin/reader`, text: response.locals.__("activitypub.reader.title") }, replyTo, replyContext, syndicationTargets, diff --git a/lib/controllers/explore.js b/lib/controllers/explore.js index f64ac15..6b424f7 100644 --- a/lib/controllers/explore.js +++ b/lib/controllers/explore.js @@ -141,10 +141,13 @@ export function exploreController(mountPath) { const csrfToken = getToken(request.session); 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) if (!rawInstance.trim()) { return response.render("activitypub-explore", { title: response.locals.__("activitypub.reader.explore.title"), + readerParent, instance: "", scope, items: [], @@ -163,6 +166,7 @@ export function exploreController(mountPath) { if (!instance) { return response.render("activitypub-explore", { title: response.locals.__("activitypub.reader.explore.title"), + readerParent, instance: rawInstance, scope, items: [], @@ -228,6 +232,7 @@ export function exploreController(mountPath) { response.render("activitypub-explore", { title: response.locals.__("activitypub.reader.explore.title"), + readerParent, instance, scope, items, diff --git a/lib/controllers/featured-tags.js b/lib/controllers/featured-tags.js index 1bb796d..db37e22 100644 --- a/lib/controllers/featured-tags.js +++ b/lib/controllers/featured-tags.js @@ -15,6 +15,7 @@ export function featuredTagsGetController(mountPath) { response.render("activitypub-featured-tags", { title: response.locals.__("activitypub.featuredTags") || "Featured Tags", + parent: { href: mountPath, text: response.locals.__("activitypub.title") }, tags, mountPath, }); diff --git a/lib/controllers/featured.js b/lib/controllers/featured.js index 45098be..3de5b33 100644 --- a/lib/controllers/featured.js +++ b/lib/controllers/featured.js @@ -57,6 +57,7 @@ export function featuredGetController(mountPath) { response.render("activitypub-featured", { title: response.locals.__("activitypub.featured") || "Pinned Posts", + parent: { href: mountPath, text: response.locals.__("activitypub.title") }, pinned, availablePosts, maxPins: MAX_PINS, diff --git a/lib/controllers/followers.js b/lib/controllers/followers.js index 98310dd..9060bc0 100644 --- a/lib/controllers/followers.js +++ b/lib/controllers/followers.js @@ -12,6 +12,7 @@ export function followersController(mountPath) { if (!collection) { return response.render("activitypub-followers", { title: response.locals.__("activitypub.followers"), + parent: { href: mountPath, text: response.locals.__("activitypub.title") }, followers: [], followerCount: 0, mountPath, @@ -33,6 +34,7 @@ export function followersController(mountPath) { response.render("activitypub-followers", { title: `${totalCount} ${response.locals.__("activitypub.followers")}`, + parent: { href: mountPath, text: response.locals.__("activitypub.title") }, followers, followerCount: totalCount, mountPath, diff --git a/lib/controllers/following.js b/lib/controllers/following.js index 3ee3155..ac02b06 100644 --- a/lib/controllers/following.js +++ b/lib/controllers/following.js @@ -12,6 +12,7 @@ export function followingController(mountPath) { if (!collection) { return response.render("activitypub-following", { title: response.locals.__("activitypub.following"), + parent: { href: mountPath, text: response.locals.__("activitypub.title") }, following: [], followingCount: 0, mountPath, @@ -33,6 +34,7 @@ export function followingController(mountPath) { response.render("activitypub-following", { title: `${totalCount} ${response.locals.__("activitypub.following")}`, + parent: { href: mountPath, text: response.locals.__("activitypub.title") }, following, followingCount: totalCount, mountPath, diff --git a/lib/controllers/migrate.js b/lib/controllers/migrate.js index b2cfa5f..91c38c0 100644 --- a/lib/controllers/migrate.js +++ b/lib/controllers/migrate.js @@ -24,6 +24,7 @@ export function migrateGetController(mountPath, pluginOptions) { response.render("activitypub-migrate", { title: response.locals.__("activitypub.migrate.title"), + parent: { href: mountPath, text: response.locals.__("activitypub.title") }, mountPath, currentAlias, result: null, @@ -61,6 +62,7 @@ export function migratePostController(mountPath, pluginOptions) { response.render("activitypub-migrate", { title: response.locals.__("activitypub.migrate.title"), + parent: { href: mountPath, text: response.locals.__("activitypub.title") }, mountPath, currentAlias, result, diff --git a/lib/controllers/moderation.js b/lib/controllers/moderation.js index a56c004..83304ae 100644 --- a/lib/controllers/moderation.js +++ b/lib/controllers/moderation.js @@ -301,6 +301,7 @@ export function moderationController(mountPath) { response.render("activitypub-moderation", { title: response.locals.__("activitypub.moderation.title"), + readerParent: { href: `${mountPath}/admin/reader`, text: response.locals.__("activitypub.reader.title") }, muted, blocked, mutedActors, diff --git a/lib/controllers/my-profile.js b/lib/controllers/my-profile.js index 7ac9ec5..d4bb79c 100644 --- a/lib/controllers/my-profile.js +++ b/lib/controllers/my-profile.js @@ -219,6 +219,7 @@ export function myProfileController(plugin) { response.render("activitypub-my-profile", { title: response.locals.__("activitypub.myProfile.title"), + readerParent: { href: mountPath, text: response.locals.__("activitypub.title") }, profile: profile || {}, handle, domain, diff --git a/lib/controllers/post-detail.js b/lib/controllers/post-detail.js index 68b7481..0bc3b67 100644 --- a/lib/controllers/post-detail.js +++ b/lib/controllers/post-detail.js @@ -197,6 +197,7 @@ export function postDetailController(mountPath, plugin) { // Truly not found (no local item either) return response.status(404).render("activitypub-post-detail", { title: response.locals.__("activitypub.reader.post.title"), + readerParent: { href: `${mountPath}/admin/reader`, text: response.locals.__("activitypub.reader.title") }, notFound: true, objectUrl, mountPath, item: null, interactionMap: {}, csrfToken: null, parentPosts: [], replyPosts: [], @@ -318,6 +319,7 @@ export function postDetailController(mountPath, plugin) { response.render("activitypub-post-detail", { title: response.locals.__("activitypub.reader.post.title"), + readerParent: { href: `${mountPath}/admin/reader`, text: response.locals.__("activitypub.reader.title") }, item: timelineItem, interactionMap, csrfToken, diff --git a/lib/controllers/profile.js b/lib/controllers/profile.js index 6db58ba..079688f 100644 --- a/lib/controllers/profile.js +++ b/lib/controllers/profile.js @@ -18,6 +18,7 @@ export function profileGetController(mountPath) { response.render("activitypub-profile", { title: response.locals.__("activitypub.profile.title"), + parent: { href: mountPath, text: response.locals.__("activitypub.title") }, mountPath, profile, actorTypes: ACTOR_TYPES, @@ -96,6 +97,7 @@ export function profilePostController(mountPath, plugin) { response.render("activitypub-profile", { title: response.locals.__("activitypub.profile.title"), + parent: { href: mountPath, text: response.locals.__("activitypub.title") }, mountPath, profile, actorTypes: ACTOR_TYPES, diff --git a/lib/controllers/profile.remote.js b/lib/controllers/profile.remote.js index 6a05ff1..2e0c629 100644 --- a/lib/controllers/profile.remote.js +++ b/lib/controllers/profile.remote.js @@ -126,6 +126,7 @@ export function remoteProfileController(mountPath, plugin) { response.render("activitypub-remote-profile", { title: name, + readerParent: { href: `${mountPath}/admin/reader`, text: response.locals.__("activitypub.reader.title") }, actorUrl, name, actorHandle, diff --git a/lib/controllers/reader.js b/lib/controllers/reader.js index b05c721..6fd15de 100644 --- a/lib/controllers/reader.js +++ b/lib/controllers/reader.js @@ -203,6 +203,7 @@ export function readerController(mountPath) { response.render("activitypub-reader", { title: response.locals.__("activitypub.reader.title"), + readerParent: { href: mountPath, text: response.locals.__("activitypub.title") }, items, tab, before: result.before, @@ -253,6 +254,7 @@ export function notificationsController(mountPath) { response.render("activitypub-notifications", { title: response.locals.__("activitypub.notifications.title"), + readerParent: { href: `${mountPath}/admin/reader`, text: response.locals.__("activitypub.reader.title") }, items: result.items, before: result.before, tab, diff --git a/lib/controllers/tag-timeline.js b/lib/controllers/tag-timeline.js index 9f10e0f..7e5aeeb 100644 --- a/lib/controllers/tag-timeline.js +++ b/lib/controllers/tag-timeline.js @@ -131,6 +131,7 @@ export function tagTimelineController(mountPath) { response.render("activitypub-tag-timeline", { title: `#${tag}`, + readerParent: { href: `${mountPath}/admin/reader`, text: response.locals.__("activitypub.reader.title") }, tag, items, before: result.before, diff --git a/views/layouts/ap-reader.njk b/views/layouts/ap-reader.njk index 12bcaa0..6698b54 100644 --- a/views/layouts/ap-reader.njk +++ b/views/layouts/ap-reader.njk @@ -18,6 +18,14 @@ {# AP link interception for internal navigation #} + {% if readerParent %} + + {% endif %} + {% block readercontent %} {% endblock %} {% endblock %}