Skip to main content
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

name
string
required
Portal display name
slug
string
required
Unique URL identifier for the portal
page_title
string
Browser tab title and meta title tag
header_text
text
Text displayed in the portal header
URL to link back to your main website
color
string
Primary brand color (hex code)
custom_domain
string
Your own domain for hosting the Help Center (must be unique)
archived
boolean
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

Upload your brand logo using Active Storage:
# 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

// 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

1

Configure Domain

Add your custom domain to the portal:
portal.update(custom_domain: 'help.example.com')
2

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
3

SSL Setup

SSL certificates are automatically provisioned
# SSL settings stored in portal
portal.ssl_settings  # jsonb field
4

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

Connect Web Widget

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
  • Allows customers to contact support directly from Help Center
  • Contextual help based on article being viewed
  • Seamless transition from self-service to live support
# 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
config.default_locale
string
Default language code (e.g., ‘en’)
config.allowed_locales
array
List of supported language codes (e.g., [‘en’, ‘es’, ‘fr’])
config.website_token
string
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';
};
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
  • Optimize logo images (WebP format recommended)
  • Use vector logos (SVG) when possible
  • Monitor portal load times
  • Enable browser caching for static assets
  • Consider CDN for custom domain portals

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.

Build docs developers (and LLMs) love