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 %}