Folds the actionable follow-requests surface into the Notifications page as a Requests tab (alongside the existing Activity tab), so the navbar exposes a single bell instead of two adjacent inboxes. The Requests tab shows a count badge for pending rows regardless of read state, while the bell badge keeps reflecting the unread-notifications count (which already covers `follow_request_received` rows). The standalone /follows/requests URL is preserved as a 301 redirect so prior notification deep-links, emails, and bookmarks still resolve. Driven by the IA review captured in docs/information-architecture.md. Specs (notifications, social-follows, journal-landing) are updated in the same change to reflect the new structure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
18 KiB
Information Architecture Review
Snapshot date: 2026-04-26. If the navbar, route table, or feed model has
shifted since then, treat this doc as stale and refresh against
apps/journal/app/routes.ts + apps/journal/app/root.tsx.
A snapshot of where every page lives, who sees it, and how visitors navigate between them. Intended for review — flag anything that doesn't make sense or should change.
Apps
trails.cool ships two front-ends:
- Journal (
trails.cool) — user accounts, social, content. Most of the IA question lives here. - Planner (
planner.trails.cool) — anonymous, ephemeral. Five routes total; not a real IA concern. Listed at the bottom for completeness.
Journal sitemap
Public surface (logged-out)
/ Anonymous home (hero + marketing + public feed)
/users/:username Public profile (full or locked stub)
/users/:username/followers Followers list (404 on private profile)
/users/:username/following Following list (404 on private profile)
/activities/:id Public activity (or 404)
/routes/:id Public route (or 404)
/auth/register Sign-up
/auth/login Sign-in
/auth/verify Magic-link click-through landing
/auth/accept-terms Re-accept gate (used after a Terms version bump)
/legal/imprint Imprint (German required)
/legal/privacy Privacy policy
/legal/terms Terms of service
/privacy 301 → /legal/privacy
Authenticated surface (logged-in)
/ Personal dashboard ("your activities" stream)
/feed Social feed (people you follow, accepted)
/notifications Notifications log (4 event types, cursor paginated)
/follows/requests Incoming Pending follow requests inbox
/users/:username Any profile (own, followed, or locked stub)
/users/:username/followers Gated by locked-account rule
/users/:username/following Gated by locked-account rule
/routes Your routes
/routes/new Create route
/routes/:id View
/routes/:id/edit Edit (handoff to Planner via JWT callback)
/activities Your activities
/activities/new Create activity
/activities/:id View
/settings Profile + email + passkeys + sync + danger zone
/sync/import/:provider Wahoo import flow
/auth/logout Logout
Navigation surfaces
Top navbar (logged-in) — in document order:
[trails.cool] Feed Routes Activities ... 🔔 Follow requests <username> Settings Logout
└─ unread badge └─ pending badge
Top navbar (logged-out):
[trails.cool] ... Login [Register]
Footer (everywhere): Imprint · Privacy · Terms · Source (GitHub) · "Alpha".
Profile self-link in nav points to /users/<self>. There is no separate
"My profile" route; you reach your own profile via your own username.
Logged-in vs logged-out home
/ does double duty:
| Visitor | What / shows |
|---|---|
| Anonymous | Hero · Sign-up CTAs · "Try the Planner" · marketing cards (flagship only) · public instance feed |
| Signed-in | "Welcome, X" · Feed button · "New activity" CTA · personal stream of your own activities |
The two surfaces share no layout — they're effectively two pages behind one
URL. home.tsx branches on user.
Two feed surfaces, three views
Decision (2026-04-26): merge Social and Public into a single /feed
page with a Followed / Public toggle. / stays "your content," /feed
becomes "everyone else's content."
| Surface | View | Audience |
|---|---|---|
/ (logged-in) |
Personal — your own activities | You |
/feed |
Followed (default) — accepted-followed users' public activities | Signed-in |
/feed?view=public |
Public — instance-wide public activities | Signed-in |
/ (logged-out) |
Hero + marketing + public feed (visitor entry point) | Anonymous |
Signed-in users can now see the public instance feed without logging out.
The logged-out / keeps its public feed as the anonymous visitor's first
impression.
Implementation notes (for whoever picks this up):
listSocialFeedandlistRecentPublicActivitiesalready exist inapps/journal/app/lib/activities.server.ts— the toggle just picks one.- URL shape:
?view=followed(default, can omit) |?view=public. Keeps the toggle bookmarkable and avoids two routes. - Empty-state in the Followed view today links to
/("see the public feed there") — that link should become the in-page Public toggle. - Anonymous request to
/feedkeeps redirecting to/auth/login; the public feed remains reachable for them on/.
Notifications vs follow requests
Two adjacent inboxes, two navbar entries:
/notifications |
/follows/requests |
|
|---|---|---|
| Purpose | Read-only event log | Actionable Approve/Reject |
| Types covered | follow_received, follow_request_received, follow_request_approved, activity_published | Only Pending follows targeting you |
| Navbar surface | 🔔 icon + red unread badge | "Follow requests" text + red pending count |
| State | read_at per row |
None — Approve/Reject mutates the follow row |
A follow_request_received notification points its "Review request" link at
/follows/requests, so the two surfaces are explicitly cross-linked rather
than merged. Mastodon makes the same split.
Settings
/settings is a single scrollable page with five concerns stacked top to
bottom:
- Profile (display name, bio, profile visibility)
- Email change (with re-verification)
- Passkeys (list, add, delete)
- Connected services (Wahoo today, Strava/Garmin later)
- Danger zone (delete account)
Spec was recently split into three (profile-settings, account-management,
connected-services) but the UI is still one page.
Cross-app linking
- Journal → Planner: "Edit in Planner" on a saved route generates a JWT
callback URL and opens
planner.trails.cool/session/<id>. Logged-out home also has a "Try the Planner" link. - Planner → Journal: the post-edit "Save" flow POSTs back to
/api/routes/:id/callbackon the Journal that issued the JWT. - The Planner has no link back to a specific Journal otherwise — it's intentionally instance-agnostic.
Planner sitemap
/ Anonymous home (CTA: "Plan a route")
/new Create a fresh anonymous session
/session/:id Collaborative editor
No accounts, no profiles, no settings. IA is essentially trivial.
Observations worth discussing
These are tensions or surprises I noticed while mapping. None are bugs — but each is a deliberate IA choice that's worth confirming.
-
/is two different products. Logged-in/is "your stuff," logged-out/is "the instance." Discoverable? Or should logged-in/keep showing something of the public surface (e.g., a "Discover" tab)? -
Signed-in users can't see the public instance feed.Resolved: merged into/feedwith a Followed / Public toggle (see above). -
The navbar's account cluster is busy.
<username>+Settings+Logoutis three controls for the same concept. Already on the redesign list — collapsing into an avatar dropdown is the natural fix. -
/feedis reachable from both the navbar AND a button on the home page. The button on logged-in/(currently labelled "Feed") is mildly redundant with the navbar entry. Worth keeping if Personal and Followed feel like separate destinations; worth dropping if the navbar's "Feed" link is obvious enough on its own. -
🔔 vs "Follow requests" are visually inconsistent. One is an icon, the other is a text link. Either both icons (consistent) or both text (discoverable) would be more legible. Mobile wrap is already going to be a problem.
-
Routes and Activities are siblings, not nested. That's correct today — an activity can exist without a route, and vice versa. Worth flagging only because some apps merge them into a single "history" timeline.
-
/settingsis one page. Fine for now (5 sections, ~6 controls per section). At ~10 sections it becomes a tab strip; at ~15 it becomes a left nav. Not urgent. -
No
/explore, no/usersdirectory, no search. A signed-in user who wants to find people to follow has no in-app path — they need a username from outside. Federation (Phase 2) makes this worse before it gets better. -
No mobile breakpoint for the navbar. The cluster of 7 links on the right will wrap on phones today. Tied to the navbar redesign.
-
Profile vs identity. Your own profile is at
/users/<you>not/meor/profile. Reachable via the navbar self-link. Fine, but means "view as logged-out visitor" is non-trivial — opening incognito is the only way to see your locked stub.
Open IA questions
If the answer to any of these is "I don't know yet," that's a signal it's worth discussing before the navbar redesign locks in shapes:
ShouldResolved: no —/and/feedmerge for signed-in users?/stays "you,"/feedbecomes "everyone else" with a Followed / Public toggle.Where does anResolved: it's the Public view inside/exploreor/discoverpage live?/feed, no new top-level destination needed.
Implementation backlog
Decisions captured above translate into two work-streams. Items marked needs decision are blockers — confirm before starting that stream.
Stream A — Merge Social and Public feeds into /feed
Code changes:
-
apps/journal/app/routes/feed.tsx- Loader reads
?view=query ("followed"default,"public"accepted; anything else falls back to default). - Branch the fetch:
listSocialFeed(user.id, 50)for Followed,listRecentPublicActivities(50)for Public — both already exist inapps/journal/app/lib/activities.server.ts. - Render a toggle (two pills/tabs) at the top of the page; active state
highlighted; toggle uses
<Link to="?view=public">so it's plain HTTP, SSR-friendly, bookmarkable, and works without JS. - Per-view
<meta>title: "Following — trails.cool" / "Public — trails.cool". - Per-view empty state: Followed view's existing "see the public feed"
escape now links to
?view=publicinstead of/.
- Loader reads
-
Translation keys (
apps/journal/app/locales/{en,de}/journal.json)social.feed.toggle.followed,social.feed.toggle.publicsocial.feed.public.heading,social.feed.public.empty- Drop
social.feed.publicFeedLinkonce the empty-state link is rewired.
-
No change to
apps/journal/app/routes/home.tsx— logged-in/keeps showing the personal stream; the "Feed" button still targets/feed(defaults to Followed view). -
No change to anonymous
/feedbehavior — it continues to redirect to/auth/login. The public feed remains visible to anonymous visitors on/.
Spec updates after Stream A ships:
openspec/specs/social-follows/spec.md— the Social activity feed requirement currently scopes/feedto followed users only. Update it (or split into a new "Feed views" requirement) to add scenarios for the Public view and the toggle behavior.openspec/specs/activity-feed/spec.md— the Instance-wide public activity feed requirement says it powers the home page. Add a scenario noting it now also powers/feed?view=publicfor signed-in users.openspec/specs/journal-landing/spec.md— no change. Logged-in/already shows the personal stream; this is unchanged.
Stream B — Merge Follow requests into Notifications
Decision (2026-04-26): fold /follows/requests into /notifications
as a tabbed sub-page (Activity / Requests). The bell icon stays the
single inbox surface; the Requests tab shows a dot when there are pending
follows still needing Approve/Reject. The Notifications spec's existing
"no inline Approve/Reject on the activity log" rule is preserved — the
Requests tab is the actionable surface, the Activity tab is the log.
Code changes:
-
apps/journal/app/routes/notifications.tsx- Loader reads
?tab=("activity"default,"requests"accepted). - For Activity tab: existing behavior (paginated rows,
?before=cursor). - For Requests tab: load
listPendingFollowRequests(user.id)andcountPendingFollowRequests(user.id)(already exist). - Page renders a tab strip at the top with both labels and an unread/ pending dot per tab; active tab highlighted.
- "Mark all read" button only appears on the Activity tab.
- Loader reads
-
apps/journal/app/routes.tsroute("follows/requests", ...)becomes a redirect to/notifications?tab=requests(301). New fileroutes/follows.requests.tsxreduces to oneloaderreturning a redirect, mirroring the existingroutes/privacy.tsxpattern.
-
apps/journal/app/lib/notifications/link-for.tsfollow_request_receivednow resolves to/notifications?tab=requestsinstead of/follows/requests. Update the unit test inlink-for.test.ts.
-
apps/journal/app/root.tsx- Drop the
Follow requestsnavbar entry entirely. - Drop
pendingFollowRequestsfrom the root loader (no longer needed for navbar). The Requests tab loader inside/notificationsfetches it on-demand. - Bell unread badge logic unchanged —
follow_request_receivedalready creates an unread row, so the existing unread count implicitly covers pending requests.
- Drop the
-
i18n updates (
packages/i18n/src/locales/{en,de}.ts)- New keys:
notifications.tabs.activity,notifications.tabs.requests. - The
settings.profile.visibility.privateHelpstring mentions/follows/requests— update to point at the new surface.
- New keys:
-
e2e tests
e2e/social.test.ts: the/follows/requests redirects anonymous visitors to logintest changes to verify the new redirect target, and the "B sees the request in /follows/requests" step navigates to/notifications?tab=requestsinstead.e2e/notifications.test.ts: same — replacebPage.goto("/follows/requests")withbPage.goto("/notifications?tab=requests").
Spec updates after Stream B ships:
openspec/specs/notifications/spec.md— extend the Notifications page and unread count requirement to describe the tabbed structure; refine the "follow_request_received card links to /follows/requests, not inline Approve/Reject" scenario to reflect the new URL and the tab separation. ThelinkForscenarios need the updated path.openspec/specs/social-follows/spec.md— update the Pending follow request management requirement: navigation moves to/notifications?tab=requests; the navbar count badge requirement is retired (it's now a dot inside the Requests tab).openspec/specs/journal-landing/spec.md— the Notifications entry in the navbar requirement should clarify that this is the only inbox-style entry (no separate Follow requests entry).
Stream C — Navbar redesign
Pending tomorrow. Principles agreed in the IA review; specifics still need decisions before implementation.
Needs decision:
- Avatar dropdown content. Profile · Settings · Logout — any others? (Theme toggle? Language toggle? Account switcher when federation lands?)
- Mobile target. Hamburger? Bottom tab bar? "Phase 2, just don't break"?
- Self-link vs avatar. Today the navbar has a
<username>text link to your profile. After the avatar dropdown, does the avatar take that role, or does Profile live only inside the dropdown?
Code changes (assuming avatar dropdown + icon-only Follow requests + no mobile work yet):
-
apps/journal/app/root.tsx- Add
displayNameto the loader'suserpayload (currently onlyid+username). - Replace the
<username> · Settings · Logoutcluster with a single avatar trigger + dropdown menu. - Replace the "Follow requests" text link with an icon-only button + badge (matching the bell's visual treatment).
- Confirm bell + Follow requests sit on the same baseline / same gap.
- Add
-
New components in
apps/journal/app/components/Avatar.tsx— initials fallback when no image (none of our 4 prod users have an avatar field today, so this is initials-only initially).NavDropdown.tsx— click-outside + Escape-to-close. Simple headless impl, no library.
-
Loader query
getSessionUseralready returnsdisplayName; just expose it in the loader return shape.
Spec updates after Stream B ships:
openspec/specs/journal-landing/spec.md— currently has only the Notifications entry in the navbar requirement. Add a new requirement (or extend that one) for the account dropdown shape and the Follow-requests icon. Or factor the navbar out into its own spec — plausible if the cluster keeps growing.openspec/specs/social-follows/spec.md— the Pending follow request management requirement says "the 'Follow requests' link in the navbar renders with a small red count badge". Update the wording to reflect the icon treatment.openspec/specs/notifications/spec.md— the Notifications page and unread count requirement already says "navbar entry renders with a count badge"; verify the wording still applies cleanly to the icon-only treatment.
Out of scope (deliberately)
These came up in the IA review but aren't part of this round:
- Search /
/exploredirectory. No way to find users in-app. Defer until federation (Phase 2) makes it more painful. - Settings sectioning. Single scrollable page is fine at 5 sections.
/mealias for own profile. Marginal value; navbar self-link is enough.- Mobile breakpoints across the app. Bigger than just the navbar.
- Is "Follow requests" a sub-page of Notifications or a sibling? Today
it's a sibling (separate navbar item). Could be a tab inside
/notifications. - Avatar dropdown content. Profile · Settings · Logout — anything else? (Theme toggle? Language toggle? Account switcher when federation lands?)
- Mobile. Hamburger menu? Bottom tab bar? Nothing yet — what's the target?
- Search. None today. Adding it changes the navbar. When does it land?