← Voltar ao blog

Hreflang no Magento 2: SEO multi-idioma do jeito certo

Rodrigo Biazzi

O Magento 2 lida bem com vários idiomas no lado do conteúdo. Cada store view tem seu próprio locale, traduções, nomes de produto, URL keys e conteúdo CMS. O que ele não faz de fábrica é avisar os buscadores que as versões em inglês, alemão e português de uma página pertencem ao mesmo conjunto. Esse é o trabalho das anotações hreflang, e o Magento 2.4 não as gera.

Este artigo mostra como estruturar uma loja Magento multi-idioma para busca, como o hreflang funciona e como gerar links alternativos corretos para produtos, categorias e páginas CMS.

Primeiro, a estrutura das lojas

O hreflang só funciona se cada versão de idioma tiver a própria URL. No Magento há três jeitos comuns de conseguir isso:

  • Store code na URL, como example.com/en/ e example.com/de/, ativado com Add Store Code to Urls (web/url/use_store) ou com uma base URL separada por store view.
  • Subdomínios, como en.example.com, com uma base URL por store view.
  • Domínios separados por país, cada um mapeado para um website ou store view na configuração do servidor web.

Qualquer um deles funciona para hreflang. O que não funciona é trocar o idioma por cookie na mesma URL. Os crawlers não mantêm o cookie da loja entre requisições, então só enxergam a store view padrão. Garanta que cada store view seja acessível só pela URL. As escolhas de estrutura por trás de websites, stores e store views estão em configuração de multi store e multi website.

Como o hreflang funciona

Cada página lista todas as suas versões de idioma, incluindo ela mesma, como elementos link no head:

<link rel="alternate" hreflang="en" href="https://example.com/en/blue-shirt.html"/>
<link rel="alternate" hreflang="de" href="https://example.com/de/blaues-hemd.html"/>
<link rel="alternate" hreflang="pt-BR" href="https://example.com/br/camisa-azul.html"/>
<link rel="alternate" hreflang="x-default" href="https://example.com/en/blue-shirt.html"/>

As regras que importam:

  1. As anotações precisam ser recíprocas. Se a página em inglês aponta para a alemã, a alemã precisa apontar de volta. Sem os links de retorno, os buscadores ignoram o par.
  2. Cada URL precisa ser a URL canonical daquela versão. Apontar o hreflang para uma URL que redireciona ou que tem canonical para outro lugar quebra o conjunto.
  3. Os códigos de idioma são ISO 639-1, com uma região ISO 3166-1 opcional, como pt-BR. Os locales do Magento usam underscore (pt_BR), então precisam de conversão.
  4. O x-default marca a versão para usuários que não batem com nenhum idioma listado. Normalmente é a loja do idioma principal ou uma página de seleção de idioma.

Achando a URL equivalente em cada store view

Para produtos e categorias, a URL em outra store view vem do URL rewrite dela naquela loja. Produtos podem ter URL keys diferentes por store view, então não dá para só trocar a base URL. O serviço URL finder busca o rewrite de uma entidade numa determinada loja.

<?php
declare(strict_types=1);

namespace Vendor\Hreflang\Model;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Api\Data\StoreInterface;
use Magento\Store\Model\ScopeInterface;
use Magento\Store\Model\StoreManagerInterface;
use Magento\UrlRewrite\Model\UrlFinderInterface;
use Magento\UrlRewrite\Service\V1\Data\UrlRewrite;

class AlternateUrls
{
    public function __construct(
        private readonly UrlFinderInterface $urlFinder,
        private readonly StoreManagerInterface $storeManager,
        private readonly ScopeConfigInterface $scopeConfig
    ) {
    }

    /**
     * @return array<string, string>
     */
    public function forEntity(string $entityType, int $entityId): array
    {
        $current = $this->storeManager->getStore();
        $links = [];

        foreach ($this->storeManager->getStores() as $store) {
            if (!$store->getIsActive() || (int) $store->getWebsiteId() !== (int) $current->getWebsiteId()) {
                continue;
            }

            $path = $this->findRequestPath($entityType, $entityId, (int) $store->getId());
            if ($path === null) {
                continue;
            }

            $links[$this->getHreflangCode($store)] = $store->getBaseUrl() . $path;
        }

        return $links;
    }

    private function findRequestPath(string $entityType, int $entityId, int $storeId): ?string
    {
        $rewrites = $this->urlFinder->findAllByData([
            UrlRewrite::ENTITY_TYPE => $entityType,
            UrlRewrite::ENTITY_ID => $entityId,
            UrlRewrite::STORE_ID => $storeId,
            UrlRewrite::REDIRECT_TYPE => 0,
        ]);

        foreach ($rewrites as $rewrite) {
            if (empty($rewrite->getMetadata())) {
                return $rewrite->getRequestPath();
            }
        }

        return null;
    }

    private function getHreflangCode(StoreInterface $store): string
    {
        $locale = (string) $this->scopeConfig->getValue(
            'general/locale/code',
            ScopeInterface::SCOPE_STORE,
            $store->getId()
        );

        return str_replace('_', '-', $locale);
    }
}

