Fixes help center public article search so query responses stay compact and locale-scoped. Whitespace-only queries are now treated as empty in both the portal UI and the server-side search path, and search suggestions stay aligned with the trimmed query. Fixes: https://github.com/chatwoot/chatwoot/issues/10402 Closes: https://github.com/chatwoot/chatwoot/issues/10402 ## Why The public help center search endpoint reused the full article serializer for query responses, which returned much more data than the search suggestions UI needed. That made responses heavier than necessary and also surfaced nested portal and category data that made the results look cross-locale. Whitespace-only searches could also still reach the backend search path, and in Enterprise that meant embedding search could be invoked for a blank query. ## What changed - return a compact search-specific payload for article query responses - keep the existing full article serializer for normal article listing responses - preserve current-locale search behavior for the portal search flow - trim whitespace-only search terms on the client so they do not open suggestions or trigger a request - reuse the normalized query on the backend so whitespace-only requests are treated as empty searches in both OSS and Enterprise paths - pass the trimmed search term into suggestions so highlighting matches the actual query being sent - add request and frontend regression coverage for compact payloads, locale scoping, and whitespace-only search behavior ## Validation 1. Open `/hc/:portal/:locale` in the public help center. 2. Enter only spaces in the search box and confirm suggestions do not open. 3. Search for a real term and confirm suggestions appear. 4. Verify the results are limited to the active locale. 5. Click a suggestion and confirm it opens the correct article page. 6. Inspect the query response and confirm it returns the compact search payload instead of the full article serializer. --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
92 lines
2.8 KiB
Ruby
92 lines
2.8 KiB
Ruby
class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::BaseController
|
|
before_action :ensure_custom_domain_request, only: [:show, :index]
|
|
before_action :portal
|
|
before_action :set_category, except: [:index, :show, :tracking_pixel]
|
|
before_action :set_article, only: [:show]
|
|
layout 'portal'
|
|
|
|
def index
|
|
@search_query = list_params[:query]
|
|
@articles = @portal.articles.published.includes(:category, :author)
|
|
|
|
@articles = @articles.where(locale: permitted_params[:locale]) if permitted_params[:locale].present?
|
|
|
|
@articles_count = @articles.count
|
|
|
|
search_articles
|
|
order_by_sort_param
|
|
limit_results
|
|
end
|
|
|
|
def show
|
|
@og_image_url = helpers.set_og_image_url(@portal.name, @article.title)
|
|
end
|
|
|
|
def tracking_pixel
|
|
@article = @portal.articles.find_by(slug: permitted_params[:article_slug])
|
|
return head :not_found unless @article
|
|
|
|
@article.increment_view_count if @article.published?
|
|
|
|
# Serve the 1x1 tracking pixel with 24-hour private cache
|
|
# Private cache bypasses CDN but allows browser caching to prevent duplicate views from same user
|
|
expires_in 24.hours, public: false
|
|
response.headers['Content-Type'] = 'image/png'
|
|
|
|
pixel_path = Rails.public_path.join('assets/images/tracking-pixel.png')
|
|
send_file pixel_path, type: 'image/png', disposition: 'inline'
|
|
end
|
|
|
|
private
|
|
|
|
def limit_results
|
|
return if list_params[:per_page].blank?
|
|
|
|
per_page = [list_params[:per_page].to_i, 100].min
|
|
per_page = 25 if per_page < 1
|
|
@articles = @articles.page(list_params[:page]).per(per_page)
|
|
end
|
|
|
|
def search_articles
|
|
@articles = @articles.search(list_params) if list_params.present?
|
|
end
|
|
|
|
def order_by_sort_param
|
|
@articles = if list_params[:sort].present? && list_params[:sort] == 'views'
|
|
@articles.order_by_views
|
|
else
|
|
@articles.order_by_position
|
|
end
|
|
end
|
|
|
|
def set_article
|
|
@article = @portal.articles.find_by(slug: permitted_params[:article_slug])
|
|
@parsed_content = render_article_content(@article.content)
|
|
end
|
|
|
|
def set_category
|
|
return if permitted_params[:category_slug].blank?
|
|
|
|
@category = @portal.categories.find_by!(
|
|
slug: permitted_params[:category_slug],
|
|
locale: permitted_params[:locale]
|
|
)
|
|
end
|
|
|
|
def list_params
|
|
@list_params ||= params.permit(:query, :locale, :sort, :status, :page, :per_page).tap do |permitted|
|
|
permitted[:query] = permitted[:query].to_s.strip.presence
|
|
end
|
|
end
|
|
|
|
def permitted_params
|
|
params.permit(:slug, :category_slug, :locale, :id, :article_slug)
|
|
end
|
|
|
|
def render_article_content(content)
|
|
ChatwootMarkdownRenderer.new(content).render_article
|
|
end
|
|
end
|
|
|
|
Public::Api::V1::Portals::ArticlesController.prepend_mod_with('Public::Api::V1::Portals::ArticlesController')
|