Introducción: cuando Google simplemente ignora su blog
Hace unas semanas me di cuenta de un problema molesto: los artículos publicados en este mismo blog no aparecían en Google. No estaban mal posicionados, no recibían poco tráfico, simplemente no estaban indexados. Al abrir Google Search Console, la situación era clara: de 32 URL enviadas en el mapa del sitio, solo 4 estaban indexadas (inicio, lista de blogs, proyectos, contactos). Cero artículos.
Lo que sigue es el camino real que tomé para diagnosticar y resolver el problema, desde la primera sospecha técnica hasta la causa oculta que nadie comprueba nunca: el contenido duplicado. Si ejecuta un blog técnico, especialmente en una aplicación Angular con renderizado del lado del servidor, probablemente reconocerá al menos uno de estos problemas.
Primera sospecha: páginas renderizadas en el lado del cliente en lugar del lado del servidor
La primera comprobación en estos casos es siempre la misma: ¿qué ve realmente el robot de Google cuando visita una página? En una aplicación Angular con SSR, si la representación del lado del servidor no funciona como se esperaba, el rastreador recibe solo el shell HTML vacío: sin específico, sin meta descripción, sin contenido textual real, todo completado a través de JavaScript después de los arranques de Angular.
En mi caso, la causa estaba en angular.json: el proyecto todavía usaba el indicador booleano heredado "prerender": true en lugar del más nuevo "outputMode": "server". Con el indicador booleano, Angular prepresenta solo rutas estáticas e ignora silenciosamente getPrerenderParams(), la función que expande rutas dinámicas como /blog/:slug al recuperar todos los slugs publicados por el backend. Resultado: todas las páginas estáticas (inicio, proyectos, contactos) se renderizaron previamente correctamente, pero cada publicación del blog siguió siendo una página puramente del lado del cliente.
// angular.json — prima (sbagliato)
"prerender": true
// angular.json — dopo (corretto)
"outputMode": "server"
Con outputMode: "server", Angular en realidad llama a getPrerenderParams() para cada ruta dinámica configurada en app.routes.server.ts, que en mi caso consulta GET /blog/posts en el backend y genera una página estática para cada slug publicado. Después de la corrección, la compilación finalmente produjo cada artículo como HTML real, con el título, la meta descripción y el artículo JSON-LD listos en el primer byte.
Error oculto de Apache: redirecciones 301 en cada página renderizada previamente
Una vez solucionado el prerenderizado, una verificación con un simple curl -I (no un navegador, que sigue los redireccionamientos de forma transparente y oculta el problema) reveló un segundo defecto: cada página prerenderizada respondió con 301 Moved Permanently hacia la misma URL con la barra diagonal agregada.
La causa es el comportamiento predeterminado de Apache, mod_dir: cuando una solicitud coincide con un directorio real en el disco (y eso es exactamente lo que crea el prerenderizado: /blog/article-name/index.html), Apache redirige automáticamente agregando la barra diagonal, antes de que las reglas mod_rewrite en .htaccess puedan intervenir. El contenido detrás de la redirección era correcto, pero la URL canónica declarada en cada página (sin una barra diagonal, de acuerdo con el mapa del sitio) nunca arrojó un 200 directo: los rastreadores siempre recibieron una URL diferente de la marcada como canónica.
<IfModule mod_dir.c>
DirectorySlash Off
</IfModule>
RewriteCond %{REQUEST_FILENAME} -d
RewriteCond %{REQUEST_FILENAME}/index.html -f
RewriteRule ^(.*)$ $1/index.html [L]
Al desactivar la barra automática y agregar una regla explícita que sirve index.html directamente en la URL sin barra, cada página prerenderizada comenzó a responder 200 en la URL canónica exacta.
Mapa de sitio estático versus mapa de sitio generado a partir de contenido real
Un tercer problema, más banal pero igualmente bloqueador: el mapa del sitio sólo contenía la página del listado/blog, no los artículos individuales. Cada nueva publicación publicada permaneció invisible para Google hasta que se descubrió a través de enlaces internos, un proceso lento y de ninguna manera garantizado.
La solución fue transformar el script de generación del mapa del sitio de una lista estática de rutas a una función que consulta el backend para cada publicación publicada:
async function fetchBlogRoutes() {
const res = await fetch(`${API_BASE_URL}/blog/posts?page=1&limit=50`);
const { data, meta } = await res.json();
const posts = [...data];
for (let page = 2; page <= meta.totalPages; page++) {
const r = await fetch(`${API_BASE_URL}/blog/posts?page=${page}&limit=50`);
const j = await r.json();
posts.push(...j.data);
}
return posts.map(p => ({ loc: `/blog/${p.slug}`, lastmod: p.updatedAt }));
}
El mismo script ahora también se ejecuta como GitHub Action, todas las noches y con cada pulsación, por lo que cada nuevo artículo publicado ingresa automáticamente al mapa del sitio sin intervención manual.
La causa más insidiosa: contenido duplicado
Después de todas estas correcciones técnicas, la situación mejoró pero no se resolvió: la mayoría de los artículos permanecieron en el estado "Detectado, actualmente no indexado": Google conocía las URL del mapa del sitio, pero aún no las había considerado lo suficientemente relevantes como para indexarlas. Al analizar la lista artículo por artículo, encontré dos publicaciones publicadas con 28 segundos de diferencia entre sí, exactamente sobre el mismo tema, con títulos casi indistinguibles y contenido casi superpuesto, probablemente una doble publicación accidental del panel del editor.
El contenido duplicado no bloquea la indexación de una sola página: es una señal de calidad evaluada a nivel de todo el dominio. En un sitio nuevo, con muy poca autoridad externa, un par de publicaciones duplicadas pueden ser suficientes para hacer que Google sea más cauteloso incluso con el contenido completamente original en el mismo dominio. Eliminé la versión con menos visitas y volví a generar el mapa del sitio: es probablemente la intervención con mayor impacto en la indexación general del sitio.
Cómo leer realmente el informe "Indexación de páginas" de Search Console
El informe de Google Search Console agrupa las páginas no indexadas por motivo y cada etiqueta requiere una acción diferente:
- Detectado, actualmente no indexado: Google conoce la URL (generalmente del mapa del sitio) pero aún no la ha rastreado. Es una cuestión de tiempo y presupuesto, no un error que corregir.
- Rastreada, actualmente no indexada: Google visitó la página pero decidió no indexarla, a menudo debido a que el contenido se considera de bajo valor o duplicado.
- Página alternativa con etiqueta canónica apropiada: normal para versiones traducidas del mismo contenido que apuntan correctamente a la versión principal, no es un error.
- Error de redirección: preste atención a la fecha del último escaneo. Si el error se solucionó después de esa fecha, el informe simplemente muestra datos desactualizados: simplemente verifique con curl -I que la URL responde 200 hoy, luego use "Validar corrección" para forzar una nueva verificación.
Lista de verificación práctica
- Comprueba con curl -I (no con el navegador) que cada página responde directamente a la URL canónica, sin redirecciones ocultas.
- Compruebe que el contenido recibido por un rastreador sea idéntico al de un navegador real: curl -Un "Googlebot" debe devolver HTML completo, no un shell vacío esperando JavaScript.
- Genere el mapa del sitio dinámicamente a partir del contenido publicado, no a partir de una lista estática de rutas.
- Busque títulos o temas casi idénticos en sus publicaciones: el contenido duplicado es un problema de dominio, no de una sola página.
- Utilice "Solicitar indexación" en Search Console en publicaciones más recientes en lugar de esperar el rastreo natural, que puede llevar semanas en un dominio joven.
Cronogramas realistas
En un dominio nuevo, con pocos signos de autoridad externa, es normal que la primera indexación demore desde unos pocos días (con solicitud manual) hasta varias semanas (mediante rastreo natural). Lo que realmente importa es eliminar los obstáculos técnicos y de calidad del contenido: una vez eliminados, sólo es cuestión de darle tiempo a Google para revisar y confiar en el dominio.