Vai al contenuto principaleVai al footer
SEO
|
10 febbraio 15

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.

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>

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.