diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-07-30 12:55:27 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-07-30 12:55:27 +0200 |
| commit | 5ef13e5aa5ea565b07441616ab1eece565aa2f53 (patch) | |
| tree | b34276c2537061e55d2cfdf7824fc713619f9230 /app/helpers/social_helper.rb | |
| parent | 60c3671b6f09f1f4d4a7d30216c94a80dbee6baa (diff) | |
Canonicalise per locale and declare hreflang alternates
German content is reachable at both /updates/foo and /de/updates/foo,
and a page with no English translation renders German at
/en/updates/foo through the fallback chain, so three URLs each
claimed to be canonical. Both tags now derive from one
locale-to-URL function, which points every duplicate at the address of
the version actually served, the unprefixed German URL, matching
default_url_options.
Alternates are declared only for locales in which the page genuinely has
a translation, since /en/ serving German through the fallback is not an
English version of the page. x-default points at German, the site's
primary language.
Diffstat (limited to 'app/helpers/social_helper.rb')
| -rw-r--r-- | app/helpers/social_helper.rb | 53 |
1 files changed, 49 insertions, 4 deletions
diff --git a/app/helpers/social_helper.rb b/app/helpers/social_helper.rb index d6932a2f..e43f5a5c 100644 --- a/app/helpers/social_helper.rb +++ b/app/helpers/social_helper.rb | |||
| @@ -92,11 +92,14 @@ module SocialHelper | |||
| 92 | @page&.published_at&.iso8601 | 92 | @page&.published_at&.iso8601 |
| 93 | end | 93 | end |
| 94 | 94 | ||
| 95 | # request.path rather than a routing helper: it is already the locale's | 95 | # The address of the version actually being served, not of the URL that |
| 96 | # own canonical form -- unprefixed for German, /en/ for English -- and | 96 | # was requested. So /de/updates/foo points at /updates/foo, and a page |
| 97 | # dropping the query string is what makes it canonical. | 97 | # with no English translation requested under /en/ points at the German |
| 98 | # URL rather than claiming to be an English page. The query string is | ||
| 99 | # dropped, which is what makes it canonical. | ||
| 98 | def og_canonical_url | 100 | def og_canonical_url |
| 99 | og_absolute_url(request.path) | 101 | locale = @page&.persisted? ? og_locale_key : I18n.locale |
| 102 | og_locale_url(locale) | ||
| 100 | end | 103 | end |
| 101 | 104 | ||
| 102 | def og_locale | 105 | def og_locale |
| @@ -104,6 +107,48 @@ module SocialHelper | |||
| 104 | OG_LOCALES[I18n.default_locale.to_sym]) | 107 | OG_LOCALES[I18n.default_locale.to_sym]) |
| 105 | end | 108 | end |
| 106 | 109 | ||
| 110 | # Locales that can appear in a URL. Cccms::LOCALES will replace this when | ||
| 111 | # the set_locale allowlist lands; :root is in available_locales but is not | ||
| 112 | # a language. | ||
| 113 | def og_url_locales | ||
| 114 | I18n.available_locales - [:root] | ||
| 115 | end | ||
| 116 | |||
| 117 | # The request path with any locale prefix stripped, so a locale-specific | ||
| 118 | # URL can be rebuilt from it. /en/updates/foo, /de/updates/foo and | ||
| 119 | # /updates/foo all reduce to /updates/foo. | ||
| 120 | def og_bare_path | ||
| 121 | pattern = og_url_locales.join("|") | ||
| 122 | request.path.sub(%r{\A/(?:#{pattern})(?=/|\z)}, "").presence || "/" | ||
| 123 | end | ||
| 124 | |||
| 125 | # Absolute URL for this page in one locale. The default locale is | ||
| 126 | # unprefixed, matching default_url_options, so /updates/foo is the | ||
| 127 | # canonical German address and /de/updates/foo is a duplicate of it. | ||
| 128 | def og_locale_url(locale) | ||
| 129 | locale = locale.to_sym | ||
| 130 | locale = I18n.default_locale unless og_url_locales.include?(locale) | ||
| 131 | |||
| 132 | path = og_bare_path | ||
| 133 | return og_absolute_url(path) if locale == I18n.default_locale | ||
| 134 | |||
| 135 | og_absolute_url(path == "/" ? "/#{locale}" : "/#{locale}#{path}") | ||
| 136 | end | ||
| 137 | |||
| 138 | # One entry per locale in which the page genuinely has a translation, | ||
| 139 | # including a self-reference, which Google requires. A locale with no | ||
| 140 | # translation is omitted: /en/ would render German content through the | ||
| 141 | # fallback chain, which is not an English version of the page. With only | ||
| 142 | # one translation there is nothing to declare. | ||
| 143 | def og_hreflang_alternates | ||
| 144 | return [] unless @page&.persisted? | ||
| 145 | |||
| 146 | locales = @page.translated_locales.map(&:to_sym) & og_url_locales | ||
| 147 | return [] if locales.size < 2 | ||
| 148 | |||
| 149 | locales.sort.map { |locale| [locale.to_s, og_locale_url(locale)] } | ||
| 150 | end | ||
| 151 | |||
| 107 | # Only locales in which this page genuinely has a translation, so a | 152 | # Only locales in which this page genuinely has a translation, so a |
| 108 | # crawler is not told about a variant that would fall back. | 153 | # crawler is not told about a variant that would fall back. |
| 109 | def og_locale_alternates | 154 | def og_locale_alternates |
