Applicazioni multilingua: come gestire meta tags e sitemap per migliorarne l'indicizzazione
Districarsi nel contorto mondo dell'indicizzazione quando l'applicazione è multilingua, significa dover seguire una serie di regole SEO per generare correttamente link alternate e le sitemaps.
Districarsi nel contorto mondo dell'indicizzazione quando l'applicazione è multilingua, significa dover seguire una serie di regole SEO per generare correttamente link alternate e le sitemaps.
La gestione della componente SEO di un applicazione web è tanto importante quanto semplice da implementare, se stiamo sfruttando Rails: più o meno ovunque sono disponibili guide sull'argomento.
La generazione delle sitemap poi, attraverso gemme come sitemap_generator
, risulta essere praticamente a costo zero.
Le difficoltà arrivano nel momento in cui ci si trova a dover gestire applicazioni multilingua, nelle quali magari alcune sezioni sono disponibili soltanto per sottoinsieme delle lingue disponibili.
La generazione degli URL
Per facilitare l'internazionalizzazione degli URL nelle nostre applicazioni ci vengono in aiuto gemme come route_translator
. Quest'ultima, ad esempio, estende la DSL Rails a livello di routing con una particolare direttiva localized
:
# config/routes.rb localized do get "/about" to: 'static#about', as: :about resources :posts, only: [:show] end
Le traduzioni sulle rotte vengono specificate nei nostri locale files:
# config/locales/fr.yml fr: routes: posts: nouvelles about: a-propos-de-nous # config/locales/en.yml en: routes: posts: posts about: about # config/locales/it.yml it: routes: posts: articoli about: chi-siamo
Per ogni rotta specificata all'interno del blocco verrà generato un route helper specifico per ogni lingua, così come un helper in grado di selezionare dinamicamente la rotta più corretta per la lingua attiva:
about_it_path # => /chi-siamo about_en_path # => /en/about about_fr_path # => /fr/a-propos-de-nous about_path # => dipendente dalla lingua corrente
Attraverso questi helper, generare uno switch per la lingua diventa molto semplice:
/ app/views/static/about.html.slim - content_for(:switch_locale) do ul.switch_locale - I18n.available_locales.each do |locale| li= I18n.with_locale(locale) do = link_to "Switch to #{locale}", about_url
Immaginiamo ora un sito con una ventina di rotte da tradurre: dovremmo replicare uno snippet simile in ogni vista? Sarebbe un approccio poco DRY. Perché non sfruttare il metodo url_for
, per generalizzare il discorso?
/ app/views/layout/application.html.slim ul.switch_locale - I18n.available_locales.each do |locale| li= I18n.with_locale(locale) do = link_to "Switch to #{locale}", url_for(locale: locale)
Il giochetto funziona fino a quando non dobbiamo trattare con modelli il cui slug è a sua volta tradotto (ad esempio attraverso l'accoppiata globalize
+ friendly_id
):
/ /app/views/posts/show.html.slim = I18n.with_locale(:it) { post.slug } # => "il-mio-primo-post" = I18n.with_locale(:en) { post.slug } # => "my-first-post" = I18n.with_locale(:fr) { post.slug } # => "mon-premier-nouvell" = params.inspect / => {"controller" => "posts", "action" => "show", "id" => "my-first-post"} - %i(it en fr).each do |locale| = I18n.with_locale(locale) { url_for(locale: locale) } / => /articoli/my-first-post / => /en/posts/my-first-post / => /fr/nouvelles/my-first-post
Cosa è successo? Beh, il metodo url_for
compone l'URL usando come informazioni il merge tra le chiavi ricevute (nel nostro caso, locale
) e le chiavi dell'hash globale params
— nel nostro caso controller
, action
ed id
.
Purtroppo per noi, la chiave id
contiene il valore dello slug relativo alla pagina corrente ("my-first-post"
). La rotta post_path
generata da route_translator
è in grado di tradurre la parte di URL relativa a controller ed action, ma poco può fare sul parametro id
che riceve come ingresso, e che non risulta essere riconducibile al modello Post
che l'ha originato.
Possiamo ovviare a questo inconveniente rendendo leggeremente più configurabile il locale switcher:
/ app/views/layout/application.html.slim ul.switch_locale - I18n.available_locales.each do |locale| li= I18n.with_locale(locale) do - url = yield(:current_page_url) || url_for(locale: locale) = link_to "Switch to #{locale}", url / app/views/posts/show.html.slim - content_for(:current_page_url) { post_url(@post) }
In questo modo, solo nelle rotte con slug tradotti, siamo liberi di specificare un particolare metodo da sfruttare per la generazione dell'URL:
<li><a href="/articoli/il-mio-primo-post">Switch to it</a></li> <li><a href="/en/posts/my-first-post">Switch to en</a></li> <li><a href="/fr/nouvelles/mon-premier-nouvell">Switch to fr</a></li>
I link alternate
Una volta impostata la generazione di URL tradotte, si possono facilmente generare i link alternate all'interno del nostro <head>
, seguendo la medesima logica:
/ app/views/layouts/application.html.slim - I18n.available_locales.each do |locale| - I18n.with_locale(locale) do - url = yield(:current_page_url) || url_for(locale: locale) - if locale == I18n.default_locale link rel="alternate" hreflang="x-default" href=url link rel="alternate" hreflang=locale href=url
Sitemap
Arrivati a questo punto abbiamo tutte le informazioni necessarie per poter generare una sitemap esaustiva.
Lo scenario che possiamo ipotizzare è quello di un sito con 2 domini differenti: myblog.it e myblog.com. Il primo dominio è esclusivo per la lingua italiana, il secondo espone i contenuti tradotti in inglese e francese.
Cercando su vari blog soluzioni per generare una sitemap non ho trovato risultati soddisfacenti: la maggioranza degli articoli si limitano a copiare quanto già riportato sulle linee guida di Google oppure sul cheatsheet di SeoMoz.
A seguito di numerose discussioni con il nostro esperto SEO, la soluzione adottata è stata quella di generare due sitemap distinte, una per dominio, specificando all'interno del tag XML <url>
anche gli URL alternativi. È possibile riprodurre il comportamento con queste righe di codice:
# app/controller/sitemap_controller.rb class SitemapController < ApplicationController def index @available_locales = @domain == "myblog.it" ? [:it] : [:en, :fr] end end # app/views/sitemap/index.xml.builder xml.urlset( "xmlns" => "http://www.sitemaps.org/schemas/sitemap/0.9", "xmlns:xhtml" => "http://www.w3.org/1999/xhtml") do @available_domain_locales.each do |locale| I18n.with_locale(locale) do xml.url xml.loc posts_url xml.priority "1.0" xml.changefreq "monthly" xml.lastmod "2015-01-01" I18n.available_locales.each do |other_locale| I18n.with_locale(other_locale) do xml.tag! "xhtml:link", rel: 'alternate', hreflang: other_locale.to_s, href: posts_url end end end end end
Il risultato è il seguente:
# http://myblog.it/sitemap.xml <?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"> <url> <loc>http://myblog.it/articoli/il-mio-primo-post</loc> <priority>1.0</priority> <lastmod>2015-01-01</lastmod> <changefreq>monthly</changefreq> <xhtml:link rel="alternate" hreflang="it" href="http://myblog.it/articoli/il-mio-primo-post" /> <xhtml:link rel="alternate" hreflang="en" href="http://myblog.com/posts/my-first-post" /> <xhtml:link rel="alternate" hreflang="fr" href="http://myblog.com/fr/nouvelles/mon-premier-nouvell" /> </url> <url> <loc>http://myblog.it/chi-siamo</loc> <priority>1.0</priority> <lastmod>2015-01-01</lastmod> <changefreq>monthly</changefreq> <xhtml:link rel="alternate" hreflang="it" href="http://myblog.it/chi-siamo" /> <xhtml:link rel="alternate" hreflang="en" href="http://myblog.com/about-us" /> <xhtml:link rel="alternate" hreflang="fr" href="http://myblog.com/fr/a-propos-de-nous" /> </url> </urlset> # http://myblog.com/sitemap.xml <?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"> <url> <loc>http://myblog.it/posts/my-first-post</loc> <priority>1.0</priority> <lastmod>2015-01-01</lastmod> <changefreq>monthly</changefreq> <xhtml:link rel="alternate" hreflang="it" href="http://myblog.it/articoli/il-mio-primo-post" /> <xhtml:link rel="alternate" hreflang="en" href="http://myblog.com/posts/my-first-post" /> <xhtml:link rel="alternate" hreflang="fr" href="http://myblog.com/fr/nouvelles/mon-premier-nouvell" /> </url> <url> <loc>http://myblog.it/fr/nouvelles/mon-premier-nouvell</loc> <priority>1.0</priority> <lastmod>2015-01-01</lastmod> <changefreq>monthly</changefreq> <xhtml:link rel="alternate" hreflang="it" href="http://myblog.it/articoli/il-mio-primo-post" /> <xhtml:link rel="alternate" hreflang="en" href="http://myblog.com/posts/my-first-post" /> <xhtml:link rel="alternate" hreflang="fr" href="http://myblog.com/fr/nouvelles/mon-premier-nouvell" /> </url> <url> <loc>http://myblog.com/about-us</loc> <priority>1.0</priority> <lastmod>2015-01-01</lastmod> <changefreq>monthly</changefreq> <xhtml:link rel="alternate" hreflang="it" href="http://myblog.it/chi-siamo" /> <xhtml:link rel="alternate" hreflang="en" href="http://myblog.com/about-us" /> <xhtml:link rel="alternate" hreflang="fr" href="http://myblog.com/fr/a-propos-de-nous" /> </url> <url> <loc>http://myblog.com/fr/a-propos-de-nous</loc> <priority>1.0</priority> <lastmod>2015-01-01</lastmod> <changefreq>monthly</changefreq> <xhtml:link rel="alternate" hreflang="it" href="http://myblog.it/chi-siamo" /> <xhtml:link rel="alternate" hreflang="en" href="http://myblog.com/about-us" /> <xhtml:link rel="alternate" hreflang="fr" href="http://myblog.com/fr/a-propos-de-nous" /> </url> </urlset>
Con questo meccanismo ogni dominio fornisce l'elenco completo delle url che gestisce e, per ogni url fornita, l'elenco completo delle alternative eventualmente cross-domain, in modo che i motori di ricerca siano in grado fin da subito di ottenere la mappatura completa delle url.