Tengo un blog bilingüe: español e inglés. Está construido con Astro y desplegado en Cloudflare Pages. Sin ads, sin cookies, sin trackers invasivos, sin paywall. Solo contenido técnico basado en experiencia real.
El problema es que un sitio puede verse bien y, aun así, seguir siendo opaco para Google. Si Google no entiende el idioma de una página, su versión canónica o su relación con el resto del sitio, ese contenido se vuelve mucho más difícil de descubrir.
Este artículo documenta los cambios concretos que implementé para llevar CRP.gi de un sitio atractivo pero poco claro para buscadores a uno correctamente estructurado para indexarse en dos idiomas. No hay teoría aquí. Son cambios reales que hice en mi propio repositorio, junto con los errores que me mostraron qué era lo importante.
El punto de partida: qué tenía y qué faltaba
Cuando lancé CRP.gi, tenía cubierto lo básico: artículos en Markdown, frontmatter con títulos y fechas, un layout de Astro que renderizaba todo y Cloudflare Pages haciendo deploy automático desde GitHub. Funcionaba. Se veía bien.
Pero al revisarlo con criterio SEO, la lista de carencias fue más larga de lo que esperaba:
- Ningún artículo tenía meta description
- No había etiquetas
hreflangpara indicar que/es/espinaca-de-popeye/y/en/espinaca-de-popeye/eran contenido equivalente en distintos idiomas - No había datos estructurados en JSON-LD
- Los artículos en inglés se renderizaban con
lang="es"en el HTML - No había canonical URLs explícitas
- No existía un redirect de
www.crp.giacrp.gi - No había enlazado interno entre artículos relacionados
- No existía una página “Empieza Aquí” para orientar al visitante nuevo
Cada uno de esos problemas tenía una solución práctica. Esto fue lo que cambié.
1. Meta descriptions: el punto más fácil para empezar
Cada artículo necesita un campo description en el frontmatter: una o dos oraciones, idealmente en torno a 150 o 160 caracteres, que puedan aparecer en los resultados de Google y en las vistas previas de redes sociales.
---
title: "Mi Artículo"
subtitle: "Un subtítulo descriptivo"
description: "Una descripción SEO diferente del subtítulo, optimizada para clics en resultados de búsqueda."
---
Reglas que seguí:
- La description debe complementar el subtitle, no repetirlo
- Debe ser breve; Google suele truncar snippets más largos
- Conviene escribirla primero para personas, porque eso también mejora su valor SEO
- Debe estar en el mismo idioma que el artículo
En mi caso, añadí description a los 38 artículos (19 en español y 19 en inglés) en una sola pasada. Ese campo ahora alimenta las meta tags, Open Graph, Twitter Cards y el JSON-LD.
En el layout de Astro, el <head> queda así:
<meta name="description" content={description || subtitle} />
<meta property="og:description" content={description || subtitle} />
<meta name="twitter:description" content={description || subtitle} />
El fallback a subtitle es intencional. Si un artículo todavía no tiene description, al menos conserva un valor útil por defecto.
2. El atributo lang: un bug silencioso, pero serio
Este fue el problema más dañino que encontré. Todos mis artículos en inglés se estaban renderizando con <html lang="es">. Cada página bajo /en/ le estaba diciendo a Google, en la práctica, que era contenido en español.
El bug estaba en src/pages/en/[slug].astro: el template de artículos en inglés tenía lang="es" fijado en el código en lugar de lang="en". Un typo de una sola letra que afectó 19 páginas.
La lección es simple: después de duplicar un template para otro idioma, verifica de inmediato el atributo lang. El usuario no lo ve, pero los buscadores sí.
3. Canonical URLs: un solo host, sin ambigüedad
Google necesita saber cuál es la versión oficial de cada URL. Si tu sitio responde tanto en crp.gi como en www.crp.gi, puede indexar ambas y dividir señales entre las dos.
La solución tiene dos partes.
En Astro, el canonical debe construirse con un dominio fijo en lugar de Astro.url.href, que puede reflejar el host del request:
const siteUrl = 'https://crp.gi';
const canonicalUrl = `${siteUrl}${Astro.url.pathname}`;
<link rel="canonical" href={canonicalUrl} />
<meta property="og:url" content={canonicalUrl} />
En Cloudflare, crea una Redirect Rule para mover el tráfico de www al dominio principal:
- Cloudflare Dashboard → Rules → Redirect Rules
- Nombre:
www to apex - Filtro: Hostname equals
www.crp.gi - Acción: Dynamic redirect a
concat("https://crp.gi", http.request.uri.path)con código 301
Eso garantiza que Google vea una sola versión de cada URL.
4. Hreflang: la señal que un sitio bilingüe no debería omitir
En un sitio bilingüe, hreflang es una de las formas más claras de decirle a Google que dos páginas son equivalentes en idiomas distintos.
En el <head> de cada página añadí tres etiquetas:
<!-- Self-referencing -->
<link rel="alternate" hreflang="es" href="https://crp.gi/es/mi-articulo/" />
<!-- Alternate language -->
<link rel="alternate" hreflang="en" href="https://crp.gi/en/my-article/" />
<!-- Default -->
<link rel="alternate" hreflang="x-default" href="https://crp.gi/es/mi-articulo/" />
La etiqueta x-default indica qué versión debería mostrarse cuando el idioma del usuario no coincide con ninguna de las versiones disponibles.
En Astro, esto va en el layout base. La URL alternativa debe pasarse como prop desde cada página:
// En [slug].astro
const alternateUrl = post.data.alternateSlug
? `https://crp.gi/en/${post.data.alternateSlug}/`
: `https://crp.gi/en/${post.data.slug}/`;
Importante: el hreflang debe ser recíproco. Si la página en español apunta a la versión en inglés, la versión en inglés debe apuntar de vuelta a la española. Si esa relación se rompe, Google puede ignorar la señal.
Esto también aplica a páginas estáticas como About, Contact y Start Here, no solo a artículos.
5. JSON-LD: datos estructurados que pueden abrir la puerta a rich results
El JSON-LD le dice a Google qué tipo de contenido contiene una página. Para un artículo de blog, el tipo correcto es BlogPosting.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "Mi Artículo",
"description": "Descripción SEO del artículo",
"author": {
"@type": "Person",
"name": "Cesar Rosa Polanco",
"url": "https://crp.gi/es/about/",
"sameAs": "https://www.linkedin.com/in/cesarrosapolanco/"
},
"datePublished": "2026-04-03T00:00:00+01:00",
"dateModified": "2026-04-03T00:00:00+01:00",
"image": "https://crp.gi/images/articles/mi-articulo.webp",
"inLanguage": "es",
"url": "https://crp.gi/es/mi-articulo/",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://crp.gi/es/mi-articulo/"
},
"publisher": {
"@type": "Organization",
"name": "CRP.gi",
"url": "https://crp.gi/",
"logo": {
"@type": "ImageObject",
"url": "https://crp.gi/favicon.svg"
}
}
}
</script>
Detalles que importan:
datePublishedydateModifieddeberían incluir timezone. Sin ella, algunos validadores pueden mostrar advertencias o interpretar las fechas con menos claridad.author.sameAsapuntando a tu LinkedIn ayuda a conectar tu identidad entre plataformas.inLanguagedebe coincidir con el valor delangen la página.- Genera el JSON-LD dinámicamente desde el frontmatter del template de Astro en lugar de hardcodearlo.
Después del deploy, valídalo con Rich Results Test para confirmar que Google lo interpreta correctamente.
6. Sitemap y robots.txt
Astro puede generar el sitemap automáticamente con @astrojs/sitemap. Asegúrate de que astro.config.mjs incluya el dominio correcto:
export default defineConfig({
site: 'https://crp.gi',
integrations: [sitemap()],
});
Eso produce un sitemap-index.xml con todas las páginas. Envíalo en Google Search Console, dentro de Sitemaps.
Para robots.txt, basta con un archivo sencillo en public/robots.txt:
User-agent: *
Allow: /
Sitemap: https://crp.gi/sitemap-index.xml
7. Enlazado interno: conecta el contenido por clusters
Los enlaces internos ayudan a distribuir autoridad entre páginas y le dan a Google más contexto sobre cómo encaja tu contenido. Un bloque de artículos relacionados al final suma, pero los enlaces más valiosos suelen ser los que aparecen de forma natural dentro del cuerpo del texto.
Organicé el contenido en clusters temáticos:
- IA práctica: Espinaca de Popeye ↔ Anatomía de un Buen Prompt ↔ IA Viene por tus Tareas
- Ciberseguridad: Phishing ↔ MTA-STS ↔ IA y CyberSec en la Oficina ↔ El Hack a McKinsey
- OpenClaw: Implementación → Parte 2; El Día de los $83 → ambos artículos de OpenClaw
- Infraestructura: Deployment de Red → SSO ↔ MTA-STS; Dashboard MS365 → SSO
Dentro de cada artículo, busqué referencias naturales donde enlazar otro post realmente aportara contexto. Por ejemplo, en el artículo sobre phishing, cuando menciono protección del correo, enlazar a MTA-STS se siente útil, no forzado.
Para el bloque de artículos relacionados, la lógica del template busca posts que compartan categoría. Si un artículo usa la categoría ai-cybersec, puede mostrar relacionados tanto de ai como de cybersec.
8. Google Search Console: solicita indexación manual cuando importe
Después de cualquier cambio importante, vale la pena solicitar indexación en Google Search Console:
- Abre Search Console y usa la barra de búsqueda
- Pega la URL completa del artículo
- Haz clic en Request Indexing
Google no permite solicitudes manuales ilimitadas, así que conviene priorizar las páginas que recibieron cambios estructurales importantes, como la corrección del lang.
El sitemap que ya enviaste hará que Google vuelva a rastrear el sitio con el tiempo, pero las solicitudes manuales pueden acelerar el proceso.
9. Una página “Empieza Aquí”: orienta al visitante nuevo
Un blog con más de 20 artículos en dos idiomas puede resultar abrumador. Una página “Empieza Aquí” le da al visitante nuevo un punto de entrada claro y le ayuda a entender por dónde empezar.
La mía incluye:
- Una explicación breve de qué es CRP.gi
- Artículos esenciales agrupados por tema
- El único artículo que recomendaría primero en cada cluster
También funciona como un hub de enlazado interno, concentrando autoridad y redistribuyéndola hacia las páginas más fuertes.
10. Lo que no hice, y por qué
No instalé Google Analytics con cookies. Cloudflare Web Analytics es gratuito, respetuoso con la privacidad y no exige banner de cookies. Para un blog personal, eso basta.
No puse ads. El contenido es el producto. Si el artículo merece ser leído, la gente vuelve. Si está saturado de anuncios, no.
No hice keyword stuffing. Los títulos y las descriptions deberían reflejar lo que el artículo realmente dice, no lo que sugiera una herramienta de keywords. Google entiende sinónimos y contexto bastante bien.
No compré backlinks ni hice outreach masivo. Escribo el tipo de contenido que me habría gustado encontrar cuando tenía el problema. Si de verdad aporta valor, los enlaces llegan solos con el tiempo.
Checklist final
Si estás montando un blog bilingüe en Astro + Cloudflare Pages, esta es la lista:
- Meta descriptions en todos los artículos, idealmente entre 150 y 160 caracteres y distintas del subtitle
-
langcorrecto en el HTML de cada página (espara español,enpara inglés) - Canonical URLs construidas con tu dominio fijo, no con el host del request
- Un redirect 301 de
wwwal dominio principal en Cloudflare Rules -
hreflangrecíproco en todas las páginas (ES↔EN +x-default) - JSON-LD tipo
BlogPostingcon fechas en ISO y timezone - Sitemap autogenerado y enviado a Search Console
-
robots.txtdentro de/public/ - Enlaces internos dentro del cuerpo del artículo, no solo al final
- Una página “Empieza Aquí” que funcione como hub de navegación
- Solicitudes manuales de indexación para páginas con cambios estructurales críticos
- Validación post-deploy con Rich Results Test
Cada punto de esta lista es algo que implementé, probé y verifiqué en producción. No hay teoría aquí, solo lo que me funcionó.
Cesar Rosa Polanco - Escrito a partir de experiencia real, con asistencia de inteligencia artificial como herramienta de amplificación cognitiva.