Algumas notas sobre esse código:

  • Rewrites com metadata são as variantes de URL de produto com caminho de categoria. Ignorá-los dá a URL curta do produto, que também deve ser a canonical.
  • REDIRECT_TYPE => 0 exclui as entradas 301 que sobram de mudanças de URL key.
  • Se um produto não está atribuído a um website, ou está desativado numa store view, não haverá página utilizável ali. Checar só o rewrite não basta para produtos desativados, porque os rewrites podem continuar existindo. Carregue o status do produto por loja ou filtre por uma product collection antes de montar a lista.
  • O exemplo limita os links ao website atual. Se seus idiomas ficam em websites diferentes, remova essa checagem, mas garanta que o produto exista em cada um.

Para os tipos de entidade, use product, category e cms-page. Os dois primeiros são definidos como constantes em ProductUrlRewriteGenerator e CategoryUrlRewriteGenerator. Quando dois locales mapeiam para o mesmo código, por exemplo duas store views em inglês para EUA e Reino Unido, use locales com região como en_US e en_GB para os códigos continuarem únicos.

Renderizando os links

Adicione um block ao container head.additional nos layouts de catalog_product_view, catalog_category_view e cms_page_view. O block recebe a entidade atual, chama o serviço e imprime um link por entrada, mais o x-default. Escape toda URL com $escaper->escapeUrl().

Esses links fazem parte da página em cache, e o cache de página é por store view e por URL, então isso está correto. Quando uma URL key muda numa store view, os alternates nas outras store views mudam também. Para produtos e categorias isso já é tratado, porque salvar a entidade limpa a cache tag dela em todas as store views. Para páginas CMS ligadas por um mapeamento próprio, a mudança numa página não limpa as irmãs, então adicione ao block as cache tags de todas as páginas do grupo ou limpe-as num observer de save.

Páginas CMS são a parte difícil

Produtos e categorias são a mesma entidade em todas as store views, com valores traduzidos. Páginas CMS não são. Uma loja multi-idioma típica tem uma página CMS por idioma, cada uma uma entidade separada com ID e identifier próprios, como about-us e ueber-uns. O Magento não tem nenhuma ligação nativa entre elas.

A solução mais limpa é um campo de mapeamento. Adicione uma coluna como hreflang_group em cms_page por declarative schema, exponha no formulário da página CMS e dê o mesmo valor de grupo às versões traduzidas. O block de hreflang então encontra todas as páginas ativas com o mesmo grupo e monta os links a partir dos identifiers e das atribuições de loja. Como adicionar a coluna está em declarative schema e data patches.

Uma página CMS atribuída a "All Store Views" com o mesmo conteúdo em todos os idiomas não deve ter hreflang nenhum, já que na prática é uma página só.

Hreflang no sitemap XML

Em vez de links no head, você pode listar os alternates dentro do sitemap XML com elementos xhtml:link por URL. Isso deixa o HTML mais leve e é mais fácil de gerar para catálogos grandes num lote só. O gerador de sitemap do Magento não faz isso de fábrica, então é preciso estender o model do sitemap ou gerar um sitemap separado. Use um método só, head ou sitemap, e não os dois, para evitar sinais conflitantes quando um deles estiver desatualizado.

Erros comuns

  • Hreflang apontando para a home do outro idioma em vez da página equivalente.
  • Usar o store code como código de idioma, como hreflang="br", que não é um código válido para português.
  • Incluir store views desativadas ou ainda em construção.
  • Links para URLs de produto com caminho de categoria quando a canonical aponta para a URL curta.
  • Conteúdo traduzido só em parte, com descrições de produto em inglês na loja alemã. O hreflang não resolve conteúdo fraco ou duplicado.

Depois do release, confira os relatórios de segmentação internacional e faça crawl de uma amostra de páginas para confirmar a reciprocidade. O hreflang é um item da lista maior em checklist de SEO técnico, e a tradução em si está em traduções no Magento 2.

FAQ

O Magento 2 adiciona tags hreflang automaticamente?

Não. O Magento 2.4 não gera tags hreflang. Você precisa de um módulo próprio ou uma extensão que monte os links alternativos para produtos, categorias e páginas CMS.

Qual código de idioma devo usar numa store view do Magento?

Pegue o locale da store view em general/locale/code e troque o underscore por hífen, então pt_BR vira pt-BR. Garanta que cada store view do conjunto tenha um código único.

Como ligo páginas CMS traduzidas para o hreflang?

O Magento não tem ligação entre páginas CMS de idiomas diferentes. Adicione um campo de grupo compartilhado à página CMS e use-o para achar as páginas equivalentes.

Posso trocar o idioma do Magento por cookie e ainda usar hreflang?

Não. Cada versão de idioma precisa da própria URL. Use store codes na URL, subdomínios ou domínios separados.

Atendo como freelancer em Magento e Adobe Commerce, incluindo configurações multi-idioma e multi store. Se você precisa de ajuda com isso, fale comigo.

Artigos relacionados