Skip to main content

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.tsx uses HeroUI Tabs with Overview, Members, Info, Ranks, Settings
  • Route matching via useMatchRoute drives selectedKey
  • Tab visibility: Info/Ranks require isMember; Settings requires canManageGuild

Target State

  • Sidebar nav (left) with links to each section
  • Active route highlighted via Link + useMatchRoute or useParams
  • Same permission rules: members see Overview, Members, Info, Posts, Ranks; officers also see Settings

Routes (unchanged)

PathDescription
/guilds/:guildIdOverview
/guilds/:guildId/membersRoster
/guilds/:guildId/info6 fixed sections (MOTD, rules, etc.)
/guilds/:guildId/postsBlog list (new)
/guilds/:guildId/posts/:postIdSingle post view/edit (new)
/guilds/:guildId/ranksRole management
/guilds/:guildId/settingsGuild settings (officers only)

Implementation

  1. Replace Tabs/Tab with a nav + list of Link components
  2. Use useMatchRoute or Link's activeProps to highlight active item
  3. Add NewspaperIcon (or similar) for Posts in nav
  4. Handle nested route highlight (e.g. /posts/123 should 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]))
  • statusdraft or published
  • visibility / draftVisibility — controls who can see published posts vs drafts
  • version — auto-incremented on each update; full history tracked in OrganizationPostHistory

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

  1. Client-side sanitizationsanitizeSlug() auto-lowercases, replaces spaces with hyphens, strips invalid chars on every keystroke
  2. JIT availability check — debounced (400ms) call to GET /organizations/:orgId/posts/check-slug?slug=xxx&excludePostId=yyy shows inline "slug taken" error. excludePostId accepts UUID or slug for edit-mode self-exclusion.
  3. Server-side DTO@Matches(/^[a-z0-9]+(?:-[a-z0-9]+)*$/) on both CreatePostDto and UpdatePostDto rejects invalid formats

Frontend URLs: PostCard and navigation use post.slug ?? post.id — clean URLs when slug exists, UUID fallback otherwise.

API

MethodPathAuthDescription
GET/organizations/:orgId/postsMemberList posts (newest first, paginated)
GET/organizations/:orgId/posts/check-slugOfficerCheck slug availability (query params)
GET/organizations/:orgId/posts/:postIdMemberSingle post (by UUID or slug)
POST/organizations/:orgId/postsOfficerCreate post
PATCH/organizations/:orgId/posts/:postIdOfficerUpdate post (by UUID or slug)
DELETE/organizations/:orgId/posts/:postIdOfficerDelete post (by UUID or slug)
GET/organizations/:orgId/posts/:postId/historyOfficerPost 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/ or apps/api/src/domains/post/
  • Use cases: CreateGuildPostUseCase, GetGuildPostsUseCase, GetGuildPostUseCase, UpdateGuildPostUseCase, DeleteGuildPostUseCase
  • Reuse RichTextEditor / RichTextDisplay from guild info

Frontend

  • RTK Query: guildsApi or new guildPostsApi slice
  • 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

  1. Replace tabs with sidebar nav in $guildId.tsx
  2. Add Posts link (route stub or 404 until Phase 2)
  3. Update route matching for nested routes (posts, posts/:id)

Phase 2: Guild Posts Backend

  1. GuildPost schema + migration
  2. Use cases + controller + routes
  3. Tests

Phase 3: Guild Posts Frontend

  1. RTK Query endpoints
  2. posts.tsx route + list page
  3. posts.$postId.tsx route + detail/edit page
  4. 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)