WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

Topic View Design — ATB-29#

Date: 2026-02-19 Linear: ATB-29 Status: Approved

Overview#

Implement the topic view page (GET /topics/:id) in the web app. The page displays a thread: the original post (OP) followed by all replies in chronological order, with HTMX Load More pagination and bookmarkable URLs.

Architecture#

Route#

The existing stub in apps/web/src/routes/topics.tsx is replaced. The factory signature stays the same:

export function createTopicsRoutes(appviewUrl: string): Hono

The single handler GET /topics/:id operates in two modes:

  • Full page mode — renders the complete HTML page (OP + breadcrumb + initial replies)
  • HTMX partial mode — detected via HX-Request header; returns only the next batch of reply cards + a new Load More button

Data Fetching (full page)#

Three stages, each with independent error handling:

Stage Requests Fatal?
1 (parallel) GET /api/topics/:id + session check Yes — 404/503/500 on failure
2 (sequential) GET /api/boards/:boardId No — breadcrumb degrades
3 (sequential) GET /api/categories/:categoryId No — breadcrumb degrades

Bookmark support#

When ?offset=N is present on initial load, replies 0→N are fetched and rendered inline. The Load More button then fetches from N onward.

Components#

All inline in topics.tsx (no new files).

PostCard({ post, postNumber, isOP })#

Renders a single post. OP gets CSS modifier post-card--op (thicker border/larger shadow). Each card shows:

  • Bold post number badge (#1, #2, …)
  • Full text with white-space: pre-wrap
  • Author handle (fallback to DID if null)
  • Relative time via timeAgo()

LoadMoreButton({ topicId, nextOffset })#

HTMX button that appends the next reply batch:

hx-get="/topics/:id?offset=N"
hx-swap="outerHTML"
hx-target="this"
hx-push-url="/topics/:id?offset=N"

hx-push-url advances the browser URL so each Load More click produces a bookmarkable URL.

ReplyFragment({ topicId, replies, total, offset })#

Renders a batch of PostCard reply cards followed by a LoadMoreButton when nextOffset < total.

Locked topic banner#

Rendered inline on the full page above the reply list. High-contrast neobrutal warning style. Hides the reply form slot when present.

Reply form slot#

<div id="reply-form-slot">
  <!-- auth-gated: login prompt or placeholder -->
</div>

The compose forms ticket will fill this slot. For now: show "Log in to reply" for guests, placeholder text for authenticated users.

Error Handling#

Follows the pattern established in boards.tsx:

Condition Response
Non-numeric ID 400 HTML page
Topic not found (API 404) 404 HTML page
Network error 503 HTML page
Other API error 500 HTML page
Programming error (TypeError etc.) Re-thrown
Board/category fetch failure Non-fatal — breadcrumb degrades
HTMX partial error Empty fragment (page does not break)

Testing#

File: apps/web/src/routes/__tests__/topics.test.tsx

Test Verifies
Non-numeric ID 400
Topic not found 404 HTML
Network error 503
AppView 500 500
Happy path — OP only Topic text, author, #1 badge rendered
Happy path — with replies Replies rendered with #2, #3 badges
Locked topic Locked banner shown, reply form hidden
Unauthenticated "Log in to reply" shown
Authenticated Reply form slot shown
Load More shown When nextOffset < total
Load More hidden When all replies loaded
HTMX partial Returns reply fragment with hx-push-url
HTMX partial error Empty fragment returned
Board fetch fails Page renders, breadcrumb degrades
Category fetch fails Page renders, breadcrumb degrades
Bookmark URL ?offset=N OP + all replies 0→N rendered inline