WordPress bilíngue sem plugin: como fiz o prefixo /en com rewrite rules
O DigitalRadar é bilíngue desde o começo, mas até outubro o idioma vinha de um parâmetro na URL: ?lang=en, guardado em cookie. Funcionava para quem navegava — e não funcionava para o Google. Robô não carrega cookie. Para o buscador existia uma URL só, com dois conteúdos possíveis, o que é a definição de conteúdo duplicado.
A correção foi colocar o site em inglês sob o prefixo /en, sem instalar plugin de tradução. Este post é o caminho que segui — incluindo a abordagem que parece óbvia e quebra o site.
O que o prefixo precisa entregar
/ → home PT /en/ → home EN
/slug-do-post/ → post PT /en/english-slug/ → post EN
/category/noticias/ → arquivo PT /en/category/noticias/ → arquivo EN
Com três condições inegociáveis: paginação funcionando nos dois lados, canonical apontando para a URL certa, e as URLs antigas redirecionando sem perder o que já estava indexado.
A abordagem que quebra (e é a mais comum)
O atalho natural é interceptar a requisição cedo, cortar o /en da REQUEST_URI e deixar o WordPress processar o resto normalmente:
// NÃO faça isso
$_SERVER['REQUEST_URI'] = preg_replace('#^/en#', '', $_SERVER['REQUEST_URI']);
Funciona nos primeiros cinco minutos e quebra em três lugares depois:
- Paginação.
paginate_links()monta os links a partir daREQUEST_URI. Se você a modificou, a página 2 de um arquivo EN aponta para a URL sem prefixo. $wp->request. Muitos temas e plugins usam essa propriedade para montar o canonical. Sem o prefixo, o canonical da página EN aponta para a PT.redirect_canonical. O WordPress compara a URL pedida com a que ele considera correta e, achando divergência, redireciona — tirando justamente o seu prefixo.
A abordagem que funciona: duplicar as regras
Em vez de mexer na URL que chega, eu ensino o WordPress a entender URLs com prefixo. Cada regra de rewrite ganha uma cópia prefixada que injeta uma query var de idioma:
function dr_rewrite_lang_rules( $rules ) {
$novas = array();
foreach ( $rules as $regex => $query ) {
$prefixada = 'en/' . ltrim( $regex, '^' );
if ( 0 === strpos( $query, 'index.php?' ) ) {
$novas[ $prefixada ] = 'index.php?dr_lang=en&'
. substr( $query, strlen( 'index.php?' ) );
}
}
$novas['en/?$'] = 'index.php?dr_lang=en'; // home do idioma
return $novas + $rules; // prefixadas primeiro
}
add_filter( 'rewrite_rules_array', 'dr_rewrite_lang_rules' );
Três detalhes fazem isso funcionar:
- Nenhum grupo de captura novo. Só o prefixo muda no regex, então as referências
$matches[1],$matches[2]das queries continuam alinhadas. - A ordem interna é preservada. O
foreachpercorre as regras na ordem original, então a hierarquia que o WordPress montou continua valendo dentro do bloco prefixado. $novas + $rules. A união de arrays em PHP mantém as chaves do primeiro, colocando as regras prefixadas — mais específicas — antes das originais.
E a regra da home do idioma (en/?$) vai depois do laço, para ficar por último entre as prefixadas: ela é a mais genérica e não pode capturar /en/qualquer-coisa.
Links: quem segue o conteúdo e quem segue a requisição
Essa distinção é o que evita link quebrado. O permalink de um post segue o idioma do conteúdo, não o da visita — um post inglês tem /en mesmo que você esteja navegando em português:
add_filter( 'post_link', function ( $url, $post ) {
return 'en' === dr_post_lang( $post ) ? dr_prefix_url( $url ) : $url;
}, 10, 2 );
Já arquivos de categoria e tag existem nos dois idiomas na mesma rota, então seguem o idioma da requisição. O mesmo vale para home_url() — com cuidado, porque ela é usada também pelo REST, pelo login e pelos feeds. No meu filtro, ficam de fora as chamadas de wp-json, wp-admin, wp-login, feeds, arquivos com extensão e — detalhe que custou um tempo até eu notar — URLs com âncora, para os @id do JSON-LD continuarem iguais nos dois idiomas.
Os 301, que é onde o tráfego se perde
Três redirecionamentos garantem uma URL por conteúdo:
?lang=en(esquema antigo) → a URL limpa com/en;- post em inglês aberto sem prefixo →
/en/english-slug/; - post em português aberto sob
/en→ URL limpa.
Uma armadilha aqui: se a home for uma página estática marcada como português, a regra do segundo item manda /en/ de volta para / e a home inglesa desaparece. O guard é simples e obrigatório:
if ( is_singular() && ! is_front_page() ) { /* ... */ }
E, para o redirect_canonical do WordPress não derrubar o prefixo, cancelo o redirecionamento quando ele tenta remover o /en.
O que o prefixo resolveu de graça
Dois ganhos que eu não tinha planejado. O cache ficou mais simples: antes era preciso configurar o LiteSpeed para variar a cópia por cookie de idioma; agora cada idioma tem a própria URL e o cache funciona sozinho. E o hreflang na home e nos arquivos passou a ser automático — a mesma rota com e sem /en é o par, sem depender de vínculo manual entre posts.
Perguntas frequentes
Dá para fazer site bilíngue no WordPress sem plugin?
Dá, se você aceita manter um post por idioma e ligar os dois por um campo personalizado. O trabalho está nas URLs, no hreflang e no filtro das listagens — não na tradução em si.
Por que não reescrever a REQUEST_URI?
Porque paginação, $wp->request e o canonical do WordPress são montados a partir dela. Alterá-la faz esses três apontarem para a URL sem prefixo.
Preciso regravar as rewrite rules depois de mexer nelas?
Sim. As regras ficam salvas no banco. Ou você abre Configurações → Links Permanentes e salva, ou chama flush_rewrite_rules() uma vez, controlado por uma constante de versão.
O slug do post em inglês pode ser igual ao do português?
Não deve. O prefixo separa as rotas, mas slug próprio em inglês é o que faz a página ranquear para a busca em inglês — que é o motivo de existir a versão traduzida.
No DigitalRadar, o motor bilíngue é código próprio e a gente conta como ele funciona. Fique no radar.
Apurado a partir das fontes citadas e verificado antes da publicação. Veja a nossa Política Editorial.