Stream F from docs/information-architecture.md. Closes the gap between "I want to follow someone on this instance" and "I have a username from outside the app." This change is the OpenSpec proposal only — the four artifacts (proposal.md, design.md, specs/, tasks.md) plus a new top-level spec target at openspec/specs/explore/. Implementation lands in a follow-up PR via /opsx:apply. Key decisions captured in design.md: - Anonymous access is allowed (the data shown is already public, and it's a stronger first-visit experience). - Directory order: MAX(activities.created_at) DESC NULLS LAST, tiebreaker users.id DESC. Recency-of-activity is the simplest signal that meaningfully reflects "who's active here right now." - "Active recently" sub-section: same query, sliced — top 5 users with a public activity in the last 30 days, hidden when empty. - Privacy filter: profile_visibility = 'public' AND not the demo persona AND (forward-compat) not banned/suspended. - No search in v1 — deferred until the user count makes it actually useful. /users/<username> already partially solves it. - Offset pagination (?page=, ?perPage=, capped 100). Cursor pagination is the right answer at ~10K users; not now. - Navbar gets an "Explore" entry for signed-in users; anonymous visitors reach /explore via a link on the visitor home. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.4 KiB
4.4 KiB
1. Server-side directory query
- 1.1 Create
apps/journal/app/lib/explore.server.tswithlistDirectory({ page, perPage })returning{ rows, totalCount }. Rows includeid,username,displayName,bio,latestActivityAt(nullable), and the user'sprofileVisibility. Filters:profile_visibility = 'public', exclude demo persona, exclude future banned/suspended (placeholder filter — leave a TODO for when the column exists). - 1.2 Order:
LEFT JOIN activities ON activities.owner_id = users.id AND activities.visibility = 'public', thenMAX(activities.created_at) DESC NULLS LAST, tiebreakerusers.id DESC. - 1.3 Add
listActiveRecently(limit = 5)returning the same row shape, filtered to users with at least one public activity in the last 30 days, capped atlimit. - 1.4 Add
countDirectory()returning the total number of users that match the directory's filter, for pagination math. - 1.5 Add a follower-count batch helper (
countFollowersBatch(userIds)) so the page doesn't fan out N+1 queries — one query per page load. - 1.6 Add
apps/journal/app/lib/explore.integration.test.tscovering: public user appears, private user excluded, demo persona excluded, ordering by recency, tiebreaker by id, "Active recently" 30-day filter, "Active recently" cap.
2. Route + UI
- 2.1 Register
/exploreinapps/journal/app/routes.ts. - 2.2 Create
apps/journal/app/routes/explore.tsx:- Loader reads
?page=(default 1) and?perPage=(default 20, clamp[1, 100]); fetcheslistActiveRecently(5),listDirectory({ page, perPage }),countDirectory(), and (for signed-in viewers) per-row follow state via existinggetFollowStatebatched. - Component renders the "Active recently" strip (if non-empty), then the main directory list, then "Previous" / "Next" links.
- Loader reads
- 2.3 Reuse
<FollowButton>for the per-row action; render only when the loader returns a signed-in viewer. - 2.4 Bio truncation utility — first 120 characters + ellipsis, plain string slice (no markdown rendering yet).
- 2.5 Empty-state copy when the directory itself is empty (no public users at all).
- 2.6 Page-beyond-last empty state — render the empty list with a "Previous page" link, no 404.
3. Navbar + visitor home links
- 3.1 Add the "Explore" entry to the signed-in navbar in
apps/journal/app/root.tsx, positioned alongside Feed / Routes / Activities. Visual treatment is plain for now; the navbar redesign (Stream C) refines it later. - 3.2 Add a link to
/exploreon the visitor home (apps/journal/app/routes/home.tsx, signed-out branch) — secondary to the Register / Sign In CTAs but on the same fold. Wording: e.g. "Browse who's here →". - 3.3 No new link on the signed-in home — the navbar entry covers it.
4. i18n
- 4.1 Add to
packages/i18n/src/locales/en.tsandde.ts:explore.title,explore.heading,explore.empty,explore.activeRecently.headingnav.explorehome.exploreLink(visitor-home wording)
- 4.2 No i18n changes needed for FollowButton — it already uses the
social.*keys.
5. Spec promotion + index
- 5.1 At archive time, promote
openspec/changes/add-explore-page/specs/explore/spec.mdtoopenspec/specs/explore/spec.md(no purpose section needed yet — the## ADDED Requirementscontent becomes the spec body). - 5.2 At archive time, merge the
journal-landingdelta intoopenspec/specs/journal-landing/spec.md. - 5.3 Add an entry for
exploreinopenspec/CAPABILITIES.mdunder the Social section.
6. Tests
- 6.1 Integration tests for the directory query (covered in 1.6).
- 6.2 Add an e2e test in
e2e/explore.test.ts:- Anonymous loads
/exploreand sees the directory but no Follow buttons. - Signed-in loads
/exploreand sees Follow buttons; clicking Follow on a public profile transitions the row to Unfollow without a hard reload. - A private profile does not appear in the directory regardless of which user is viewing.
- Anonymous loads
- 6.3 Run
pnpm typecheck && pnpm lint && pnpm test && pnpm test:e2ebefore opening the PR.
7. PR + rollout
- 7.1 Open a PR with the change, link to
openspec/changes/add-explore-page/, enable auto-merge. - 7.2 After merge, archive the change (
/opsx:archive add-explore-page) so the deltas promote into top-level specs. - 7.3 Update
docs/information-architecture.mdto flip Stream F to ✅ Shipped and refresh the navbar diagram to include "Explore".