SEO para un blog bilingüe en Astro + Cloudflare Pages: lo que implementé de verdad
Cloud

🔍 SEO para un blog bilingüe en Astro + Cloudflare Pages: lo que implementé de verdad

Un recorrido práctico por los cambios que hice para que Google entienda, indexe y posicione un sitio estático bilingüe

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:

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í:

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:

  1. Cloudflare Dashboard → Rules → Redirect Rules
  2. Nombre: www to apex
  3. Filtro: Hostname equals www.crp.gi
  4. 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:

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:

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:

  1. Abre Search Console y usa la barra de búsqueda
  2. Pega la URL completa del artículo
  3. 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:

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:

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.

¿Primera vez aquí?

Conoce los temas y artículos clave del blog.

Empieza Aquí →
← Volver a artículos Disponible en inglés →