diff --git a/app/helpers/better_together/application_helper.rb b/app/helpers/better_together/application_helper.rb
index d34ed789c..2dcf9dd8d 100644
--- a/app/helpers/better_together/application_helper.rb
+++ b/app/helpers/better_together/application_helper.rb
@@ -80,6 +80,12 @@ def host_community_logo_url
rails_storage_proxy_url(attachment)
end
+ # Returns the canonical URL for the current request, allowing overrides via
+ # +content_for(:canonical_url)+.
+ def canonical_url
+ content_for?(:canonical_url) ? content_for(:canonical_url) : request.original_url
+ end
+
# Builds SEO-friendly meta tags for the current view. Defaults are derived
# from translations and fall back to the Open Graph description when set.
# rubocop:todo Metrics/MethodLength
@@ -94,9 +100,17 @@ def seo_meta_tags # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
keywords = content_for?(:meta_keywords) ? content_for(:meta_keywords) : nil
+ canonical = canonical_url
+ hreflang_tags = I18n.available_locales.map do |locale|
+ tag.link(rel: 'alternate', hreflang: locale, href: url_for(locale:, only_path: false))
+ end
+ hreflang_tags << content_for(:hreflang_links) if content_for?(:hreflang_links)
+
tags = []
tags << tag.meta(name: 'description', content: description)
tags << tag.meta(name: 'keywords', content: keywords) if keywords.present?
+ tags << tag.link(rel: 'canonical', href: canonical)
+ tags.concat(hreflang_tags)
safe_join(tags, "\n")
end
@@ -121,7 +135,7 @@ def open_graph_meta_tags # rubocop:todo Metrics/AbcSize, Metrics/MethodLength, M
t('og.default_description', platform_name: host_platform.name)
end
- og_url = content_for?(:og_url) ? content_for(:og_url) : request.original_url
+ og_url = content_for?(:og_url) ? content_for(:og_url) : canonical_url
og_image = content_for?(:og_image) ? content_for(:og_image) : host_community_logo_url
diff --git a/docs/seo.md b/docs/seo.md
new file mode 100644
index 000000000..2eec654f5
--- /dev/null
+++ b/docs/seo.md
@@ -0,0 +1,27 @@
+# SEO
+
+The engine exposes helpers that output common search engine optimisation tags.
+
+## Canonical URL
+
+`seo_meta_tags` includes a canonical `` tag pointing to the current request
+URL. You can override the URL by setting a `content_for` block:
+
+```erb
+<% content_for :canonical_url, article_url(@article, locale: :en) %>
+```
+
+## Hreflang links
+
+Alternate language links are generated for each available locale. Additional
+links can be appended using `content_for :hreflang_links` and are merged with the
+default set:
+
+```erb
+<% content_for :hreflang_links do %>
+ <%= tag.link rel: 'alternate', hreflang: 'x-default', href: root_url %>
+<% end %>
+```
+
+Links supplied via `content_for` are merged with the automatically generated
+links rather than replacing them.
diff --git a/spec/helpers/better_together/application_helper_spec.rb b/spec/helpers/better_together/application_helper_spec.rb
new file mode 100644
index 000000000..f42a1f67e
--- /dev/null
+++ b/spec/helpers/better_together/application_helper_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+module BetterTogether
+ RSpec.describe ApplicationHelper, type: :helper do
+ before do
+ allow(helper).to receive(:host_platform).and_return(double(name: 'Test Platform', cache_key_with_version: 'test-platform'))
+ allow(helper).to receive(:host_community_logo_url).and_return(nil)
+ allow(controller.request).to receive(:original_url).and_return('http://test.host/en/current')
+ allow(I18n).to receive(:available_locales).and_return(%i[en fr])
+ allow(helper).to receive(:url_for) do |opts|
+ "http://test.host/#{opts[:locale]}/current"
+ end
+ end
+
+ describe '#seo_meta_tags' do
+ it 'includes default canonical and hreflang links' do
+ html = helper.seo_meta_tags
+ expect(html).to include('