SEO de verdad: 404 reales, una señal por página y sitemap completo
Las páginas que no existen vuelven a responder 404, cada página declara una sola directiva y un solo hreflang, y el idioma por defecto deja de redirigir a sí mismo. Medido antes y después.
Esta versión sale de pasar una auditoría técnica de SEO completa sobre una web en producción construida con el boilerplate. Casi todo lo que encontró venía de aquí, así que se arregla aquí. Cada punto se ha medido antes y después con el mismo script y los mismos datos.
Las páginas que no existen responden 404
Un post o una categoría inexistentes respondían 200 con la página de "no encontrado": un soft
404 que Google puede acabar indexando. La causa era un loading.tsx en la raíz de [locale]: con
Cache Components, en cuanto se pinta un Suspense el código de estado ya está enviado, y un
notFound() posterior no puede cambiarlo.
Ahora no hay límite de carga global. Cada sección tiene el suyo, y el blog resuelve el post o la
categoría con lecturas cacheadas antes de cualquier Suspense, siguiendo el patrón de la guía
de streaming de Next. Cada escritura de posts o categorías invalida esa caché, y su vida corta hace
que los posts programados aparezcan a su hora.
Una sola directiva por página
Cada página emitía dos etiquetas robots, y las páginas inexistentes, tres y contradictorias. Ahora
hay una, generada desde buildRobots(). De paso, /admin y /dashboard, que se declaraban
indexables, pasan a noindex, nofollow.
El idioma por defecto, en la raíz de verdad
Con localePrefix: 'as-needed' el idioma por defecto vive sin prefijo, pero los canonical se
construían con él: el canonical de la home apuntaba a /en, que redirigía a /. Ahora ningún
canonical, hreflang, sitemap ni enlace lleva el prefijo del idioma por defecto, y /en/* responde
con un 308 permanente en un solo salto.
Además, la raíz ya no redirige según el idioma del navegador: cada URL sirve su idioma y el visitante cambia con el selector. Es lo que recomiendan los buscadores.
Un solo hreflang, y lang desde el servidor
El hreflang se declaraba dos veces, en la cabecera HTTP y en el <head>, y sin la propia página.
Ahora va solo en el <head>, se incluye a sí mismo, x-default apunta al idioma por defecto y un
artículo sin traducción no declara ninguna. <html lang> sale del servidor en vez de ponerlo un
script al cargar.
Sitemap y robots.txt
El sitemap declara todos los idiomas, el por defecto sin prefijo, y un lastmod real en vez de la
hora a la que se generó. El robots.txt pasa a un solo bloque para todos los rastreadores y bloquea
los parámetros de prefetch de Next y de campaña.
Cuatro fallos más
- Títulos con la marca duplicada: «Blog | App | App».
- Los prefetch de Next compartían el límite de 300 peticiones por minuto con las páginas. Un listado precarga decenas de enlaces, así que visitantes reales podían recibir un 429. Ahora tienen su propio límite.
- Cada visita a un post cambiaba su fecha de modificación, así que
dateModifiedy el sitemap mentían. El contador de visitas ya no la toca. - Las imágenes del CDN salían con el dominio duplicado en
og:image.
Si actualizas desde la 2.0
- Borra
src/app/[locale]/loading.tsxsi lo tienes personalizado: las secciones llevan ahora el suyo y el esqueleto está en@/shared/components/page-skeleton. - Los layouts de
/adminy/dashboardpasan afeatures/admin/components/admin-shell.tsxyfeatures/dashboard/components/dashboard-shell.tsx, envueltos en un layout de servidor. - El listado del blog se mueve a
src/app/[locale]/blog/(index)/. La URL no cambia. - Si escribes posts o categorías desde código propio, llama a
revalidateTag(BLOG_CACHE_TAG, 'max')después, como hacen las rutas del admin. - Si quieres volver a redirigir la raíz según el idioma del navegador, quita
localeDetection: falsedesrc/i18n/routing.ts.
Las reglas nuevas están documentadas en src/lib/seo/CLAUDE.md, para ti y para tu agente.