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:
co-authored by
Claude Opus 4.8
parent
74db16158d
commit
f9cc702030
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user