Streams A, B, C, D, E, F all landed today. This refresh marks them shipped with their PR numbers, brings the sitemap and navbar diagrams in sync with the deployed state, and rewrites the "Notifications vs follow requests" and "Settings" sections to describe the new structure rather than the historical split. Snapshot date bumped to "2026-04-26 (post-streams)" so a future re-read knows what state this snapshot reflects. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
25 KiB
Information Architecture Review
Snapshot date: 2026-04-26 (post-streams). Streams A, B, C, D, E, F all
shipped on 2026-04-26 (see backlog below for PR numbers). 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.
To regenerate this doc (fresh or refresh), invoke the /ia-review
skill — it walks the route tables + nav surfaces, builds the sitemap,
flags drift since this snapshot, and preserves the decisions/backlog
already captured below. The companion /spec-drift-review skill
does the equivalent for openspec/specs/ against shipped code.
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)
/explore Local user directory (also reachable to signed-in)
/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 Feed (Followed default, ?view=public for instance-wide)
/explore Local user directory (also reachable to anonymous)
/notifications Tabbed inbox: Activity (default) | ?tab=requests
/follows/requests 301 → /notifications?tab=requests (legacy URL)
/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 → /settings/profile (redirect)
/settings/profile Display name, bio, profile visibility
/settings/account Email + danger zone (delete account)
/settings/security Passkeys
/settings/connections Sync providers (Wahoo today)
/sync/import/:provider Wahoo import flow
/auth/logout Logout
Navigation surfaces
Top navbar (logged-in, desktop ≥ md) — in document order:
[trails.cool] Feed Explore Routes Activities ... 🔔 [Avatar ▾]
└─ unread badge └─ Profile / Settings / Log Out
Top navbar (logged-in, mobile < md):
[trails.cool] ... 🔔 [☰]
└─ drawer with all destinations + account
Top navbar (logged-out, all viewports):
[trails.cool] ... Login [Register]
Footer (everywhere): Imprint · Privacy · Terms · Source (GitHub) · "Alpha".
Profile self-link is the avatar dropdown's first item (Profile →
/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 and follow requests (single inbox)
Folded into /notifications as a tabbed page (Stream B, PR #316). One
navbar entry — the bell — covers both surfaces:
| Tab | URL | Purpose |
|---|---|---|
| Activity (default) | /notifications |
Read-only event log: follow_received, follow_request_received, follow_request_approved, activity_published |
| Requests | /notifications?tab=requests |
Actionable list of Pending follow requests with Approve / Reject buttons |
The bell badge counts unread notifications (which already includes
follow_request_received rows, so pending requests are reflected). The
Requests tab shows an additional dot reflecting the pending count
regardless of read state, so a user who's read the notification but
hasn't acted yet still sees there's something to do. The legacy
/follows/requests URL 301-redirects to the Requests tab.
Settings
Split into four sub-pages behind a shared sidebar layout (Stream E,
PR #323). /settings itself redirects to /settings/profile.
| URL | Concerns |
|---|---|
/settings/profile |
display name, bio, profile visibility |
/settings/account |
email change + danger zone (delete account) |
/settings/security |
passkeys |
/settings/connections |
sync providers (Wahoo today) |
Specs (profile-settings, account-management, connected-services)
describe behavior, not URL structure — the URL split is a UI choice that
doesn't change the requirements.
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)? Status: open — not sure yet if this needs resolving. -
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.Folded into Stream C (navbar redesign): regroup the cluster behind an avatar dropdown. -
Decision: drop the button on logged-in/feedis reachable from both the navbar AND a button on the home page./— the navbar entry is enough. See Stream D. -
🔔 vs "Follow requests" are visually inconsistent.Resolved by Stream B — Follow requests folded into the bell as a Requests tab, so there's a single inbox icon in the navbar. -
Routes and Activities are siblings, not nested. That's correct today — an activity can exist without a route, and vice versa. Flagged for a broader review: the concept of routes-vs-activities, plus the impending word collision with social "activities" (comment, like, publish) once federation lands. Tracked separately — see "Open exploration" below.
-
Decision: break it apart. See Stream E./settingsis one page. -
NoDecision: propose an/explore, no/usersdirectory, no search./explorespec. See Stream F. -
No mobile breakpoint for the navbar.Decision: include mobile responsiveness in Stream C (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. Status: open — no decision yet.
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 ✅ Shipped (PR #319)
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 ✅ Shipped (PR #316)
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 ✅ Shipped (PR #324)
Scope (decided):
- Regroup the account cluster (
<username>+ Settings + Logout) behind an avatar dropdown. - Treat mobile responsiveness as a first-class concern in the redesign, not a phase-2 follow-up. Today the navbar wraps badly on phones.
- The bell + Requests tab from Stream B already gives the navbar a consistent inbox surface; nothing further needed there.
Needs decision before implementation:
- Avatar dropdown content. Profile · Settings · Logout — any others? (Theme toggle? Language toggle? Account switcher when federation lands?)
- Mobile pattern. Hamburger drawer? Bottom tab bar? Condensed top bar with the dropdown absorbing most controls?
- 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 (click → profile, dropdown chevron → menu), or does Profile live only inside the dropdown?
Stream D — Drop the redundant "Feed" button on logged-in / ✅ Shipped (PR #318)
Decision (2026-04-26): logged-in / no longer needs a "Feed" button
in the page header — the navbar entry is the single discoverable path.
Code changes:
apps/journal/app/routes/home.tsx— remove the<a href="/feed">button next to the "New Activity" CTA in the signed-in branch (currently lines ~170–183). Keep "New Activity" as the only header action.- No spec changes;
journal-landing/spec.mdhas a Social feed link for signed-in users requirement that mentions a "Feed (or equivalent) link" — that requirement should be retired.
Tiny PR, ~5 lines + a spec update.
Stream E — Break Settings apart ✅ Shipped (PR #323)
Decision (2026-04-26): /settings becomes a sectioned area rather
than a single scrollable page. Spec was already split into three
(profile-settings, account-management, connected-services); the UI
should follow.
Needs decision before implementation:
- Layout pattern. Tab strip on
/settings(single URL, JS-driven tabs)? Nested routes (/settings/profile,/settings/security,/settings/connections,/settings/danger)? Sidebar nav? - Section list. The current page has five concerns:
- Profile (display name, bio, profile visibility)
- Email (with re-verification)
- Passkeys
- Connected services (Wahoo today)
- Danger zone (delete account) Keep five? Merge "Email" into "Profile"? Pull "Danger zone" into "Account" alongside Email?
Spec impact: profile-settings, account-management, and
connected-services are already separate specs — they describe behavior,
not URL structure. Whichever URL pattern wins, only journal-landing
(or wherever the navbar currently sits) needs to know.
Stream F — Propose an /explore spec ✅ Shipped (proposal #320, implementation #321, archive/promote #322)
Decision (2026-04-26): the gap between "I want to find people to
follow" and "I have a username from outside" needs an in-app path.
Federation (Phase 2) makes this more valuable, but local-only /explore
is useful on day one.
Approach: kick off an OpenSpec proposal via /opsx:propose rather
than diving into code. The proposal phase decides:
- What
/exploreactually shows. A directory of all local users? A curated "active in the last N days" list? A randomized rotation? Just the public activity feed (which is currently buried on logged-out/)? - Whether search is part of v1 or a follow-up.
- Where the link lives in the navbar (its own entry vs. inside
/feed). - Privacy: private profiles need to be excluded from any directory; this is the same locked-account access rule that already gates followers/following lists.
Spec impact: new spec file at
openspec/specs/explore/spec.md (or similar — name TBD by the proposal),
plus an entry in CAPABILITIES.md.
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:
/mealias for own profile. Marginal value; navbar self-link is enough.- Mobile breakpoints across the app. Bigger than just the navbar. Stream C handles the navbar; the rest of the app is a separate effort.
Open exploration
Items that aren't ready for an implementation backlog because the underlying concept still needs work, not just the UI.
Routes vs Activities — terminology and model
The IA review flagged that Routes and Activities are sibling top-level concepts. Two reasons to revisit this beyond the URL structure:
-
Conceptual overlap. A route is a planned path. An activity is a recorded outing (often along a route). Some apps (Strava, Komoot) collapse these into one timeline; we keep them split. Worth confirming the split still earns its keep.
-
Word collision with social "activities". Once federation lands (and arguably already, with the
activity_publishednotification type), "activity" will be overloaded:- Athletic activity: a bike ride, a hike, a run.
- Social activity: a comment, a like, a follow, a publish event. This is the ActivityPub sense — and it's the one that will appear in feeds, notifications, and remote inboxes.
The collision is going to bite. ActivityPub uses "Activity" as a technical term throughout; the user-facing "Activity" (a ride) will constantly be next to ActivityPub Activities (a follow, a like) in logs, diagrams, and possibly UI strings.
Plan: dedicate a separate review doc (e.g.
docs/routes-vs-activities.md) to think this through before any spec
or code change. Open questions for that doc:
- Do we rename the user-facing "Activity" to something else (Outing? Trip? Ride? Record? Trace?) before federation cements the social meaning?
- Or do we keep "Activity" for the user-facing object and use "Event" or "Stream item" for the social/ActivityPub sense?
- Are Routes still needed as a top-level object, or could they be a subordinate concept of Activities (a "saved planned version" of something you may eventually ride)?
- What does the export/import story look like — GPX is route-shaped; Strava's TCX is activity-shaped; how do we want the model to talk about each?
Not urgent, but worth resolving before federation is far enough along that renaming is a migration headache.
- 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?