Repository source: docs/plans/mitrapclub_theme_and_features_checklist.md
MIT Rap Club — de-genericize + extend checklist
Raw candidate list, not yet scoped or decided — input for the next FDP round (likely Step 1, since several items are genuine either/or calls). Grouped by what to strip vs. what to add; each item below reflects something actually checked in the current code, not a guess.
Strip away generic (not-MIT-Rap-Club-specific) surface
- [x] Header/title text (`SiteConfig::siteName()`) renders the profile's
`name` (`"mitrapclub"`, lowercase slug) rather than `displayName`
(`"MIT Rap Club"`) — inconsistent with the about page, which already
says "About MIT Rap Club". **Done in Cycle 1**
(`feature/mitrapclub-branding-fixes`): added `SiteConfig::displayName()`
and switched the 6 pure-display call sites to it, leaving the 6
identifier/filename call sites on `siteName()`.
- [x] `permittedThemes` still offers all 13 generic app themes (light, dark,
console, lcd, chicago, vapor, forge, sticker, arena, thermal,
whitehot, word97, auto) alongside `mitrapclub` — decide whether
visitors should be able to switch away from the club's own look at
all (qdb keeps the same breadth; chouse does too). **Resolved in
Cycle 3** (`feature/mitrapclub-structural-identity`, merged to
`main`) as a no-op: kept full breadth, matching `qdb`/`chouse`
precedent.
- [x] About page's "A continuous social graph" and "How participation
works" sections are hardcoded, identical on every profile, and
describe the app's OpenPGP identity/vouching system in infra-level
language — not club-voiced, not gated by any presentation slot.
**Done in Cycle 2** (`feature/mitrapclub-about-copy`): rewritten
club-voiced ("Built on who vouches for whom" / "Anyone can watch,
members hold the mic") for `mitrapclub`, with the same functional
links preserved; other profiles unchanged.
- [x] About page's "Portable by design" section (Backup / API / llms.txt)
is generic developer-facing content, unlikely to matter to a visiting
fan or member. **Done in Cycle 2**: rewritten club-voiced ("Nothing
here lives or dies with one server"), same links preserved.
- [x] Nav labels (Board / About / Users / Tools / Account) are the shared
forum vocabulary. Precedent exists for a fully custom nav: qdb's
`QdbPresentation::navigation()` replaces this entirely (Welcome /
Latest / Top / 1337 / Random / Add Quote / Search), with Account/
Invite deliberately dropped from the nav but still reachable by
direct URL. **Done in Cycle 3** (`feature/mitrapclub-structural-identity`):
`mitrapclub` keeps the generic forum nav/routes (no qdb-style bespoke
nav), just drops `Tools` from the visible list via a new `toolsNav`
presentation slot; `/tools/` stays reachable by direct URL.
- [x] `/tools/` page (backup downloads, SQLite viewer, bookmarklets, etc.)
is generic site-operator tooling — decide whether it belongs in the
club's nav at all, or should just stay reachable by direct link
(same pattern qdb already uses for Account/Invite). **Done in Cycle 3**:
resolved together with the nav item above.
- [x] Favicon (`public/favicon.ico`) and the PWA manifest's `theme_color`/
`background_color`/`icons` (`BrowserRuntimeAssetRenderer::manifest()`)
are hardcoded and shared across every profile — the browser tab icon
and installed-app icon aren't club-branded, even though the manifest
`name`/`short_name` already are. **Done in Cycle 3**: added a real
per-profile favicon/icon override mechanism (`favicon` presentation
slot + `FaviconRegistry`); `mitrapclub` is wired to
`public/assets/favicon-mitrapclub.ico`, currently a byte-identical
copy of the shared default since no club icon asset exists yet —
swapping in real art later is a data change, not new plumbing.
- [x] Busy-page copy ("Temporarily Busy") is the generic boston/qdb
fallback text, not club-voiced. **Done in Cycle 1**
(`feature/mitrapclub-branding-fixes`): now "The Cypher's Full" /
"The mic's getting passed around right now. Try again in a moment."
for `mitrapclub` only.
- [x] The fixed sentence in `ProfilePresentationContent::about()`
("... is a small forum for people who want a more durable local
internet...") is still generic infra copy, shared verbatim by every
profile — flagged and accepted as v1 scope in the last feature; worth
revisiting now that we're extending further. **Done in Cycle 2**:
intro is now per-profile (`introText`); `mitrapclub` reads "...is
where the rhymes get studied as hard as they get spit..."; other
profiles byte-identical (confirmed by diff).
Extend the theme (visual identity)
- [ ] Current `theme-mitrapclub.css` only overrides CSS color variables and
a handful of selectors (site-header, eyebrow, card, nav-link,
inputs/buttons) — no distinctive visual motif like cypherposium.com's
Polaroid-photo-in-a-chalk-cube imagery or chalk-style display type.
- [ ] No hero/banner treatment on the board page — cypherposium.com leads
with a bold graphic headline ("what is rap?") plus two event CTAs;
the current board is just the shared layout with new colors.
- [ ] No club-specific display typeface for headings (only a body/UI font
swap today).
- [ ] No visual distinction between post types on the board — an event
announcement thread looks identical to an ordinary discussion thread.
Club-specific features (new capability, not just skin)
- [x] Event posts have no structured presentation (date/location/RSVP
link) — still plain threads. This is the deferred "Option B" from
the original feature's Step 1 (a qdb-style dedicated events
experience); worth a fresh Step 1 decision now that there's appetite
to extend. **Done in Cycle 4** (`feature/mitrapclub-events-experience`):
took the lighter-weight route instead of a full dedicated experience —
optional structured date/location/link on a thread's root post,
rendered as a distinct block on the board and thread page. A full
qdb-style events experience remains available later if this proves
insufficient.
- [x] No embedded media support for YouTube clips or Instagram posts inside
threads, even though the club's actual content is video/photo-heavy.
**Done** (Cycle 5, `feature/mitrapclub-media-embeds`,
`mitrapclub_media_embeds_*`): a YouTube/Instagram URL in a post body
renders as a small local card (no raw third-party HTML), with a
narrow per-provider tracking-param strip on the card's displayed
URL. Shipped behind a new feature flag (`FORUM_MEDIA_EMBEDS_ENABLED`)
defaulted off everywhere; still needs a manual operator enable on the
production `mitrapclub` instance before it's visible there.
**Extended in Cycle 6** (`feature/mitrapclub-media-embeds-inline-player`,
`mitrapclub_media_embeds_inline_player_*`): the card's "no embedded
video" limitation was raised directly by the user after this shipped.
YouTube now plays inline behind a collapsed, click-to-load expando
(a sandboxed `youtube-nocookie.com` iframe, zero new outbound network
dependency); Instagram gets a cached thumbnail/title preview instead
of staying a bare link, fetched by parsing the public post page's
`og:title`/`og:image` tags (Instagram's oEmbed API dropped those
fields in late 2025) via a client-triggered beacon so no page render
ever blocks on it. Gated behind a second flag
(`FORUM_MEDIA_EMBEDS_INLINE_PLAYER_ENABLED`, depends on
`FORUM_MEDIA_EMBEDS_ENABLED`), defaulted off; also needs a manual
operator enable before it's visible anywhere.
- [ ] Continue media-provider expansion. **Spotify tracks/albums/playlists
and direct TikTok videos are implemented** on
`feature/top-five-media-embeds`, alongside YouTube Shorts; see the
[support checklist](media_embed_support_checklist.md). Remaining
candidates are unscheduled: **SoundCloud** and **Bandcamp** have
higher priority for the club's rap/hip-hop content than **X/Twitter**,
**Facebook**, **Vimeo**, and **Twitch**. Redirecting share links
remain a separate follow-up.
- [ ] No "what's next" callout/banner on the board distinct from the
generic thread list (cypherposium.com's two event CTAs are the
closest reference).
- [ ] Composer has a club-flavored placeholder prompt, but no guided post
type (announce an event vs. share a clip vs. start a discussion).
- [ ] About page mentions Code Cypher / CMS/W / Lupe Fiasco's rap-theory
lineage in two sentences — could expand with more of the Step 1
research now that there's room for it.
- [ ] Social links only appear on `/about/` — consider surfacing them in
the header or footer too.
- [ ] The Facebook link still points at a single photo post, not a
confirmed club page URL — needs the real page URL once known.
Cycle split
Grouped into five FDP cycles by what moves together and how much solution uncertainty each has:
Branding fixes —
displayNamevsnamein the header, busy-page copy. Small, low-uncertainty; skipped Step 1, went straight to Step 2. Done onfeature/mitrapclub-branding-fixes, merged tomain. Per-profile favicon/manifest icon moved to Cycle 3 (no icon asset available yet); the Facebook link swap is unscheduled until the club's real page URL is known.De-genericize copy — gate/rewrite the about page's social-graph, participation, and portable sections plus the fixed intro sentence; expand the Code Cypher/CMS/W paragraph. Low-uncertainty, mostly content. Done on
feature/mitrapclub-about-copy, merged tomain, except the Code Cypher/CMS/W paragraph expansion, which stayed out of scope (communityParagraphsuntouched) and remains open below.Visual identity extension — hero/banner, display typeface, nav relabel/trim, theme-freedom decision. Real design trade-offs; run a Step 1 first. Split via that Step 1 into a structural half (done) and a creative half (deferred):
Structural half — nav trim, theme-freedom decision, favicon/icon override mechanism. Done on
feature/mitrapclub-structural-identity, merged tomain. Theme-freedom resolved as a no-op: kept fullpermittedThemesbreadth, matchingqdb/chouseprecedent.Creative half — hero/banner treatment, display typeface. Unscheduled; deserves its own Step 1 once there's room to actually design it, per the structural feature's Step 1 recommendation (Option B).
Events experience — structured event posts (date/location), a "what's next" callout. This is Option B deferred from the original feature's Step 1; give it a fresh Step 1. Done on
feature/mitrapclub-events-experience, merged tomain: a thread's root post can carry an optional event date/location/link (canonical record headers → write-path validation → read model → a structured event block on the board card and thread page, for any profile, not justmitrapclub). A dedicated "what's next" callout distinct from the thread list stayed out of scope — still open below.Media embeds — YouTube/Instagram embedding in threads. Independent of events; has real technical options (oEmbed fetch vs. link preview vs. iframe); give it a Step 1. Done on
feature/mitrapclub-media-embeds, merged tomain: Step 1 picked the link-preview card (no raw third-party HTML); shipped a YouTube/Instagram URL detector, a card-aware body renderer (byte-identical output when disabled), wired into the one shared body-rendering closure used by all 8 post/reply render call sites, behind a newFORUM_MEDIA_EMBEDS_ENABLEDflag defaulted off everywhere. A broader provider list (SoundCloud, TikTok, Spotify, Bandcamp, etc.) and general-purpose URL canonicalization both stayed out of scope — see the open item above andtodo.txt. Extended in Cycle 6 onfeature/mitrapclub-media-embeds-inline-player, merged tomain, prompted directly by the user noticing the card never actually played anything: Step 1 picked a sandboxedyoutube-nocookie.comiframe behind a collapsed expando for YouTube (zero new outbound dependency) paired with a cached thumbnail/title preview for Instagram. Mid-Step-1, Instagram's oEmbed turned out to have dropped the thumbnail/title fields it would have needed (changed in late 2025), so the approved mechanism became parsing the public post page'sog:title/og:imagetags instead — triggered by a client-side beacon hitting a new, deliberately unauthenticated but abuse-bounded internal endpoint, so no page render ever blocks on the fetch. Gated behind a second flag,FORUM_MEDIA_EMBEDS_INLINE_PLAYER_ENABLED(depends onFORUM_MEDIA_EMBEDS_ENABLED), defaulted off everywhere. Extended in Cycle 7 onfeature/mitrapclub-media-embeds-fetched-titles, merged tomain, prompted by a bug report: a thread with no subject whose body is a bare recognized media URL showed the raw URL as its "title" and silently lost its embed to unrelated title/body duplicate-content suppression. Rather than patch that collision at render time, the fix fetches the linked content's real title (YouTube's keyless oEmbed; Instagram's existing page-scraper) and stores it as the thread's ownsubjectvia a new append-onlyThreadSubjectRecord(mirroringThreadLabelRecord's proven shape) and a narrow, system-onlyLocalWriteService::setThreadSubjectIfEmpty()write path — never overwriting an existing subject, human-provided or otherwise, so it stays congruent with a possible future manual-retitle feature without building anything that feature doesn't need yet. Until the fetch succeeds, such a thread shows the literal string"Untitled"instead of the raw link, closing the race/collision window completely. Reuses Cycle 6'sMediaEmbedPreviewCacheStore/warm-cache beacon endpoint unchanged in shape, extended for ayoutubeprovider and an optionalthread_idto trigger the subject write-back. Gated behind the existingFORUM_MEDIA_EMBEDS_ENABLEDflag alone (not the inline-player flag), since this is a thread-title concern independent of how the embed itself renders. Extended by top-five media support onfeature/top-five-media-embeds: YouTube Shorts, Spotify track/album/playlist players, and direct TikTok video players share the existing flags and click-to-load behavior. Source links remain visible outside players; Instagram previews are responsive. Spotify/TikTok use body-derived titles and retain embeds despite title/body duplicate suppression. See the implementation summary for live-provider, shared-view, theme, and regression verification.