Canonical sources: Portal philosophy, posture, and language live in
/docs/planning/champion-portal/source/README.md.
Use these sources (includingCHRIST_CENTERED__IDENTITY_STATEMENT.md) when writing specs or user-facing copy. Prefer quoting/paraphrasing over inventing new language.
Status: ✅ Complete
Estimated Effort: 3-4 days
Dependencies: Phase 9.1 (Community Seed Data)
Related Docs: 9.1-community-seed-data.md, DESIGN-GUIDELINES.md
When a Champion joins a community, they should receive a warm, informative welcome that explains what the community is about and suggests initial actions. This welcome content appears as an “About” panel on the community landing page and can be dismissed after a period or manually hidden.
We considered using the existing News system for welcome content but determined it’s the wrong fit:
| Aspect | News | Community About Panel |
|---|---|---|
| Purpose | Time-sensitive announcements | Static welcome/orientation |
| Lifecycle | Publishes → ages → archives | Persistent until dismissed |
| Visibility | Appears in feeds, multiple places | Shows once on landing page |
| Repetition | Would appear in every community’s news | One welcome per community |
| Content type | “What’s happening” | “What is this place” |
Decision: Welcome content lives as a dedicated “About” panel on the community landing page, extending the existing display_description pattern.
Add ActionText rich text to communities:
# app/models/cp/community.rb
has_rich_text :welcome_content
No migration needed for ActionText—it uses the existing action_text_rich_texts table.
Store templates in YAML, organized by community type and (for affinities) by category:
# config/welcome_packs.yml
district:
- title: "Welcome to {{district_name}}!"
body: |
You've found your local Bruin community! Here in {{district_name}},
Champions gather for coffee meetups, networking events, and watch parties.
**A few ideas to get started:**
- Browse the directory to see who else is nearby
- Check upcoming events in the area
- Introduce yourself in the discussion board
We're glad you're here. 🎉
- title: "{{district_name}} Champions"
body: |
Welcome to the {{district_name}} chapter of Alumni Champions!
This is your home base for connecting with fellow Bruins in the area.
Whether you're new to town or have been here for years, there's always
room at the table.
**Quick actions:**
- Say hello in Discussions
- Find Champions near you
- See what events are coming up
college:
- title: "Welcome, {{college_name}} Alum!"
body: |
You're joining a community of fellow {{college_name}} graduates.
This space is for staying connected to your college, sharing updates,
and helping current students navigate their Belmont journey.
**Get started:**
- Connect with classmates in the directory
- Share career updates or advice
- Keep up with college news
major:
- title: "{{major_name}} Alumni"
body: |
Welcome to the {{major_name}} alumni community!
Whether you're using your degree every day or took a different path,
you share something with everyone here—the experience of studying
{{major_name}} at Belmont.
**Jump in:**
- See who else studied {{major_name}}
- Share job opportunities or career advice
- Reconnect with old classmates
industry:
- title: "{{industry_name}} Professionals"
body: |
You've joined a community of Belmont alumni working in {{industry_name}}.
This is a great place to network, share opportunities, and support
fellow Bruins in your field.
**Get started:**
- Browse professionals in your industry
- Share job leads or advice
- Ask questions and offer expertise
affinity:
# Organized by affinity category (from affinities.category column)
Athletics:
- title: "Welcome, Fellow Bruin Athlete!"
body: |
You're part of the {{affinity_name}} alumni community.
Whether you played, managed, trained, or cheered, your time as a
Bruin athlete shaped who you are. This is your space to stay connected.
**Get involved:**
- Reconnect with teammates
- Follow current team news
- Attend alumni games and events
Greek Life:
- title: "Welcome, {{affinity_name}}!"
body: |
You've joined your chapter's alumni community on the Champion Portal.
Greek life creates bonds that last a lifetime. Use this space to
stay connected with your brothers or sisters.
**Get started:**
- Find chapter alumni in the directory
- Share updates and milestones
- Support current members
Campus Life:
- title: "{{affinity_name}} Alumni"
body: |
Welcome to the {{affinity_name}} alumni community!
Your involvement in {{affinity_name}} was part of what made your
Belmont experience unique. Stay connected with others who shared it.
**Jump in:**
- Reconnect with fellow members
- Share memories and updates
- Support current students
Instrumental Ensembles:
- title: "{{affinity_name}} Alumni"
body: |
Welcome to the {{affinity_name}} alumni network!
Music brought you together at Belmont. This community keeps that
connection alive after graduation.
**Get involved:**
- Find fellow musicians
- Share performance updates
- Stay connected to the ensemble
Vocal Ensembles:
- title: "{{affinity_name}} Alumni"
body: |
Welcome to the {{affinity_name}} community!
Your voice was part of something special at Belmont. Stay connected
with the singers who shared that stage with you.
**Get started:**
- Reconnect with fellow singers
- Share musical updates
- Support current members
Spiritual Life:
- title: "{{affinity_name}} Community"
body: |
Welcome to the {{affinity_name}} alumni community.
Faith and service were central to your Belmont experience. This
space helps you stay connected to others who shared that journey.
**Get involved:**
- Connect with fellow alumni
- Share how your faith journey continues
- Support current students
Geographic:
- title: "{{affinity_name}} Alumni"
body: |
Welcome to the {{affinity_name}} community!
You share a connection through {{affinity_name}}. This is your
space to stay in touch.
**Get started:**
- Find fellow alumni
- Share updates
- Stay connected
Post Graduation:
- title: "{{affinity_name}} Alumni"
body: |
Welcome to the {{affinity_name}} community!
Your post-graduation involvement with Belmont connects you with
others who share that experience.
**Get started:**
- Connect with fellow members
- Share experiences
- Stay involved
default:
- title: "Welcome to {{affinity_name}}!"
body: |
You've joined the {{affinity_name}} alumni community.
This is your space to connect with others who share your
affiliation with {{affinity_name}}.
**Get started:**
- Browse the member directory
- Join the conversation
- Stay connected
custom:
- title: "Welcome to {{community_name}}!"
body: |
You've joined a special community on the Champion Portal.
This space was created for a specific purpose. Look around,
get to know the other members, and dive in!
**Get started:**
- See who else is here
- Check the discussion board
- Introduce yourself
# app/services/cp/welcome_content_generator.rb
module Cp
class WelcomeContentGenerator
PACKS = YAML.load_file(Rails.root.join("config/welcome_packs.yml"))
def initialize(community)
@community = community
end
def generate
pack = select_pack
return nil unless pack
interpolate(pack)
end
private
def select_pack
packs = packs_for_type
return nil if packs.blank?
# Rotate through packs based on community ID for variety
packs[@community.id % packs.length]
end
def packs_for_type
case @community.community_type
when "district", "college", "major", "industry", "custom"
PACKS[@community.community_type]
when "affinity"
affinity_packs
else
[]
end
end
def affinity_packs
# Use the affinity's category to select appropriate welcome pack
category = @community.affinity&.category
if category.present? && PACKS.dig("affinity", category).present?
PACKS.dig("affinity", category)
else
PACKS.dig("affinity", "default") || []
end
end
def interpolate(pack)
title = interpolate_string(pack["title"])
body = interpolate_string(pack["body"])
{ title: title, body: body }
end
def interpolate_string(str)
str.gsub("{{district_name}}", @community.name)
.gsub("{{college_name}}", @community.name)
.gsub("{{major_name}}", @community.name)
.gsub("{{industry_name}}", @community.name)
.gsub("{{affinity_name}}", @community.name)
.gsub("{{community_name}}", @community.name)
end
end
end
# app/models/cp/community.rb
# Add ActionText
has_rich_text :welcome_content
# Generate welcome content on creation
after_create :generate_welcome_content
private
def generate_welcome_content
return if welcome_content.present?
generator = Cp::WelcomeContentGenerator.new(self)
result = generator.generate
return unless result
# Build HTML from title + body
html = <<~HTML
<h3>#{result[:title]}</h3>
#{result[:body].gsub(/\n\n/, '</p><p>').gsub(/\n/, '<br>')}
HTML
update(welcome_content: html)
end
Champions can dismiss welcome content either:
# Migration: add_dismissed_welcome_to_cp_champion_communities
add_column :cp_champion_communities, :welcome_dismissed_at, :datetime
add_column :cp_champion_communities, :community_view_count, :integer, default: 0
# app/models/cp/champion_community.rb
def welcome_dismissed?
welcome_dismissed_at.present? || community_view_count >= 10
end
def dismiss_welcome!
update(welcome_dismissed_at: Time.current)
end
def increment_view_count!
increment!(:community_view_count)
end
Add welcome panel to the community landing page, displayed below the header and above the tabs/content:
<%# app/views/cp/communities/show.html.erb %>
<%# ... header section ... %>
<%# Welcome Content Panel %>
<% if show_welcome_panel?(@community, @membership) %>
<div class="mx-4 sm:mx-0 mb-6" id="welcome-panel">
<div class="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl border border-blue-100 p-6">
<div class="flex items-start justify-between gap-4">
<div class="flex-1 prose prose-sm max-w-none">
<%= @community.welcome_content %>
</div>
<button type="button"
class="flex-shrink-0 text-gray-400 hover:text-gray-600"
data-controller="dismiss-welcome"
data-dismiss-welcome-url-value="<%= dismiss_welcome_cp_community_path(@community) %>"
data-dismiss-welcome-panel-value="welcome-panel"
aria-label="Dismiss welcome message">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="mt-4 pt-4 border-t border-blue-100">
<button type="button"
class="text-sm text-blue-600 hover:text-blue-800 font-medium"
data-controller="dismiss-welcome"
data-dismiss-welcome-url-value="<%= dismiss_welcome_cp_community_path(@community) %>"
data-dismiss-welcome-panel-value="welcome-panel">
Got it, thanks!
</button>
</div>
</div>
</div>
<% end %>
<%# ... rest of page ... %>
# app/helpers/cp/communities_helper.rb
def show_welcome_panel?(community, membership)
return false unless community.welcome_content.present?
return false unless membership.present?
return false if membership.welcome_dismissed?
true
end
// app/javascript/controllers/dismiss_welcome_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = { url: String, panel: String }
connect() {
// Nothing needed on connect
}
dismiss() {
// Hide panel immediately for responsiveness
const panel = document.getElementById(this.panelValue)
if (panel) {
panel.style.opacity = "0"
panel.style.transition = "opacity 0.3s ease-out"
setTimeout(() => panel.remove(), 300)
}
// Persist to server
fetch(this.urlValue, {
method: "POST",
headers: {
"X-CSRF-Token": document.querySelector("[name='csrf-token']").content,
"Accept": "application/json"
}
})
}
}
# app/controllers/cp/communities_controller.rb
def dismiss_welcome
membership = @community.champion_communities.find_by(champion: current_cp_champion)
if membership
membership.dismiss_welcome!
head :ok
else
head :not_found
end
end
# config/routes.rb (within cp namespace)
resources :communities do
post :dismiss_welcome, on: :member
end
Increment view count each time a champion visits their community page:
# app/controllers/cp/communities_controller.rb
def show
@community = Cp::Community.find(params[:id])
@membership = @community.champion_communities.find_by(champion: current_cp_champion)
# Track view for welcome dismissal
@membership&.increment_view_count! unless @membership&.welcome_dismissed?
# ... rest of show action ...
end
Add field to community edit form for staff to customize welcome content:
<%# app/views/champions/communities/edit.html.erb %>
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">
Welcome Content
<span class="text-gray-400 font-normal">(shown to new members)</span>
</label>
<%= form.rich_text_area :welcome_content,
class: "block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" %>
<p class="mt-2 text-sm text-gray-500">
This content appears on the community page for new members.
Leave blank to use the auto-generated welcome message.
</p>
<% if @community.welcome_content.blank? %>
<button type="button"
class="mt-2 text-sm text-blue-600 hover:text-blue-800"
data-action="click->community-form#regenerateWelcome">
Generate default welcome content
</button>
<% end %>
</div>
Add rake task for bulk regeneration:
# lib/tasks/welcome_content.rake
namespace :welcome_content do
desc "Regenerate welcome content for all communities missing it"
task generate_missing: :environment do
count = 0
Cp::Community.active.find_each do |community|
next if community.welcome_content.present?
generator = Cp::WelcomeContentGenerator.new(community)
result = generator.generate
next unless result
html = <<~HTML
<h3>#{result[:title]}</h3>
#{result[:body].gsub(/\n\n/, '</p><p>').gsub(/\n/, '<br>')}
HTML
community.update(welcome_content: html)
count += 1
print "."
end
puts "\nGenerated welcome content for #{count} communities"
end
desc "Preview welcome content for a community type"
task :preview, [:type] => :environment do |t, args|
type = args[:type] || "district"
packs = YAML.load_file(Rails.root.join("config/welcome_packs.yml"))
if type == "affinity"
packs["affinity"].each do |category, templates|
puts "\n=== Affinity: #{category} ==="
templates.each_with_index do |pack, i|
puts "\n--- Template #{i + 1} ---"
puts "Title: #{pack['title']}"
puts pack['body']
end
end
else
templates = packs[type]
templates.each_with_index do |pack, i|
puts "\n--- Template #{i + 1} ---"
puts "Title: #{pack['title']}"
puts pack['body']
end
end
end
end
# test/models/cp/community_welcome_test.rb
require "test_helper"
class Cp::CommunityWelcomeTest < ActiveSupport::TestCase
test "generates welcome content on creation for district" do
community = Cp::Community.create!(
name: "Nashville",
community_type: :district,
active: true
)
assert community.welcome_content.present?
assert_includes community.welcome_content.to_plain_text, "Nashville"
end
test "generates welcome content for affinity using category" do
affinity = affinities(:phi_mu) # Has category: "Greek Life"
community = Cp::Community.create!(
name: affinity.name,
community_type: :affinity,
affinity: affinity,
active: true
)
assert community.welcome_content.present?
# Should use Greek Life template, not default
assert_includes community.welcome_content.to_plain_text, "chapter"
end
test "uses default affinity template when category not found" do
affinity = Affinity.create!(name: "Unknown Group", category: "Nonexistent")
community = Cp::Community.create!(
name: affinity.name,
community_type: :affinity,
affinity: affinity,
active: true
)
assert community.welcome_content.present?
end
test "rotates through packs based on community ID" do
# Create multiple communities to test rotation
communities = 3.times.map do |i|
Cp::Community.create!(
name: "City #{i}",
community_type: :district,
active: true
)
end
# At least 2 should have different content (if multiple packs exist)
contents = communities.map { |c| c.welcome_content.to_plain_text }
# This test assumes district has multiple packs
end
end
# test/models/cp/champion_community_dismissal_test.rb
require "test_helper"
class Cp::ChampionCommunityDismissalTest < ActiveSupport::TestCase
setup do
@membership = cp_champion_communities(:sarah_nashville)
end
test "welcome not dismissed by default" do
refute @membership.welcome_dismissed?
end
test "manual dismissal works" do
@membership.dismiss_welcome!
assert @membership.welcome_dismissed?
assert @membership.welcome_dismissed_at.present?
end
test "auto-dismisses after 3 views" do
refute @membership.welcome_dismissed?
3.times { @membership.increment_view_count! }
assert @membership.welcome_dismissed?
end
test "view count increments" do
assert_equal 0, @membership.community_view_count
@membership.increment_view_count!
assert_equal 1, @membership.community_view_count
end
end
# test/controllers/cp/communities_dismiss_welcome_test.rb
require "test_helper"
class Cp::CommunitiesDismissWelcomeTest < ActionDispatch::IntegrationTest
setup do
@champion = cp_champions(:sarah_champion)
@community = cp_communities(:nashville)
sign_in @champion
end
test "dismisses welcome content" do
membership = @community.champion_communities.find_by(champion: @champion)
assert_not membership.welcome_dismissed?
post dismiss_welcome_cp_community_url(@community)
assert_response :ok
membership.reload
assert membership.welcome_dismissed?
end
test "returns 404 for non-member" do
@community = cp_communities(:atlanta) # Champion not a member
post dismiss_welcome_cp_community_url(@community)
assert_response :not_found
end
end
# test/services/cp/welcome_content_generator_test.rb
require "test_helper"
class Cp::WelcomeContentGeneratorTest < ActiveSupport::TestCase
test "generates content for district community" do
community = Cp::Community.new(
id: 1,
name: "Nashville",
community_type: :district
)
generator = Cp::WelcomeContentGenerator.new(community)
result = generator.generate
assert result.present?
assert result[:title].present?
assert result[:body].present?
assert_includes result[:title], "Nashville"
end
test "generates content for affinity with category" do
affinity = affinities(:phi_mu)
community = Cp::Community.new(
id: 1,
name: affinity.name,
community_type: :affinity,
affinity: affinity
)
generator = Cp::WelcomeContentGenerator.new(community)
result = generator.generate
assert result.present?
assert_includes result[:body], "chapter" # Greek Life template
end
test "interpolates all variable types" do
%w[district college major industry].each do |type|
community = Cp::Community.new(
id: 1,
name: "Test Name",
community_type: type
)
generator = Cp::WelcomeContentGenerator.new(community)
result = generator.generate
refute_includes result[:title], "{{"
refute_includes result[:body], "{{"
end
end
end
| File | Purpose |
|---|---|
config/welcome_packs.yml |
Welcome content templates by community type |
app/services/cp/welcome_content_generator.rb |
Service to generate welcome content |
app/javascript/controllers/dismiss_welcome_controller.js |
Stimulus controller for dismissal |
lib/tasks/welcome_content.rake |
Rake tasks for content management |
db/migrate/xxx_add_welcome_tracking_to_champion_communities.rb |
Migration for dismissal tracking |
test/models/cp/community_welcome_test.rb |
Model tests |
test/models/cp/champion_community_dismissal_test.rb |
Dismissal tracking tests |
test/services/cp/welcome_content_generator_test.rb |
Service tests |
test/controllers/cp/communities_dismiss_welcome_test.rb |
Controller tests |
| File | Changes |
|---|---|
app/models/cp/community.rb |
Add has_rich_text :welcome_content, after_create callback |
app/models/cp/champion_community.rb |
Add dismissal methods |
app/controllers/cp/communities_controller.rb |
Add dismiss_welcome action, view tracking |
app/views/cp/communities/show.html.erb |
Add welcome panel UI |
app/helpers/cp/communities_helper.rb |
Add show_welcome_panel? helper |
config/routes.rb |
Add dismiss_welcome route |
app/views/champions/communities/edit.html.erb |
Add welcome content editor |
The welcome panel should feel:
Not in scope for 9.5, but noted for future: