fix(help-center): documentation layout on custom domain root, locale (#14850)

Help centers using the documentation layout now render correctly in two
cases that previously fell back to the wrong output. Opening a portal at
its custom-domain root (e.g. docs.example.com/ ) now shows the full
documentation home
in place instead of the classic layout, and portals whose locale is a
region variant that Chatwoot doesn't ship a
translation for (e.g. th_TH , fr_ML ) now show their categories in the
sidebar on article pages.

Closes
https://linear.app/chatwoot/issue/CW-7437/portal-layout-is-not-properly-working-in-custom-domain

## How to test

1. Create a portal with the documentation layout and a custom domain
(e.g. example.chat.test ), with at least one
category containing a published article.
2. Visit the custom-domain root ( http://example.chat.test:3000/ ) → it
should show the full documentation home
(sidebar, topbar), not the classic layout, with no redirect.
3. Set the portal's locale to a region variant Chatwoot doesn't
translate, e.g. th_TH , with categories/articles
under that locale.
4. Open an article → the sidebar should list the article's category and
sibling articles. Before the fix the sidebar
was empty for these locales.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pranav
2026-06-24 16:12:55 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 74db16158d
commit f9cc702030
7 changed files with 123 additions and 34 deletions
+1 -1
View File
@@ -144,7 +144,7 @@ jobs:
# Backend tests with parallelization
backend-tests:
<<: *defaults
parallelism: 20
parallelism: 18
steps:
- checkout
- node/install:
@@ -0,0 +1,29 @@
module PortalHomeData
extend ActiveSupport::Concern
private
def load_home_data
base_articles = @portal.articles.published.where(locale: @locale).includes(:author, :category)
@visible_categories = @portal.categories
.where(locale: @locale)
.joins(:articles).where(articles: { status: :published })
.order(position: :asc)
.group('categories.id')
@popular_topics = @visible_categories.first(3)
@featured = base_articles.order_by_views.limit(6)
@category_contributors = build_category_contributors(@visible_categories)
end
def build_category_contributors(categories)
category_ids = categories.map(&:id)
return {} if category_ids.empty?
@portal.articles
.published
.where(locale: @locale, category_id: category_ids)
.includes(:author)
.group_by(&:category_id)
.transform_values { |articles| articles.filter_map(&:author).uniq.first(3) }
end
end
+5
View File
@@ -1,5 +1,6 @@
class DashboardController < ActionController::Base
include SwitchLocale
include PortalHomeData
GLOBAL_CONFIG_KEYS = %w[
LOGO
@@ -63,6 +64,10 @@ class DashboardController < ActionController::Base
return unless @portal
@locale = @portal.default_locale
if @portal.layout == 'documentation'
request.variant = :documentation
load_home_data
end
render 'public/api/v1/portals/show', layout: 'portal', portal: @portal and return
end
@@ -39,9 +39,11 @@ class Public::Api::V1::Portals::BaseController < PublicController
end
def switch_locale_with_portal(&)
@locale = validate_and_get_locale(params[:locale])
# Keep @locale as the portal's own locale code (e.g. th_TH) for content queries,
# while UI translations fall back to an available I18n locale (e.g. th).
@locale = params[:locale]
I18n.with_locale(@locale, &)
I18n.with_locale(validate_and_get_locale(@locale), &)
end
def switch_locale_with_article(&)
@@ -49,13 +51,12 @@ class Public::Api::V1::Portals::BaseController < PublicController
Rails.logger.info "Article: not found for slug: #{params[:article_slug]}"
render_404 && return if article.blank?
article_locale = if article.category.present?
article.category.locale
else
article.locale
end
@locale = validate_and_get_locale(article_locale)
I18n.with_locale(@locale, &)
@locale = if article.category.present?
article.category.locale
else
article.locale
end
I18n.with_locale(validate_and_get_locale(@locale), &)
end
def allow_iframe_requests
@@ -1,4 +1,6 @@
class Public::Api::V1::PortalsController < Public::Api::V1::Portals::BaseController
include PortalHomeData
before_action :ensure_custom_domain_request, only: [:show]
before_action :redirect_to_portal_with_locale, only: [:show]
before_action :portal
@@ -31,28 +33,4 @@ class Public::Api::V1::PortalsController < Public::Api::V1::Portals::BaseControl
portal
redirect_to "/hc/#{@portal.slug}/#{@portal.default_locale}"
end
def load_home_data
base_articles = @portal.articles.published.where(locale: @locale).includes(:author, :category)
@visible_categories = @portal.categories
.where(locale: @locale)
.joins(:articles).where(articles: { status: :published })
.order(position: :asc)
.group('categories.id')
@popular_topics = @visible_categories.first(3)
@featured = base_articles.order_by_views.limit(6)
@category_contributors = build_category_contributors(@visible_categories)
end
def build_category_contributors(categories)
category_ids = categories.map(&:id)
return {} if category_ids.empty?
@portal.articles
.published
.where(locale: @locale, category_id: category_ids)
.includes(:author)
.group_by(&:category_id)
.transform_values { |articles| articles.filter_map(&:author).uniq.first(3) }
end
end
@@ -0,0 +1,50 @@
require 'rails_helper'
describe 'GET / on a help center custom domain', type: :request do
let(:account) { create(:account) }
let(:agent) { create(:user, account: account, role: :agent) }
around do |example|
with_modified_env FRONTEND_URL: 'http://www.chatwoot.test' do
example.run
end
end
context 'when the portal uses the documentation layout' do
let!(:portal) do
create(:portal, account: account, slug: 'doc-portal', custom_domain: 'docs.example.com',
config: { allowed_locales: ['en'], default_locale: 'en', layout: 'documentation' })
end
let!(:category) do
create(:category, name: 'Getting Started', portal: portal, account_id: account.id, locale: 'en', slug: 'getting-started')
end
before do
create(:article, category: category, portal: portal, account: account, author: agent, locale: 'en', status: :published)
end
it 'renders the documentation home in place without redirecting' do
host! portal.custom_domain
get '/'
expect(response).to have_http_status(:success)
expect(response.body).to include('sidebar-drawer-checkbox')
expect(response.body).to include('Getting Started')
end
end
context 'when the portal uses the classic layout' do
let!(:portal) do
create(:portal, account: account, slug: 'classic-portal', custom_domain: 'classic.example.com',
config: { allowed_locales: ['en'], default_locale: 'en', layout: 'classic' })
end
it 'renders the classic home without the documentation layout' do
host! portal.custom_domain
get '/'
expect(response).to have_http_status(:success)
expect(response.body).not_to include('sidebar-drawer-checkbox')
end
end
end
@@ -211,4 +211,30 @@ RSpec.describe 'Public Articles API', type: :request do
expect(response.headers['Content-Type']).to eq('image/png')
end
end
describe 'documentation layout sidebar for a region-variant locale' do
let!(:th_portal) do
create(:portal, slug: 'th-portal', custom_domain: 'th.example.com',
config: { allowed_locales: ['th_TH'], default_locale: 'th_TH', layout: 'documentation' })
end
let!(:th_category) do
create(:category, name: 'TH Category', portal: th_portal, account_id: account.id, locale: 'th_TH', slug: 'th-cat')
end
let!(:th_article) do
create(:article, category: th_category, portal: th_portal, account_id: account.id, author_id: agent.id, locale: 'th_TH')
end
before do
create(:article, category: th_category, portal: th_portal, account_id: account.id, author_id: agent.id,
locale: 'th_TH', title: 'Sibling In Sidebar', status: :published)
end
it 'lists the category and sibling articles using the full portal locale' do
host! 'th.example.com'
get "/hc/#{th_portal.slug}/articles/#{th_article.slug}"
expect(response).to have_http_status(:success)
expect(response.body).to include('Sibling In Sidebar')
end
end
end