diff --git a/app/helpers/knowledge_base_helper.rb b/app/helpers/knowledge_base_helper.rb
index 816b04f1b..e33da30f4 100644
--- a/app/helpers/knowledge_base_helper.rb
+++ b/app/helpers/knowledge_base_helper.rb
@@ -11,7 +11,7 @@ module KnowledgeBaseHelper
custom_address = knowledge_base.custom_address_uri
return path if !custom_address
- custom_path = path.gsub(%r{^/help}, custom_address.path || '').presence || '/'
+ custom_path = knowledge_base.custom_address_path(path)
prefix = full ? knowledge_base.custom_address_prefix(request) : ''
"#{prefix}#{custom_path}"
@@ -61,4 +61,18 @@ module KnowledgeBaseHelper
def dropdown_menu_direction
system_locale_via_uri.dir == 'ltr' ? 'right' : 'left'
end
+
+ def canonical_link_tag(knowledge_base, *objects)
+ path = kb_public_system_path(*objects)
+
+ tag :link, rel: 'canonical', href: knowledge_base.canonical_url(path)
+ end
+
+ def kb_public_system_path(*objects)
+ objects
+ .compact
+ .map { |elem| elem.translation.to_param }
+ .unshift(help_root_path)
+ .join('/')
+ end
end
diff --git a/app/models/knowledge_base.rb b/app/models/knowledge_base.rb
index f7ed3da9c..e8f7a18ec 100644
--- a/app/models/knowledge_base.rb
+++ b/app/models/knowledge_base.rb
@@ -102,6 +102,29 @@ class KnowledgeBase < ApplicationModel
"#{custom_address_uri.scheme}://#{host}#{port_string}"
end
+ def custom_address_path(path)
+ uri = custom_address_uri
+
+ return path if !uri
+
+ custom_path = custom_address_uri.path || ''
+ applied_path = path.gsub(%r{^/help}, custom_path)
+
+ applied_path.presence || '/'
+ end
+
+ def canonical_host
+ custom_address_uri&.host || Setting.get('fqdn')
+ end
+
+ def canonical_scheme_host
+ "#{Setting.get('http_type')}://#{canonical_host}"
+ end
+
+ def canonical_url(path)
+ "#{canonical_scheme_host}#{custom_address_path(path)}"
+ end
+
def full_destroy!
ChecksKbClientNotification.disable_in_all_classes!
diff --git a/app/views/layouts/knowledge_base.html.erb b/app/views/layouts/knowledge_base.html.erb
index 2a1df3589..067709cd6 100644
--- a/app/views/layouts/knowledge_base.html.erb
+++ b/app/views/layouts/knowledge_base.html.erb
@@ -13,6 +13,7 @@
<%= stylesheet_link_tag "knowledge_base.css", :media => 'all' %>
<%= render 'knowledge_base/public/inline_stylesheet', knowledge_base: @knowledge_base, locale: system_locale_via_uri %>
+<%= canonical_link_tag @knowledge_base, @category, @object %>
<%= render 'knowledge_base/public/top_banner', object: @object || @knowledge_base if editor? %>
diff --git a/spec/system/knowledge_base_public/canonical_link_spec.rb b/spec/system/knowledge_base_public/canonical_link_spec.rb
new file mode 100644
index 000000000..15467becc
--- /dev/null
+++ b/spec/system/knowledge_base_public/canonical_link_spec.rb
@@ -0,0 +1,104 @@
+require 'rails_helper'
+
+RSpec.describe 'Public Knowledge Base canonical link', type: :system, current_user_id: 1, authenticated_as: false do
+ include_context 'basic Knowledge Base'
+
+ let(:path) { '/path' }
+ let(:subdomain) { 'subdomain.example.net' }
+ let(:locale) { primary_locale.system_locale.locale }
+ let(:category_slug) { category.translations.first.to_param }
+ let(:answer_slug) { published_answer.translations.first.to_param }
+
+ before do
+ published_answer
+ knowledge_base.update! custom_address: custom_address
+ end
+
+ shared_examples 'having canonical links on all pages' do
+ it 'includes canonical link on home page' do
+ visit help_root_path(locale)
+ expect(page).to have_canonical_url("#{prefix}/#{locale}")
+ end
+
+ it 'includes canonical link on category page' do
+ visit help_category_path(locale, category)
+ expect(page).to have_canonical_url("#{prefix}/#{locale}/#{category_slug}")
+ end
+
+ it 'includes canonical link on answer page' do
+ visit help_answer_path(locale, published_answer.category, published_answer)
+ expect(page).to have_canonical_url("#{prefix}/#{locale}/#{category_slug}/#{answer_slug}")
+ end
+ end
+
+ shared_examples 'core locations' do
+ let(:scheme) { ssl ? 'https' : 'http' }
+ before { Setting.set('http_type', scheme) }
+
+ context 'with custom domain' do
+ let(:custom_address) { subdomain }
+ let(:prefix) { "#{scheme}://#{subdomain}" }
+
+ it_behaves_like 'having canonical links on all pages'
+ end
+
+ context 'with custom path' do
+ let(:custom_address) { path }
+ let(:prefix) { "#{scheme}://#{Setting.get('fqdn')}#{path}" }
+
+ it_behaves_like 'having canonical links on all pages'
+ end
+
+ context 'with custom domain and path' do
+ let(:custom_address) { "#{subdomain}#{path}" }
+ let(:prefix) { "#{scheme}://#{subdomain}#{path}" }
+
+ it_behaves_like 'having canonical links on all pages'
+ end
+
+ context 'without custom address' do
+ let(:custom_address) { nil }
+ let(:prefix) { "#{scheme}://#{Setting.get('fqdn')}/help" }
+
+ it_behaves_like 'having canonical links on all pages'
+ end
+ end
+
+ context 'when SSL disabled' do
+ let(:ssl) { false }
+
+ include_examples 'core locations'
+ end
+
+ context 'when SSL enabled' do
+ let(:ssl) { true }
+
+ include_examples 'core locations'
+ end
+
+ matcher :have_canonical_url do |expected|
+ match do
+ return false if canonical_link_element.blank?
+
+ canonical_link_target == expected
+ end
+
+ failure_message do
+ return 'no canonical link found' if canonical_link_element.blank?
+
+ "expected canonical link pointing to \"#{expected}\", but found \"#{canonical_link_target}\" instead"
+ end
+
+ def canonical_link_element
+ return @canonical_link_element if defined?(@canonical_link_element)
+
+ @canonical_link_element = actual.first('head link[rel=canonical]', visible: :hidden, minimum: 0)
+ end
+
+ def canonical_link_target
+ @canonical_link_target ||= canonical_link_element[:href]
+ end
+
+ description { "have canonical tag with href of #{expected}" }
+ end
+end