trails/docs/information-architecture.md
Ullrich Schäfer c7331ed056 Refresh IA doc — all six streams shipped on 2026-04-26
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>
2026-04-26 10:03:05 +02:00

25 KiB
Raw Blame History

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):

  • 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/<id>. 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/<you> 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 <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=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 (<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 ~170183). 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 <username> · 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?