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