Hacer un blog bilingue no es solo traducir texto. Si no diseñas bien rutas, canonical y navegación, terminas con contenido duplicado, indexación confusa y usuarios viendo mezcla de idiomas.
En este refactor resolvimos ese problema de punta a punta, y en este post te dejo el paso a paso con decisiones técnicas reales.
Problema inicial
Teníamos síntomas claros:
/blogmostraba mezcla ES/EN,- algunas rutas viejas seguían vivas,
- usuarios entraban por URL directa y no tenían experiencia consistente por idioma,
- Search Console mostraba canónicas alternativas y rutas duplicadas.
Objetivo: que cualquier URL genérica redirija a idioma correcto y que cada artículo tenga su par ES/EN bien declarado para Google.
Decisión de arquitectura
Tomamos estas decisiones:
- Rutas por idioma como primera clase:
/{lang}/... - Redirección inteligente para rutas genéricas (
/,/blog,/tags) hreflang+x-defaultpor artículo- Estado de idioma con cookie (
site_lang) - Páginas localizadas para home, tags y categorías
Paso 1: rutas localizadas
Creamos páginas localizadas:
src/pages/[lang]/index.astrosrc/pages/[lang]/blog/[...slug].astrosrc/pages/[lang]/blog/index.astrosrc/pages/[lang]/blog/category/index.astrosrc/pages/[lang]/tags/index.astrosrc/pages/[lang]/tags/[tag].astro
Resultado: cada contexto ahora vive en su idioma.
Paso 2: middleware para consistencia de idioma
El middleware resuelve idioma preferido con cookie + Accept-Language, y redirige rutas genéricas.
Ejemplo (simplificado):
const preferredLang = cookieLang ?? detectLangFromHeader(acceptLanguageHeader);
if (normalizedPath === "/") {
return Response.redirect(new URL(`/${preferredLang}/`, url.origin), 302);
}
if (normalizedPath === "/blog") {
return Response.redirect(new URL(`/${preferredLang}/blog/`, url.origin), 302);
}
Con eso dejamos de mostrar listados mezclados.
Paso 3: canonical y hreflang por artículo
Extendimos SEO para soportar alternates:
export interface LanguageAlternateEntry {
lang: string;
url: string;
}
En cada post calculamos alternates por translationKey.
const alternates = translations.map((translation) => ({
lang: translation.data.lang,
url: `${siteUrl}${translation.data.lang}/blog/${translation.id}/`,
}));
alternates.push({ lang: "x-default", url: `${siteUrl}en/blog/${defaultId}/` });
Google entiende mejor qué versión indexar por idioma.
Paso 4: navegación coherente
Ajustamos header para que conserve idioma al navegar:
- Home ->
/${lang} - Tags ->
/${lang}/tags - Categories ->
/${lang}/blog/category
También agregamos switcher ES | EN y mantenemos cookie de preferencia.
Paso 5: cleanup de contenido duplicado visual
Detectamos que algunos posts nuevos tenían bloque markdown de “Related” + componente visual de related cards (duplicado visual).
Se limpió el bloque markdown para dejar una sola experiencia de recomendados, consistente con el diseño.
Paso 6: reporte de cobertura de traducciones
Para no romper la paridad ES/EN agregamos script de auditoría:
pnpm run seo:report-translations
Genera:
docs/seo/translation-coverage.md
Esto ayuda a detectar translation keys incompletas antes de deploy.
Paso 7: QA técnico post-refactor
Checklist mínimo que usamos:
-
/blogredirige a/{lang}/blog/ -
/tagsredirige a/{lang}/tags/ - cada post muestra canonical correcta
-
hreflangincluye ES/EN + x-default - no hay mezcla de idiomas en listados
- build pasa sin errores
Errores comunes en este tipo de refactor
- Traducir contenido pero no rutas (SEO roto).
- Rutas localizadas sin canonical consistente.
- Menú que rompe el contexto de idioma.
- Redirecciones 302/301 mal encadenadas.
- No auditar cobertura de traducciones.
Qué publicaría como serie técnica
De este refactor salen al menos 3 posts fuertes:
- Arquitectura de rutas bilingues en Astro.
- SEO internacional práctico: canonical + hreflang sin humo.
- Estrategia de migración sin perder indexación.
Cierre
El valor de este refactor no fue “soportar dos idiomas”. Fue recuperar consistencia: para usuarios, para crawlers y para el mantenimiento técnico del proyecto.
Si estás por hacer algo parecido, no empieces por traducciones. Empezá por arquitectura de rutas, metadata y redirecciones.
Relacionado:
Happy reading! ☕
Comments