BookinglyTech News
Software

El App Router de Next.js ignoró 1.200 páginas generadas sin dar un solo error

Un desarrollador cuenta los tres fallos silenciosos que encontró al levantar 1.200 páginas estáticas con Next.js 16 App Router, Tailwind y exportación estática en Cloudflare Pages.

3 min de lecturaDev.to0 vistas

Un desarrollador tardó una semana en construir factorcalculator.org, una herramienta gratuita que calcula factores, pares de factores, factorización prima y divisores de cualquier número, con el desarrollo completo a la vista en vez de solo el resultado. Seis calculadoras hechas a mano y 1.200 páginas generadas, de /factors-of-1/ a /factors-of-1200/, sobre Next.js 16 App Router, Tailwind, exportación estática y despliegue en Cloudflare Pages. El despliegue salió. Tres cosas no, y dos de ellas fallaron en silencio hasta que dejaron de hacerlo.

El segmento dinámico que nunca fue dinámico

Quería URLs del tipo /factors-of-84/. La carpeta obvia era app/factors-of-[number]/page.js. Compila. Sin error, sin warning. Y produce exactamente una página: una ruta estática literal en /factors-of-%5Bnumber%5D/. Su generateStaticParams no se ejecutó nunca. No es que devolviera vacío: es que no llegó a correr. El App Router solo trata un segmento como dinámico cuando todo el segmento es una expresión entre corchetes. factors-of-[number] es una carpeta cuyo nombre da la casualidad de contener corchetes, y para el router eso es una ruta estática con un nombre raro.

La salida pasa por usar [slug] en la raíz y parsear el prefijo a mano. Dos detalles que importan. Primero, las rutas estáticas siguen ganando: Next.js resuelve antes los segmentos estáticos, así que /gcf-calculator/ no acaba en el comodín. Segundo, dynamicParams = false hace trabajo real. Sin esa bandera el segmento dinámico intentará renderizar /factors-of-99999999/ bajo demanda; con ella, todo lo que no esté en generateStaticParams devuelve 404, que es justo lo que quieres cuando tu espacio de URLs es finito y conocido. Añadió además un guard para ceros a la izquierda, porque /factors-of-012/ y /factors-of-0012/ son variantes infinitas de una página que ya existe. Contenido duplicado servido en bandeja: barato de bloquear, caro de limpiar después.

El desajuste de hidratación que volvía solo

toLocaleString() renderiza "1,234" en el servidor y "1 234" o "1.234" en ciertos locales del cliente. Dos cadenas distintas, un desajuste de hidratación. Todo lo que dependa del locale — Intl.NumberFormat, new Date(), Math.random() — es un riesgo en una página pre-renderizada, porque servidor y navegador no tienen por qué coincidir. Lo cambió por un formateo determinista con una expresión regular.

El problema reapareció en otros puntos a medida que el componente crecía. El arreglo de verdad fue dejar de perseguir la coincidencia entre servidor y cliente en un widget interactivo, y garantizarla directamente no renderizando nada interactivo hasta después del montaje: un estado mounted que arranca en false y pasa a true dentro de un useEffect, con un esqueleto estático mientras tanto. El HTML del servidor y el primer render del cliente salen idénticos, React hidrata limpio y el efecto cambia la pieza interactiva. El precio es explícito y conviene decirlo: la calculadora es invisible para cualquier cosa que no ejecute JavaScript. Le valió porque el contenido — lista de factores, pares, factorización prima, árbol — sí va renderizado en servidor en cada página de número. Solo la entrada interactiva queda detrás del montaje.

Queda un ruido menor: las extensiones del navegador inyectan atributos en el y React se queja. Eso no es culpa del código, se silencia con suppressHydrationWarning.

1.200 páginas que no fueran relleno

La versión fácil del SEO programático es una plantilla con una variable dentro, y da 1.200 páginas idénticas al 98%. Aquí ningún texto lo escribió un modelo de lenguaje ni salió de una lista de spintax.

Falta la parte que el autor promete y que casi nadie cuenta: qué hizo el tráfico real con esas 1.200 páginas. Eso es lo que decidirá si el trabajo sirvió.