Guild TOC Layout & Posts
Implementation plan for replacing guild tabs with a sidebar TOC and adding a guild posts (blog) feature.
Overview
- TOC Layout: Replace horizontal tabs with a sidebar table-of-contents navigation
- Guild Posts: New blog-style entity for officer announcements (read-only for members)
1. TOC Layout
Current State
$guildId.tsxuses HeroUITabswith Overview, Members, Info, Ranks, Settings- Route matching via
useMatchRoutedrivesselectedKey - Tab visibility: Info/Ranks require
isMember; Settings requirescanManageGuild
Target State
- Sidebar nav (left) with links to each section
- Active route highlighted via
Link+useMatchRouteoruseParams - Same permission rules: members see Overview, Members, Info, Posts, Ranks; officers also see Settings
Routes (unchanged)
| Path | Description |
|---|---|
/guilds/:guildId | Overview |
/guilds/:guildId/members | Roster |
/guilds/:guildId/info | 6 fixed sections (MOTD, rules, etc.) |
/guilds/:guildId/posts | Blog list (new) |
/guilds/:guildId/posts/:postId | Single post view/edit (new) |
/guilds/:guildId/ranks | Role management |
/guilds/:guildId/settings | Guild settings (officers only) |
Implementation
- Replace
Tabs/Tabwith anav+ list ofLinkcomponents - Use
useMatchRouteorLink'sactivePropsto highlight active item - Add
NewspaperIcon(or similar) for Posts in nav - Handle nested route highlight (e.g.
/posts/123should highlight "Posts")
2. Guild Posts
Schema
See apps/api/prisma/schema.prisma for the authoritative OrganizationPost model. Key fields:
slug— optional URL-friendly identifier, unique per organization (@@unique([organizationId, slug]))status—draftorpublishedvisibility/draftVisibility— controls who can see published posts vs draftsversion— auto-incremented on each update; full history tracked inOrganizationPostHistory
Slug System
Posts support URL-friendly slugs (e.g. /guilds/my-guild/posts/raid-schedule-update).
Resolution: All API endpoints accepting a postId param resolve by UUID or slug. The shared utility findPostByIdOrSlug (mirrors findOrganizationByIdOrAlias) checks if the param is a UUID regex match — if so queries by id, otherwise by slug.
Uniqueness: Slugs are unique per organization via @@unique([organizationId, slug]). Optional — posts without slugs fall back to UUID in URLs.
Validation (3 layers):
- Client-side sanitization —
sanitizeSlug()auto-lowercases, replaces spaces with hyphens, strips invalid chars on every keystroke - JIT availability check — debounced (400ms) call to
GET /organizations/:orgId/posts/check-slug?slug=xxx&excludePostId=yyyshows inline "slug taken" error.excludePostIdaccepts UUID or slug for edit-mode self-exclusion. - Server-side DTO —
@Matches(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)on bothCreatePostDtoandUpdatePostDtorejects invalid formats
Frontend URLs: PostCard and navigation use post.slug ?? post.id — clean URLs when slug exists, UUID fallback otherwise.
API
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /organizations/:orgId/posts | Member | List posts (newest first, paginated) |
| GET | /organizations/:orgId/posts/check-slug | Officer | Check slug availability (query params) |
| GET | /organizations/:orgId/posts/:postId | Member | Single post (by UUID or slug) |
| POST | /organizations/:orgId/posts | Officer | Create post |
| PATCH | /organizations/:orgId/posts/:postId | Officer | Update post (by UUID or slug) |
| DELETE | /organizations/:orgId/posts/:postId | Officer | Delete post (by UUID or slug) |
| GET | /organizations/:orgId/posts/:postId/history | Officer | Post version history |
Visibility & Caching
Published visibility: public (anyone), guild (members only), officers (officers only). Unauthenticated requests see only public posts. Authenticated guild members see public + guild; officers also see officers.
Draft visibility: creator (author only), officers (all officers), specific (draftVisibleTo list). Drafts require includeDrafts=true (officers only, verified server-side).
Caching: Posts endpoints use Cache-Control: no-store and Vary: Authorization so responses are not cached by auth state. The frontend waits for guild (and thus auth) before fetching posts (skip: !guild).
Domain
- New domain or extend guild:
apps/api/src/domains/guild/orapps/api/src/domains/post/ - Use cases:
CreateGuildPostUseCase,GetGuildPostsUseCase,GetGuildPostUseCase,UpdateGuildPostUseCase,DeleteGuildPostUseCase - Reuse
RichTextEditor/RichTextDisplayfrom guild info
Frontend
- RTK Query:
guildsApior newguildPostsApislice - Routes:
posts.tsx(list),posts.$postId.tsx(detail/edit) - List: card per post with title, excerpt, date; "New post" button for officers
- Detail: read view for all; edit mode for officers (reuse RichTextEditor)
3. Task Breakdown
Phase 1: TOC Layout
- Replace tabs with sidebar nav in
$guildId.tsx - Add Posts link (route stub or 404 until Phase 2)
- Update route matching for nested routes (posts, posts/:id)
Phase 2: Guild Posts Backend
- GuildPost schema + migration
- Use cases + controller + routes
- Tests
Phase 3: Guild Posts Frontend
- RTK Query endpoints
posts.tsxroute + list pageposts.$postId.tsxroute + detail/edit page- Tests
4. Scope Notes
- Guild posts are one-way (officers → members), not a forum
- No comments, reactions, or nested discussions
- Pagination: default 10–20 per page
- Ordering: newest first (configurable later if needed)