Champion Portal Development Sub-Phase 3.9
Status: ✅ Complete (January 2026)
Estimated Effort: 2-3 days
Focus: Public preview pages for events, discussions, and communities to support rich link sharingPrerequisites: Phase 3.5 Complete (Discussion Boards)
Related Documents:
- ../../JOBS-TO-BE-DONE.md — Job C9: Feel Like I Belong
- ../../development/DESIGN-GUIDELINES.md — Warm, welcoming design
What Was Implemented:
Cp::EventsController — Public access for show action with public_event_view? methodCp::BoardsController — Public access for show action with public_post_view? methodCp::CommunitiesController — Enhanced public view with @public_stats for non-logged-in usersCp::BaseController — Added store_return_path and stored_return_path methodsCp::HelpController — Made publicly accessible (moved Help link to footer)_public_cta_box.html.erb — NEW: Reusable CTA component with warm, encouraging copy
:event, :discussion, :community content typesmax-w-4xl mx-auto wrapper for consistent widthevents/show.html.erb — Public view hides RSVP, stats footer, trackingboards/show.html.erb — Public view hides comments, reactions, author actionscommunities/show.html.erb — “What’s Happening Here” and “Why Join” sections for non-membersMetaTagsHelper — NEW: Dynamic Open Graph and Twitter Card meta tags for rich link previewsCp::Champion#public_display_name — Returns “FirstName L.” format for privacy| File | Change |
|——|——–|
| app/views/cp/shared/_public_cta_box.html.erb | NEW - Reusable CTA component |
| app/helpers/meta_tags_helper.rb | NEW - Rich preview meta tags |
| app/controllers/cp/base_controller.rb | Return path storage |
| app/controllers/cp/events_controller.rb | Public access for global/public events |
| app/controllers/cp/boards_controller.rb | Public access for public community posts |
| app/controllers/cp/communities_controller.rb | Public stats and landing page sections |
| app/models/cp/champion.rb | public_display_name method |
max-w-4xl (896px) width across all pagesWhen Champions share links to events, discussions, or communities via iMessage/SMS, the recipient sees a rich preview (via Open Graph meta tags implemented in the meta_tags_helper). However, clicking through currently requires authentication before seeing any content.
This phase creates public “landing pages” that:
Goal: Convert link clicks into signups by showing value before asking for commitment.
| Recipient sees rich preview: “Spring Brunch | May 15, 2025 | Nashville” |
From JOBS-TO-BE-DONE.md:
Job C9: Feel Like I Belong “When I’m far from Nashville or years past graduation, I want to feel part of something bigger.”
Public landing pages extend the sense of belonging to potential Champions, showing them the vibrant community they could join.
| Feature | Description |
|---|---|
| Public Event Pages | View event details without auth (for global or public community events) |
| Public Discussion Posts | View post content without auth (for public community posts) |
| Enhanced Public Community Pages | Show activity metrics (member count, post count, event count) |
| Reusable CTA Component | Warm, encouraging “Join” box with Belmont language |
| Return Path Storage | Store intended URL, redirect after signup/login |
| Feature | Reason |
|---|---|
| Public access to private communities | Privacy by design |
| Comments visible on public posts | Encourage joining to see conversation |
| RSVP functionality on public events | Incentive to join |
| Member list details on public communities | Privacy protection |
| Content Type | When Public |
|---|---|
| Events | Global events (show_globally: true) OR events in public communities |
| Discussion Posts | Posts in public communities only |
| Communities | Communities with visibility: :public OR accessed with ref param |
Modify Cp::EventsController to allow public access to show action for qualifying events:
# app/controllers/cp/events_controller.rb
before_action :authenticate_cp_champion!, except: [:show]
before_action :require_authentication_or_public, only: [:show]
private
def require_authentication_or_public
return if public_event_view?
authenticate_cp_champion!
end
def public_event_view?
return false unless @event
# Global events are always public
return true if @event.show_globally?
# Events in public communities are public
@event.communities.any?(&:visibility_public?)
end
Modify Cp::BoardsController to allow public access to show action for qualifying posts:
# app/controllers/cp/boards_controller.rb
before_action :authenticate_cp_champion!, except: [:show]
before_action :require_authentication_or_public, only: [:show]
private
def require_authentication_or_public
return if public_post_view?
authenticate_cp_champion!
end
def public_post_view?
return false unless @community
@community.visibility_public?
end
Already supports public access. Enhance to load activity metrics for public view:
# In show action, add for public view:
if !@is_logged_in
@public_stats = {
member_count: @community.champions.verified.count,
post_count: @community.board_posts.active.count,
event_count: @community.events.published.upcoming.count
}
end
Add to ApplicationController or Cp::BaseController:
# Store the intended destination before redirecting to login
def store_return_path
session[:return_to] = request.original_url if request.get?
end
# Retrieve and clear the stored path
def stored_return_path
session.delete(:return_to)
end
Modify authentication redirect in Devise configuration or controller:
# After successful sign in
def after_sign_in_path_for(resource)
stored_return_path || super
end
Views will check cp_champion_signed_in? to show/hide content:
<% if cp_champion_signed_in? %>
<!-- Full interactive content -->
<% else %>
<!-- Public preview + CTA -->
<%= render "cp/shared/public_cta_box", content_type: :event, community: @event.communities.first %>
<% end %>
File: app/helpers/meta_tags_helper.rb
Rich link previews (iMessage, SMS, Slack, Facebook, LinkedIn) require Open Graph and Twitter Card meta tags. The MetaTagsHelper provides a centralized system for dynamic meta tag generation.
| Tag | Default Value |
|---|---|
| Title | “Belmont Alumni Champions” |
| Description | “Once a Bruin, Always Connected.” |
| Image | https://alumnichampions.com/meta-image-champions.png |
| Type | “website” |
# Generic meta tag setter - use in any view
set_meta_tags(
title: "My Page Title",
description: "Page description for previews",
image: url_for(@image),
url: request.original_url,
type: "website"
)
# Render in layout <head>
<%= render_meta_tags %>
| Method | Content Type | Description |
|---|---|---|
set_community_meta_tags(community, inviter) |
Community | Uses hero/thumbnail image, member count |
set_event_meta_tags(event) |
Event | Date, location, cover image, type=”event” |
set_board_post_meta_tags(post, community) |
Discussion | Post preview, community context |
set_invite_meta_tags(inviter, community) |
Invite Link | Personalized invite from Champion |
set_news_post_meta_tags(post) |
News | Article preview, type=”article” |
set_roadmap_meta_tags |
Roadmap | Development roadmap page |
<!-- Open Graph tags -->
<meta property="og:title" content="Spring Brunch | Belmont Alumni Champions">
<meta property="og:description" content="May 15, 2025 • Café Lula, Nashville • Join us for a casual brunch...">
<meta property="og:image" content="https://alumnichampions.com/rails/active_storage/blobs/.../event-cover.jpg">
<meta property="og:url" content="https://alumnichampions.com/events/spring-brunch-2025">
<meta property="og:type" content="event">
<!-- Twitter Card tags -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Spring Brunch | Belmont Alumni Champions">
<meta name="twitter:description" content="May 15, 2025 • Café Lula, Nashville • Join us for a casual brunch...">
<meta name="twitter:image" content="https://alumnichampions.com/rails/active_storage/blobs/.../event-cover.jpg">
Events:
{Event Title} | Belmont Alumni Champions{Date} • {Location} • {Short description}eventCommunities:
{Community Name} | Belmont Alumni Champions{Inviter} invites you to join {Community}Join {N} Champions in {Name}Discussion Posts:
{Post Title (60 chars)} | {Community Name}Join the discussion! {Post preview (140 chars)}Invite Links:
{Inviter Name} invites you to join Belmont Alumni ChampionsBoth Champion Portal layouts include meta tags:
<!-- app/views/layouts/champions.html.erb -->
<head>
<%= render_meta_tags %>
...
</head>
<!-- app/views/layouts/public.html.erb -->
<head>
<%= render_meta_tags %>
...
</head>
<%# Event show page %>
<% set_event_meta_tags(@event) %>
<%# Community page %>
<% set_community_meta_tags(@community) %>
<%# Discussion post %>
<% set_board_post_meta_tags(@post, @community) %>
<%# Signup page with invite context %>
<% set_invite_meta_tags(@inviter, @community) %>
rails_blob_url for ActiveStorage images with only_path: falseVisible:
Hidden:
Layout:
┌─────────────────────────────────────────────────────────────────────┐
│ [Cover Image - full width] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Spring Brunch with Nashville Champions │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ │
│ 📅 Saturday, May 15, 2025 • 10:00 AM CT │
│ 📍 Café Lula, Nashville, TN │
│ 👥 Hosted by Nashville Champions │
│ │
│ Join us for a casual brunch to connect with fellow Bruins in │
│ Nashville! We'll meet at Café Lula on 12 South for good food │
│ and great conversation... │
│ │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ╭─────────────────────────────────────────────────────────────╮ │
│ │ 🎓 Want to RSVP? │ │
│ │ │ │
│ │ Join the Belmont Alumni Champions community to RSVP for │ │
│ │ this event and connect with fellow Bruins. │ │
│ │ │ │
│ │ [Join Alumni Champions] [Already a member? Sign in] │ │
│ ╰─────────────────────────────────────────────────────────────╯ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Visible:
Hidden:
Layout:
┌─────────────────────────────────────────────────────────────────────┐
│ ← Back to Nashville Champions │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Best coffee shops for working remotely? │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ │
│ Posted by Sarah C. • January 15, 2026 │
│ │
│ Hey Nashville Bruins! I'm looking for recommendations for │
│ coffee shops with good wifi and space to work. I usually │
│ work from home but want to mix it up sometimes... │
│ │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ╭─────────────────────────────────────────────────────────────╮ │
│ │ 💬 Join the conversation! │ │
│ │ │ │
│ │ This discussion has 12 replies from fellow Champions. │ │
│ │ Join to see comments and share your thoughts. │ │
│ │ │ │
│ │ [Join Alumni Champions] [Already a member? Sign in] │ │
│ ╰─────────────────────────────────────────────────────────────╯ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Visible:
Hidden:
Layout:
┌─────────────────────────────────────────────────────────────────────┐
│ [Hero Image - Nashville skyline] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Nashville Champions │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ │
│ Connect with fellow Bruins in Nashville! Join us for local │
│ events, discussions, and community building. │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 👥 45 │ │ 💬 23 │ │ 📅 3 │ │
│ │ Champions │ │ Discussions │ │ Events │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ╭─────────────────────────────────────────────────────────────╮ │
│ │ 🏠 Find your Belmont home in Nashville │ │
│ │ │ │
│ │ Join 45 fellow Bruins who are staying connected, │ │
│ │ attending events, and building community together. │ │
│ │ │ │
│ │ [Join Nashville Champions] [Already a member? Sign in] │ │
│ ╰─────────────────────────────────────────────────────────────╯ │
│ │
└─────────────────────────────────────────────────────────────────────┘
File: app/views/cp/shared/_public_cta_box.html.erb
Props:
content_type — :event, :discussion, :communitycommunity — The associated community (for context)comment_count — For discussions, how many replies (optional)Copy by Content Type:
| Type | Headline | Body |
|---|---|---|
| Event | “🎓 Want to RSVP?” | “Join the Belmont Alumni Champions community to RSVP for this event and connect with fellow Bruins.” |
| Discussion | “💬 Join the conversation!” | “This discussion has {N} replies from fellow Champions. Join to see comments and share your thoughts.” |
| Community | “🏠 Find your Belmont home in {City}” | “Join {N} fellow Bruins who are staying connected, attending events, and building community together.” |
Buttons:
/signup?community={slug} (if community context)/loginStyling:
Cp::EventsController — Add public access for show action
require_authentication_or_public before_actionpublic_event_view? private method@is_public_view flag for view conditionalsCp::BoardsController — Add public access for show action
require_authentication_or_public before_actionpublic_post_view? private method@is_public_view flag for view conditionalsCp::CommunitiesController — Enhance public view
@public_stats for non-logged-in usersCp::BaseController or ApplicationController
store_return_path methodstored_return_path methodCp::SessionsController or Devise config
after_sign_in_path_for to use stored pathCp::RegistrationsController
after_sign_up_path_for to use stored pathapp/views/cp/shared/_public_cta_box.html.erb — Reusable CTA component
content_type, community, comment_count paramsapp/views/cp/events/show.html.erb — Add public view conditional
@is_public_view@is_public_viewapp/views/cp/boards/show.html.erb — Add public view conditional
@is_public_view@is_public_viewapp/views/cp/communities/show.html.erb — Enhance public view
@is_public_view@is_public_viewCp::Champion or helper — Add public_display_name method
# test/controllers/cp/events_controller_test.rb
test "show allows public access for global event" do
# No sign in
get cp_event_url(@global_event)
assert_response :success
end
test "show allows public access for event in public community" do
get cp_event_url(@public_community_event)
assert_response :success
end
test "show requires auth for event in private community" do
get cp_event_url(@private_community_event)
assert_redirected_to new_cp_champion_session_path
end
test "show stores return path for non-auth users" do
get cp_event_url(@global_event)
assert_equal cp_event_url(@global_event), session[:return_to]
end
# test/controllers/cp/boards_controller_test.rb
test "show allows public access for post in public community" do
get cp_board_post_url(@public_community, @post)
assert_response :success
end
test "show requires auth for post in private community" do
get cp_board_post_url(@private_community, @post)
assert_redirected_to new_cp_champion_session_path
end
test "show hides comments for non-auth users" do
get cp_board_post_url(@public_community, @post)
assert_select ".comments-section", count: 0
end
# test/helpers/champion_helper_test.rb
test "public_display_name returns first name and last initial" do
champion = cp_champions(:sarah_champion)
champion.update!(first_name: "Sarah", last_name: "Champion")
assert_equal "Sarah C.", champion.public_display_name
end
# test/integration/public_landing_pages_test.rb
test "non-auth user can view public event and sign up" do
# View event without auth
get cp_event_url(@global_event)
assert_response :success
assert_select ".public-cta-box"
# Click "Join" should preserve return path
# (Full flow would be tested in system test)
end
| Question | Decision |
|---|---|
| Event visibility | Global events OR events in public communities |
| Discussion visibility | Posts in public communities only |
| Author display | First name + last initial (e.g., “Sarah C.”) |
| CTA destination | Use existing flow: /signup?community=slug with session-based return path |
| File | Purpose |
|---|---|
app/views/cp/shared/_public_cta_box.html.erb |
Reusable CTA component |
test/integration/public_landing_pages_test.rb |
Integration tests |
| File | Changes |
|---|---|
app/controllers/cp/events_controller.rb |
Add public access logic |
app/controllers/cp/boards_controller.rb |
Add public access logic |
app/controllers/cp/communities_controller.rb |
Add public stats loading |
app/controllers/cp/base_controller.rb |
Add return path methods |
app/views/cp/events/show.html.erb |
Add public view conditional |
app/views/cp/boards/show.html.erb |
Add public view conditional |
app/views/cp/communities/show.html.erb |
Add activity metrics |
app/models/cp/champion.rb |
Add public_display_name method |
test/controllers/cp/events_controller_test.rb |
Add public access tests |
test/controllers/cp/boards_controller_test.rb |
Add public access tests |