0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-11 01:06:53 +08:00
discourse/spec/requests/crawler_hreflang_spec.rb
asa0w0 d5b6f5753d
FIX: Use hyphens instead of underscores for hreflang tags (#39884)
## Problem
The AI localization feature currently generates `hreflang` tags using
underscores (e.g., `pt_BR`). According to ISO 639-1 and SEO standards,
search engines like Google expect hyphens (e.g., `pt-BR`). This causes
validation errors in tools like PageSpeed Insights and Google Search
Console ("Unexpected language code").

## Solution
This PR applies `.tr('_', '-')` to the `locale` string within the
`hreflang` attribute. This ensures that:
1. The `href` URL remains functional (still using the internal
underscore format for the parameter).
2. The `hreflang` metadata becomes SEO-compliant by using the hyphen
format.

## Validation
Confirmed that this change transforms codes like `pt_BR` into `pt-BR` in
the rendered HTML header, resolving the "Unexpected language code"
error.

---------

Co-authored-by: Natalie Tay <natalie.tay@gmail.com>
2026-05-19 21:18:46 +08:00

140 lines
5.3 KiB
Ruby
Vendored

# frozen_string_literal: true
describe "Crawler hreflang tags" do
fab!(:user)
fab!(:post) { Fabricate(:post, user:) }
describe "when viewing a topic as crawler" do
before do
SiteSetting.content_localization_enabled = true
SiteSetting.content_localization_crawler_param = true
SiteSetting.content_localization_supported_locales = "en|ja|es"
end
it "includes hreflang tags when viewed as crawler" do
get "/t/#{post.topic.slug}/#{post.topic.id}",
headers: {
"User-Agent" => "Googlebot/2.1 (+http://www.google.com/bot.html)",
}
expect(response.body).to include('<link rel="alternate" href=')
expect(response.body).to include('hreflang="x-default"')
expect(response.body).to include('hreflang="en"')
expect(response.body).to include('hreflang="ja"')
expect(response.body).to include('hreflang="es"')
expect(response.body).to include("?#{Discourse::LOCALE_PARAM}=ja")
end
it "uses hyphens instead of underscores in hreflang attribute for region-qualified locales" do
SiteSetting.content_localization_supported_locales = "en|pt_BR|zh_CN"
get "/t/#{post.topic.slug}/#{post.topic.id}",
headers: {
"User-Agent" => "Googlebot/2.1 (+http://www.google.com/bot.html)",
}
expect(response.body).to include('hreflang="pt-BR"')
expect(response.body).to include('hreflang="zh-CN"')
expect(response.body).not_to include('hreflang="pt_BR"')
expect(response.body).not_to include('hreflang="zh_CN"')
expect(response.body).to include("?#{Discourse::LOCALE_PARAM}=pt_BR")
expect(response.body).to include("?#{Discourse::LOCALE_PARAM}=zh_CN")
end
it "doesn't include hreflang tags for normal users" do
get "/t/#{post.topic.slug}/#{post.topic.id}"
expect(response.body).not_to include('hreflang="x-default"')
end
it "doesn't include hreflang tags when settings are disabled" do
SiteSetting.content_localization_enabled = false
get "/t/#{post.topic.slug}/#{post.topic.id}",
headers: {
"User-Agent" => "Googlebot/2.1 (+http://www.google.com/bot.html)",
}
expect(response.body).not_to include('hreflang="x-default"')
end
it "self-canonicalizes translated topic pages when ?tl= is present" do
get "/t/#{post.topic.slug}/#{post.topic.id}?#{Discourse::LOCALE_PARAM}=ja",
headers: {
"User-Agent" => "Googlebot/2.1 (+http://www.google.com/bot.html)",
}
expect(response.body).to include(
%(<link rel="canonical" href="#{Discourse.base_url}/t/#{post.topic.slug}/#{post.topic.id}?#{Discourse::LOCALE_PARAM}=ja">),
)
expect(response.headers["X-Robots-Tag"].to_s).not_to include("noindex")
end
it "self-canonicalizes translated list pages when ?tl= is present" do
get "/latest?#{Discourse::LOCALE_PARAM}=ja",
headers: {
"User-Agent" => "Googlebot/2.1 (+http://www.google.com/bot.html)",
}
expect(response.body).to include(
%(<link rel="canonical" href="#{Discourse.base_url}/latest?#{Discourse::LOCALE_PARAM}=ja">),
)
end
it "does not append ?tl= to canonical when locale param is absent" do
get "/t/#{post.topic.slug}/#{post.topic.id}",
headers: {
"User-Agent" => "Googlebot/2.1 (+http://www.google.com/bot.html)",
}
expect(response.body).to match(
%r{<link rel="canonical" href="#{Regexp.escape(Discourse.base_url)}/t/#{post.topic.slug}/#{post.topic.id}"\s*/?>},
)
end
it "ignores ?tl= when the locale is not in the supported list" do
get "/t/#{post.topic.slug}/#{post.topic.id}?#{Discourse::LOCALE_PARAM}=xyz",
headers: {
"User-Agent" => "Googlebot/2.1 (+http://www.google.com/bot.html)",
}
expect(response.body).to include(
%(<link rel="canonical" href="#{Discourse.base_url}/t/#{post.topic.slug}/#{post.topic.id}">),
)
end
it "uses & as separator when another allowed param is present" do
Fabricate.times(35, :post, topic: post.topic, user:)
get "/t/#{post.topic.slug}/#{post.topic.id}?page=2&#{Discourse::LOCALE_PARAM}=ja",
headers: {
"User-Agent" => "Googlebot/2.1 (+http://www.google.com/bot.html)",
}
expect(response.body).to include(
%(<link rel="canonical" href="#{Discourse.base_url}/t/#{post.topic.slug}/#{post.topic.id}?page=2&amp;#{Discourse::LOCALE_PARAM}=ja">),
)
end
it "does not modify canonical when content_localization_crawler_param is disabled" do
SiteSetting.content_localization_crawler_param = false
get "/t/#{post.topic.slug}/#{post.topic.id}?#{Discourse::LOCALE_PARAM}=ja",
headers: {
"User-Agent" => "Googlebot/2.1 (+http://www.google.com/bot.html)",
}
expect(response.body).to match(
%r{<link rel="canonical" href="#{Regexp.escape(Discourse.base_url)}/t/#{post.topic.slug}/#{post.topic.id}"\s*/?>},
)
end
it "does not append ?tl= to canonical for non-crawler requests" do
get "/t/#{post.topic.slug}/#{post.topic.id}?#{Discourse::LOCALE_PARAM}=ja"
expect(response.body).to match(
%r{<link rel="canonical" href="#{Regexp.escape(Discourse.base_url)}/t/#{post.topic.slug}/#{post.topic.id}"\s*/?>},
)
end
end
end