# frozen_string_literal: true
# name: community-landing
# about: Branded public landing page for unauthenticated visitors
# version: 2.0.0
# authors: Community
# url: https://github.com/community/community-landing
enabled_site_setting :community_landing_enabled
after_initialize do
module ::CommunityLanding
PLUGIN_NAME = "community-landing"
PLUGIN_DIR = File.expand_path("..", __FILE__)
end
class ::CommunityLanding::LandingController < ::ApplicationController
requires_plugin CommunityLanding::PLUGIN_NAME
skip_before_action :check_xhr
skip_before_action :redirect_to_login_if_required
skip_before_action :preload_json, raise: false
content_security_policy false
def index
fetch_community_data
css = load_file("assets", "stylesheets", "community_landing", "landing.css")
js = load_file("assets", "javascripts", "community_landing", "landing.js")
base_url = Discourse.base_url
csp = "default-src 'self' #{base_url}; " \
"script-src 'self' 'unsafe-inline'; " \
"style-src 'self' 'unsafe-inline'; " \
"img-src 'self' #{base_url} data: https:; " \
"font-src 'self' #{base_url}; " \
"frame-ancestors 'self'"
response.headers["Content-Security-Policy"] = csp
render html: build_html(css, js).html_safe, layout: false, content_type: "text/html"
end
private
def load_file(*path_parts)
File.read(File.join(CommunityLanding::PLUGIN_DIR, *path_parts))
rescue StandardError => e
"/* Error loading #{path_parts.last}: #{e.message} */"
end
def fetch_community_data
s = SiteSetting
if s.community_landing_contributors_enabled
@top_contributors = User
.joins(:posts)
.where(posts: { created_at: s.community_landing_contributors_days.days.ago.. })
.where.not(username: %w[system discobot])
.where(active: true, staged: false)
.group("users.id")
.order("COUNT(posts.id) DESC")
.limit(s.community_landing_contributors_count)
.select("users.*, COUNT(posts.id) AS post_count")
end
if s.community_landing_groups_enabled
@groups = Group
.where(visibility_level: Group.visibility_levels[:public])
.where(automatic: false)
.limit(s.community_landing_groups_count)
end
if s.community_landing_topics_enabled
@hot_topics = Topic
.listable_topics
.where(visible: true)
.where("topics.created_at > ?", 30.days.ago)
.order(posts_count: :desc)
.limit(s.community_landing_topics_count)
.includes(:category, :user)
end
chat_count = 0
begin
chat_count = Chat::Message.count if defined?(Chat::Message)
rescue
chat_count = 0
end
@stats = {
members: User.real.count,
topics: Topic.listable_topics.count,
posts: Post.where(user_deleted: false).count,
likes: Post.sum(:like_count),
chats: chat_count,
}
end
def e(text)
ERB::Util.html_escape(text.to_s)
end
# ── SVG Icons ──
SUN_SVG = ''
MOON_SVG = ''
QUOTE_SVG = ''
STAT_MEMBERS_SVG = ''
STAT_TOPICS_SVG = ''
STAT_POSTS_SVG = ''
STAT_LIKES_SVG = ''
STAT_CHATS_SVG = ''
COMMENT_SVG = ''
HEART_SVG = ''
IOS_BADGE_SVG = ''
ANDROID_BADGE_SVG = ''
def hex_to_rgb(hex)
hex = hex.to_s.gsub("#", "")
return "0, 0, 0" unless hex.match?(/\A[0-9a-fA-F]{6}\z/)
"#{hex[0..1].to_i(16)}, #{hex[2..3].to_i(16)}, #{hex[4..5].to_i(16)}"
end
def build_color_overrides(s)
accent = s.community_landing_accent_color.presence || "#d4a24e"
accent_hover = s.community_landing_accent_hover_color.presence || "#c4922e"
dark_bg = s.community_landing_dark_bg_color.presence || "#06060f"
light_bg = s.community_landing_light_bg_color.presence || "#faf6f0"
stat_icon_color = s.community_landing_stat_icon_color.presence || accent
about_grad_start = s.community_landing_about_gradient_start.presence || "#fdf6ec"
about_grad_end = s.community_landing_about_gradient_end.presence || "#fef9f0"
app_grad_start = s.community_landing_app_cta_gradient_start.presence || accent
app_grad_end = s.community_landing_app_cta_gradient_end.presence || accent_hover
accent_rgb = hex_to_rgb(accent)
"\n"
end
# ── Logo helpers ──
def logo_img(url, alt, css_class, height)
""
end
def render_logo(dark_url, light_url, site_name, base_class, height)
if dark_url && light_url
logo_img(dark_url, site_name, "#{base_class} cl-logo--dark", height) +
logo_img(light_url, site_name, "#{base_class} cl-logo--light", height)
else
logo_img(dark_url || light_url, site_name, base_class, height)
end
end
# ── App badge helper ──
def render_app_badge(store_url, icon_svg, label, badge_h, badge_style)
style_class = case badge_style
when "pill" then "cl-app-badge--pill"
when "square" then "cl-app-badge--square"
else "cl-app-badge--rounded"
end
"" \
"#{icon_svg}" \
"#{e(label)}" \
"\n"
end
def build_html(css, js)
s = SiteSetting
site_name = s.title
login_url = "/login"
# Logo URLs
logo_dark_url = s.community_landing_logo_dark_url.presence
logo_light_url = s.community_landing_logo_light_url.presence
if logo_dark_url.nil? && logo_light_url.nil?
fallback = s.respond_to?(:logo_url) ? s.logo_url.presence : nil
logo_dark_url = fallback
end
has_logo = logo_dark_url.present? || logo_light_url.present?
logo_h = s.community_landing_logo_height rescue 30
og_logo = logo_dark_url || logo_light_url
footer_logo_url = s.community_landing_footer_logo_url.presence
html = +""
html << "\n\n
#{e(s.community_landing_hero_subtitle)}
\n" primary_label = s.community_landing_hero_primary_button_label.presence || "View Latest Topics" primary_url = s.community_landing_hero_primary_button_url.presence || "/latest" secondary_label = s.community_landing_hero_secondary_button_label.presence || "Explore Our Spaces" secondary_url = s.community_landing_hero_secondary_button_url.presence || login_url html << "#{e(s.community_landing_app_cta_subtext)}
\n" if s.community_landing_app_cta_subtext.present? html << "