Captures two recurring review processes as repeatable Claude skills so they don't have to be reinvented from scratch each time: - /ia-review: walks the route tables + nav surfaces, builds the sitemap, flags drift, and writes/refreshes docs/information-architecture.md. Refresh mode preserves the user's accumulated decisions and implementation backlog while only rewriting the snapshot sections. - /spec-drift-review: walks every spec in openspec/specs/, compares it to shipped code, and produces a categorized drift report (high/medium/low severity) plus code-without-spec findings and structural suggestions (split/merge/new specs, CAPABILITIES.md catch-up). Excludes specs touched by active openspec changes since that mismatch is intended in-flight work, not drift. Also extends the IA doc with the items resolved/deferred during the review (observations 1-10), three new streams (D: drop redundant Feed button on logged-in /, E: break Settings apart, F: propose /explore spec), and an "Open exploration" section for the routes-vs-activities terminology question. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
24 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.
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)
/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)? 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
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
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 /
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
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
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?