Tres semanas después de publicar un sitemap dinámico que orgullosamente listaba cada perfil de miembro, Google Search Console me dijo que esos perfiles eran un error. No con un error de plano, sino con dos veredictos más callados: Soft 404 y Duplicada sin canónica seleccionada por el usuario.
Esta es la historia de leer ese reporte, separar el ruido de la
única señal real, y el arreglo, que fue quitar páginas del índice, no agregarlas.
TL;DR
- El reporte "Por qué las páginas no se indexan" de Google es ~80% benigno por diseño. Aprende a triarlo o vas a perseguir fantasmas.
- Mis perfiles de miembros salieron marcados como Soft 404 (uno) y Duplicada sin canónica seleccionada por el usuario (otro). La misma causa raíz: el perfil público anónimo es deliberadamente flaco, un esqueleto con el username, todo lo demás es PII oculta a quien no ha iniciado sesión. Flaco + casi idéntico entre usuarios se lee como "página vacía" y "clúster de duplicados".
- No puedes arreglar un Soft 404 enriqueciendo una página que por contrato no tienes permitido enriquecer. Así que el arreglo es
noindex,followen la captura, más sacar las URLs del sitemap (un sitemap que lista una URL noindex es una autocontradicción que Google va a señalar). - El modelo mental en una línea: una página que a propósito no tiene contenido para los visitantes anónimos no tiene nada que hacer en un índice construido para visitantes anónimos.
El reporte que lo empezó todo
El reporte de cobertura del índice, ordenado por número de páginas:
| Razón | Fuente | Páginas |
|---|---|---|
| Descubierta, actualmente sin indexar | Sistemas de Google | 55 |
| Duplicada sin canónica seleccionada por el usuario | Sitio web | 9 |
| Página alternativa con etiqueta canónica correcta | Sitio web | 3 |
| Página con redirección | Sitio web | 2 |
| Rastreada, actualmente sin indexar | Sistemas de Google | 2 |
| Soft 404 | Sitio web | 1 |
| Excluida por etiqueta 'noindex' | Sitio web | 1 |
| No encontrada (404) | Sitio web | 1 |
| Bloqueada por acceso prohibido (403) | Sitio web | 1 |
Noventa y tantas URLs "sin indexar". El instinto es entrar en pánico. La primera jugada correcta es acomodar cada renglón en benigno-por-diseño o señal-real, porque la mayor parte de esta lista es Google trabajando exactamente como se pretende.
Triaje: qué ignorar
Antes de tocar código, clasifiqué cada renglón. La mayoría no necesitaba acción:
-
Página con redirección (2):
http://→https://ywww.→ apex. Eso es canonicalización funcionando. Google me está diciendo que siguió la redirección e indexó el destino. Nada que arreglar. -
Excluida por etiqueta 'noindex' (1): la página de reset de
contraseña. Ese
noindexlo pusimos a propósito. Una palomita verde vestida de advertencia. -
No encontrada 404 (1): la raíz del origen de la API (el host
api.). No tiene homepage; no debería indexarse. Un rastreador encontró un enlace a ella una vez. Inofensivo. - Bloqueada 403 (1): una URL detrás de auth que regresa 403 a los rastreadores sin sesión. Correcto.
- Descubierta / Rastreada, actualmente sin indexar (57): el número grande y aterrador, y el más benigno de todos. Estas son las decisiones de crawl budget de Google mismo ("conocemos esta URL, no hemos priorizado indexarla"). No las arreglas directo; se resuelven conforme el sitio se gana la prioridad de rastreo a través de contenido y enlaces. Tratarlas como bugs es el clásico hoyo de conejo de Search Console.
Eso deja los únicos renglones que describen mis páginas portándose mal:
Duplicada sin canónica seleccionada por el usuario (9 + un 3
relacionado) y Soft 404 (1). Los dos, resultó, eran los mismos dos tipos de página.
Señal 1: las páginas de tag del blog (una falsa alarma, verificada)
Nueve de las URLs "Duplicada sin canónica" eran filtros de tag del blog: /blog?tag=TechCareers, /blog?tag=CloudComputing, y así. Las tres "Página alternativa con canónica correcta" tenían la misma forma
(/blog?tag=gestión, …).
El reflejo es "¡agrega etiquetas canónicas!". Pero revisé primero el
componente PageMeta, y ya hacía lo correcto:
function resolveCanonical(canonical?: string): string {
if (canonical) return canonical;
if (typeof window === "undefined") return SITE_URL;
const { origin, pathname } = window.location;
return `${origin}${pathname}`; // nota: pathname, sin query string
}
La canónica se construye desde origin + pathname: el query string se descarta. Así que cada /blog?tag=X se auto-reporta con canónica /blog. Ese es el estado final buscado: los filtros de tag son vistas flacas y duplicativas que no deberían indexarse por separado; deberían consolidarse en /blog. La división 9-contra-3 es nada más Google a media reclasificación: algunas URLs ya se habían asentado en "página alternativa
con canónica correcta" (la cubeta deseada), el resto seguía en la cubeta genérica de duplicados de camino para allá.
Lección: antes de "arreglar" una advertencia de canónica, confirma qué canónica emite la página en realidad. La mitad de estas advertencias son Google reportando un estado transitorio de algo que ya hiciste bien. Cerré esta como funcionando-como-se-diseñó y seguí adelante.
Señal 2: los perfiles (el bug real)
Las dos URLs restantes eran perfiles de miembros:
-
/profile/rafael-ortiz→ Soft 404 -
/profile/hectorC→ Duplicada sin canónica seleccionada por el usuario
Un Soft 404 significa que la página regresa HTTP 200 pero parece un
no-encontrado: vacía, flaca, o de relleno. Para ver por qué Google lo
pensó, aquí está el cuerpo rastreable entero de la captura de un perfil:
body = (
'<section class="mx-auto max-w-3xl py-8">'
f"<h1>@{escape(username)}</h1>"
f'<p class="text-xs text-white/50">{escape(meta_bits)}</p>' # rol · miembro-desde
'<p><a href="/">MEMBER COMMUNITY: tech community</a></p>'
"</section>"
)
Un username, una etiqueta de rol, una fecha de ingreso, un enlace a home.
Eso es todo, y es deliberadamente eso. El perfil público que se le
muestra a un visitante sin sesión está anonimizado por privacidad: nombre real, bio, avatar, especialidad, país, enlaces, posts recientes son todos PII, ocultos para viewer=None. La captura pre-renderizada se construye justo desde esa vista anónima, a propósito, para que el rastreador nunca pueda exponer más de lo que ve un humano sin sesión.
Ahora los dos veredictos tienen sentido como una sola causa raíz:
-
Soft 404: la página es tan flaca que Google decide que no hay
contenido real. A
rafael-ortizle tocó cruzar el umbral. -
Duplicada sin canónica: quítale el username y cada captura de perfil es byte por byte el mismo esqueleto. Google agrupa las páginas casi idénticas y elige una representante; al resto le cae el veredicto de duplicada. A
hectorCle tocó el palito corto.
Por qué los arreglos obvios están los dos mal
"Enriquece la página para que no sea flaca." No se puede. La flacura es una garantía de privacidad, no un descuido. Agregarle bio/avatar/posts a la captura filtraría PII a un HTML que los rastreadores sin sesión (y quien sea que vea el código fuente) pueden leer. La regla, la captura nunca expone más de lo que ve un humano sin sesión, es de carga estructural.
"Pon una canónica para que los duplicados se consoliden." Una canónica apunta los duplicados a una página representante. Pero no hay perfil representante: cada uno es una persona distinta con contenido (oculto) distinto. Y consolidar no tocaría el Soft 404. La canónica es la herramienta equivocada.
Así que ninguno de los dos arreglos orientados a indexar aplica. Lo que fuerza una pregunta más honesta: ¿estas páginas pertenecen a un índice de búsqueda siquiera? Una página que a un visitante anónimo le muestra nada más @username y un rol no tiene por qué rankear. Indexarla es puro ruido:
reportes de Soft 404, clústeres de duplicados, y crawl budget gastado
re-jalando esqueletos (alimentando esa pila de 55 URLs "Descubierta, sin indexar").
La respuesta es no. Sírvele la página a los humanos y a los desplegadores de enlaces en redes; dile a los rastreadores que no la indexen.
El arreglo: noindex,follow, y la contradicción del sitemap
Tres cambios.
1. Enséñale al core del pre-render a sobreescribir robots. El embudo compartido render_into_shell parcha el <head> del shell. Le di un argumento opcional robots que reemplaza quirúrgicamente el
<meta name="robots"> por default del shell:
if robots is not None:
out = re.sub(
r'<meta name="robots" content="[^"]*" />',
f'<meta name="robots" content="{escape(robots)}" />',
out,
count=1,
)
Default None → sin tocar, así que las capturas de blog / reportes /
eventos mantienen su index,follow. Solo los callers que se apuntan
cambian de comportamiento.
2. Apunta el adaptador de perfil.
return render_into_shell(
shell_html,
title=title,
description=description,
canonical=canonical,
body_html=body,
jsonld=_person_jsonld(profile, canonical),
og_type="profile",
robots="noindex,follow",
)
¿Por qué noindex,**follow** y no noindex,nofollow? Porque la página todavía enlaza hacia afuera (a home, y con el tiempo a contenido indexable). follow deja que el link equity fluya a través del perfil aunque el perfil mismo no se indexe. nofollow lo dejaría varado. La distinción importa: noindex es sobre esta página; follow es sobre las páginas a las que apunta.
3. Quita los perfiles del sitemap. Este es el paso que la gente se salta. Un sitemap es una lista de "por favor indexa estas". Un meta noindex es "por favor no indexes esta". Si una URL aparece en las dos, le entregaste a Google una contradicción, y va a sacar una advertencia nueva:
URL enviada marcada como 'noindex'. Acabas de cambiar dos advertencias por una nueva.
Así que el endpoint del sitemap de perfiles, y su entrada en el índice de sitemaps, salieron por completo:
<!--
Sin sitemap-profiles.xml: las capturas de perfil anónimas son noindex,follow
(esqueletos flacos de solo-username, PII oculta por diseño → Soft 404 / duplicada
en GSC). Listarlas aquí chocaría con el meta noindex.
-->
La ironía no se me escapa: la pasada de SEO anterior agregó ese sitemap de perfiles. Me tomó tres semanas de datos de rastreo reales aprender que publicar un sitemap para páginas que un rastreador no debería querer era el instinto equivocado. Search Console es un maestro lento pero honesto.
Pruebas de regresión
Dos afirmaciones fijan el comportamiento. Primero, la captura voltea robots:
def test_profile_snapshot_is_noindex():
html = render_profile_page(_anon_profile(), SHELL)
assert '<meta name="robots" content="noindex,follow" />' in html
assert '<meta name="robots" content="index, follow" />' not in html
Segundo, el endpoint del sitemap ya no existe (para que nunca pueda
derivar de vuelta en silencio a contradecir el meta):
async def test_sitemap_profiles_removed(client):
resp = await client.get("/sitemap-profiles.xml")
assert resp.status_code == 404
El fixture del shell de la prueba de noindex necesita la etiqueta
<meta name="robots"> real que carga el index.html de producción, algo fácil de olvidar, y sin ella el reemplazo de regex es un no-op silencioso que pasa por la razón equivocada.
Qué medir después
noindex es una señal, no un switch. Después de desplegar, vuelve a
renderizar las capturas vivas para que de verdad carguen el meta nuevo, luego en Search Console:
- Validar corrección tanto en el item de Soft 404 como en el de Duplicada. Esto le dice a Google que re-rastree el conjunto afectado.
- Observa los perfiles migrar a Excluida por etiqueta 'noindex': ese es el estado de éxito aquí, no un problema nuevo. Significa que Google vio el meta y obedeció.
- Confirma que el conteo de "Descubierta, actualmente sin indexar" va bajando conforme el crawl budget deja de gastarse re-jalando esqueletos.
Espera semanas, no horas. Los cambios de cobertura del índice se propagan en la agenda de Google.
La conclusión
El reporte de índice de Google mezcla "tu
página está rota" con "Google trabaja como se pretende" con "Google todavía no llega a ella", y se ven casi idénticos. Acomodar cada renglón en benigno contra señal antes de abrir un editor me salvó de "arreglar" etiquetas canónicas que ya estaban correctas.
No toda página quiere estar indexada. Una página construida para
mostrarle nada a los visitantes anónimos no tiene por qué rankear, y
forzarla a un índice produce exactamente el ruido de Soft-404-y-duplicada que esperarías. La jugada madura de SEO a veces es sacar páginas fuera: noindex,follow en la página, y fuera del sitemap, juntos, para que las dos señales concuerden.
Top comments (0)