# 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/`). 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): - `listSocialFeed` and `listRecentPublicActivities` already exist in `apps/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 `/feed` keeps 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/`. Logged-out home also has a "Try the Planner" link. - **Planner → Journal:** the post-edit "Save" flow POSTs back to `/api/routes/:id/callback` on 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. 1. **`/` 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.* 2. ~~**Signed-in users can't see the public instance feed.**~~ *Resolved: merged into `/feed` with a Followed / Public toggle (see above).* 3. ~~**The navbar's account cluster is busy.**~~ *Folded into Stream C (navbar redesign): regroup the cluster behind an avatar dropdown.* 4. ~~**`/feed` is reachable from both the navbar AND a button on the home page.**~~ *Decision: drop the button on logged-in `/` — the navbar entry is enough. See Stream D.* 5. ~~**🔔 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.* 6. **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.* 7. ~~**`/settings` is one page.**~~ *Decision: break it apart. See Stream E.* 8. ~~**No `/explore`, no `/users` directory, no search.**~~ *Decision: propose an `/explore` spec. See Stream F.* 9. ~~**No mobile breakpoint for the navbar.**~~ *Decision: include mobile responsiveness in Stream C (navbar redesign).* 10. **Profile vs identity.** Your own profile is at `/users/` not `/me` or `/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: - ~~Should `/` and `/feed` merge for signed-in users?~~ *Resolved: no — `/` stays "you," `/feed` becomes "everyone else" with a Followed / Public toggle.* - ~~Where does an `/explore` or `/discover` page live?~~ *Resolved: it's the Public view inside `/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:** 1. **`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 in `apps/journal/app/lib/activities.server.ts`. - Render a toggle (two pills/tabs) at the top of the page; active state highlighted; toggle uses `` so it's plain HTTP, SSR-friendly, bookmarkable, and works without JS. - Per-view `` title: "Following — trails.cool" / "Public — trails.cool". - Per-view empty state: Followed view's existing "see the public feed" escape now links to `?view=public` instead of `/`. 2. **Translation keys (`apps/journal/app/locales/{en,de}/journal.json`)** - `social.feed.toggle.followed`, `social.feed.toggle.public` - `social.feed.public.heading`, `social.feed.public.empty` - Drop `social.feed.publicFeedLink` once the empty-state link is rewired. 3. **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). 4. **No change** to anonymous `/feed` behavior — 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 `/feed` to 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=public` for 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:** 1. **`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)` and `countPendingFollowRequests(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. 2. **`apps/journal/app/routes.ts`** - `route("follows/requests", ...)` becomes a redirect to `/notifications?tab=requests` (301). New file `routes/follows.requests.tsx` reduces to one `loader` returning a redirect, mirroring the existing `routes/privacy.tsx` pattern. 3. **`apps/journal/app/lib/notifications/link-for.ts`** - `follow_request_received` now resolves to `/notifications?tab=requests` instead of `/follows/requests`. Update the unit test in `link-for.test.ts`. 4. **`apps/journal/app/root.tsx`** - Drop the `Follow requests` navbar entry entirely. - Drop `pendingFollowRequests` from the root loader (no longer needed for navbar). The Requests tab loader inside `/notifications` fetches it on-demand. - Bell unread badge logic unchanged — `follow_request_received` already creates an unread row, so the existing unread count implicitly covers pending requests. 5. **i18n updates** (`packages/i18n/src/locales/{en,de}.ts`) - New keys: `notifications.tabs.activity`, `notifications.tabs.requests`. - The `settings.profile.visibility.privateHelp` string mentions `/follows/requests` — update to point at the new surface. 6. **e2e tests** - `e2e/social.test.ts`: the `/follows/requests redirects anonymous visitors to login` test changes to verify the new redirect target, and the "B sees the request in /follows/requests" step navigates to `/notifications?tab=requests` instead. - `e2e/notifications.test.ts`: same — replace `bPage.goto("/follows/requests")` with `bPage.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. The `linkFor` scenarios 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 (`` + 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 `` 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 `` 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.md` has 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: 1. Profile (display name, bio, profile visibility) 2. Email (with re-verification) 3. Passkeys 4. Connected services (Wahoo today) 5. 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 `/explore` actually 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):** 1. **`apps/journal/app/root.tsx`** - Add `displayName` to the loader's `user` payload (currently only `id` + `username`). - Replace the ` · Settings · Logout` cluster 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. 2. **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. 3. **Loader query** - `getSessionUser` already returns `displayName`; 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: - **`/me` alias 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: 1. **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. 2. **Word collision with social "activities".** Once federation lands (and arguably already, with the `activity_published` notification 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?