Customize your Help Center portal to match your brand identity and provide the best experience for your customers. Configure colors, domains, locales, and more.
Portal Configuration
Core Settings
Unique URL identifier for the portal
Browser tab title and meta title tag
Text displayed in the portal header
URL to link back to your main website
Primary brand color (hex code)
Your own domain for hosting the Help Center (must be unique)
Whether the portal is archived (hidden from public)
Schema Overview
class Portal < ApplicationRecord
belongs_to :account
belongs_to :channel_web_widget , optional: true
has_one_attached :logo
has_many :categories
has_many :articles
has_many :inboxes
# Validations
validates :name , presence: true
validates :slug , presence: true , uniqueness: true
validates :custom_domain , uniqueness: true , allow_nil: true
validate :config_json_format
end
Branding
Custom Logo
Upload your brand logo using Active Storage:
Attach Logo
Remove Logo
Logo Data
# Upload logo via blob
blob = ActiveStorage :: Blob . find_signed (blob_id)
portal. logo . attach (blob)
Brand Colors
Set your primary brand color:
portal. update ( color: '#6366F1' ) # Indigo
The portal automatically generates hover states and adjusts colors for contrast:
// From portalThemeHelper.js
export const setPortalHoverColor = theme => {
const resolvedTheme = getResolvedTheme ( theme );
const portalColor = window . portalConfig . portalColor ;
const bgColor = resolvedTheme === 'dark' ? '#151718' : 'white' ;
const hoverColor = adjustColorForContrast ( portalColor , bgColor );
document . documentElement . style . setProperty (
'--dynamic-hover-color' ,
hoverColor
);
};
Colors are automatically adjusted for accessibility in both light and dark themes.
Theme System
Supported Themes
The Help Center supports three theme modes:
Light Default bright theme
Dark Dark theme for reduced eye strain
System Follows user’s OS preference
Theme Switching
Switch Theme
Detect System Theme
Watch System Changes
// From portalThemeHelper.js
export const switchTheme = theme => {
// Update localStorage
if ( theme === 'system' ) {
localStorage . removeItem ( 'theme' );
} else {
localStorage . theme = theme ;
}
const resolvedTheme = getResolvedTheme ( theme );
document . documentElement . classList . remove ( 'dark' , 'light' );
document . documentElement . classList . add ( resolvedTheme );
setPortalHoverColor ( theme );
updateThemeInHeader ( theme );
};
Theme preference is stored in localStorage and persists across visits. The portal respects user preference even when OS theme changes.
Locale Configuration
Setting Default Locale
Configure the portal’s default language:
portal. config [ 'default_locale' ] = 'en'
# Helper method
portal. default_locale # Returns 'en' if not set
Allowed Locales
Specify which languages your portal supports:
portal. update (
config: {
default_locale: 'en' ,
allowed_locales: [ 'en' , 'es' , 'fr' , 'de' ]
}
)
Categories and articles must use locales from the allowed_locales list. Attempts to create content in unsupported locales will fail validation.
Locale Switching
Users can switch languages via the locale switcher:
// From portalHelpers.js
InitializationHelpers . navigateToLocalePage = () => {
document . addEventListener ( 'change' , e => {
const localeSwitcher = e . target . closest ( '.locale-switcher' );
if ( ! localeSwitcher ) return ;
const { portalSlug } = localeSwitcher . dataset ;
window . location . href = `/hc/ ${ encodeURIComponent ( portalSlug ) } / ${ encodeURIComponent ( localeSwitcher . value ) } /` ;
});
};
Custom Domain
Setting Up Custom Domain
Configure Domain
Add your custom domain to the portal: portal. update ( custom_domain: 'help.example.com' )
DNS Configuration
Point your domain to Chatwoot:
Add a CNAME record pointing to your Chatwoot installation
Or add A records to Chatwoot’s IP addresses
SSL Setup
SSL certificates are automatically provisioned # SSL settings stored in portal
portal. ssl_settings # jsonb field
Verify
Access your Help Center at https://help.example.com
Domain Validation
Domains must be unique across all portals:
validates :custom_domain , uniqueness: true , allow_nil: true
CNAME Instructions
Send setup instructions to your DNS administrator:
PortalInstructionsMailer . send_cname_instructions (
portal: portal,
recipient_email: '[email protected] '
). deliver_later
The mailer includes detailed DNS configuration instructions specific to your portal.
Live Chat Integration
Integrate your Help Center with Chatwoot’s live chat:
# Link portal to a web widget inbox
portal. update ( channel_web_widget_id: widget. id )
# Relationship
belongs_to :channel_web_widget ,
class_name: 'Channel::WebWidget' ,
optional: true
# Via controller parameters
def live_chat_widget_params
return { channel_web_widget_id: nil } if params[ :inbox_id ]. blank?
inbox = Inbox . find (params[ :inbox_id ])
return {} unless inbox. web_widget?
{ channel_web_widget_id: inbox. channel . id }
end
Configuration Schema
Portal configuration is stored as JSONB:
# Allowed configuration keys
CONFIG_JSON_KEYS = %w[
allowed_locales
default_locale
website_token
] . freeze
# Validation
def config_json_format
config[ 'default_locale' ] = default_locale
denied_keys = config. keys - CONFIG_JSON_KEYS
if denied_keys. any?
errors. add ( :config , "keys #{ denied_keys. join ( ',' ) } not supported" )
end
end
Default language code (e.g., ‘en’)
List of supported language codes (e.g., [‘en’, ‘es’, ‘fr’])
Token for web widget integration
Portal Status
Active Portals
scope :active , -> { where ( archived: false ) }
# Usage
Account . portals . active
Archiving
Hide a portal without deleting it:
portal. update ( archived: true )
Archived portals are not accessible to customers but remain in the database for reference.
Plain Layout Mode
Enable simplified layout for embedding:
// From portalHelpers.js
if ( window . portalConfig . isPlainLayoutEnabled === 'true' ) {
InitializationHelpers . appendPlainParamToURLs ();
} else {
// Full portal experience
InitializationHelpers . initializeThemesInPortal ();
InitializationHelpers . navigateToLocalePage ();
InitializationHelpers . initializeSearch ();
InitializationHelpers . initializeTableOfContents ();
}
Plain layout removes navigation, branding, and theme controls for embedding articles in iframes or external sites.
Accessibility
RTL Language Support
Automatic text direction for right-to-left languages:
InitializationHelpers . setDirectionAttribute = () => {
const htmlElement = document . querySelector ( 'html' );
const localeFromHtml = htmlElement . lang ;
htmlElement . dir =
localeFromHtml && getLanguageDirection ( localeFromHtml )
? 'rtl'
: 'ltr' ;
};
External Link Security
Automatic security attributes for external links:
if ( ! isInternalLink ) {
link . target = '_blank' ;
link . rel = 'noopener noreferrer' ;
}
Best Practices
Use high-resolution logos (minimum 200px width)
Choose accessible colors with sufficient contrast
Test appearance in both light and dark themes
Keep header text concise and welcoming
Include a homepage link to your main site
Start with your primary language as default_locale
Add locales progressively based on customer demand
Create categories in all supported locales
Maintain consistent structure across languages
Test locale switching functionality
Use descriptive subdomains (help., support., docs.)
Verify DNS propagation before announcing
Monitor SSL certificate renewal
Keep domain configuration documented
Test all features after domain changes
API Operations
Create Portal
portal = account. portals . create! (
name: 'Help Center' ,
slug: 'help' ,
page_title: 'Support Documentation' ,
color: '#6366F1' ,
config: {
default_locale: 'en' ,
allowed_locales: [ 'en' , 'es' , 'fr' ]
}
)
Update Portal
portal. update! (
header_text: 'How can we help you?' ,
homepage_link: 'https://example.com' ,
color: '#8B5CF6'
)
Delete Portal
portal. destroy!
# Cascades to categories and articles
Troubleshooting
Custom domain not working - Verify DNS records have propagated (can take up to 48 hours). Check that the domain points to the correct host.
Theme not saving - Ensure JavaScript is enabled and localStorage is accessible. Check browser console for errors.
Locale not appearing - Confirm the locale is in the portal’s allowed_locales array and categories exist for that locale.
Logo not displaying - Verify the image format is supported (JPG, PNG, SVG, WebP). Check file size is under maximum limit.