Saltar al contenido
AldeaCode Logo
Generador de Slugs / Next.js Generadores 100% local

Routing dinámico con slugs en Next.js: [slug], generateStaticParams y App Router

El routing dinámico por slug en Next.js es el puente entre una base de datos de contenido y una URL pública. La mecánica es simple, los matices viven en la generación en build, en el comportamiento de fallback y en qué router usas. La mayoría no tienen nada que ver con Next.js y todo que ver con dónde vive el slug.

Las carpetas [slug] definen la forma de la ruta

En el App Router, una carpeta llamada [slug] bajo app/ se convierte en un segmento dinámico. La estructura app/blog/[slug]/page.tsx casa con /blog/lo-que-sea, y el valor encajado queda disponible en el param slug. Los corchetes son obligatorios; sin ellos el segmento se trata como path literal.

El Pages Router usa pages/blog/[slug].tsx en su lugar, con la misma convención de corchetes pero sin la carpeta envoltorio de page.tsx. Los dos routers conviven en el mismo proyecto: una ruta que existe en app/ gana sobre la misma ruta en pages/. Si estás migrando, empieza por una ruta cada vez en el App Router y deja que el Pages Router cubra el resto hasta que cierres la mudanza.

Para segmentos dinámicos anidados usa más carpetas: app/[lang]/blog/[slug]/page.tsx casa con /en/blog/welcome y recibes ambos params. Para rutas catch-all usa [...slug], que colapsa cualquier profundidad en un array. Esto va bien para sitios de documentación donde el slug es a su vez un path con barras.

generateStaticParams convierte slugs en páginas pre-renderizadas

En el App Router, le dices a Next.js qué slugs renderizar en build exportando una función async generateStaticParams desde el módulo de página. La función devuelve un array de objetos param; Next.js genera un HTML por objeto.

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await fetchAllPosts();
  return posts.map((p) => ({ slug: p.slug }));
}

Los slugs que devuelves tienen que coincidir con lo que la página espera en runtime. Si tu CMS guarda slugs sin prefijo de locale pero tus URLs incluyen /en, los params aquí son los de la forma del archivo ({ slug }), no la URL completa. Cabléalo con cuidado o verás warnings de "404 not generated" en build.

Las páginas no presentes en el array devuelto se manejan según el ajuste dynamicParams de la ruta. Por defecto true significa que los slugs desconocidos se renderizan en servidor bajo demanda. Pon false para que slugs desconocidos den 404 duro, que es lo que quieres en sitios estáticos de catálogo cerrado.

Los slugs necesitan una única fuente de verdad

El bug más común con slugs en Next.js es tener el slug en dos sitios. El CMS aplica una normalización, el sistema de archivos otra, y un refactor los deja desincronizados. Elige una fuente.

Para contenido CMS (Sanity, Contentful, Ghost) el CMS es la fuente. Slugifica siempre en el backend, guarda el slug como campo, exponlo por la API. Tu código Next.js lee el slug del CMS en build (en generateStaticParams) y en runtime (en el componente de página). Nunca re-derives el slug a partir del título en el frontend; el momento en que tu slugifier discrepe del del CMS, los enlaces se rompen.

Para contenido basado en archivos (MDX en una carpeta de contenido) el nombre del archivo es la fuente. Quita la extensión, no toques el resto. Resiste la tentación de re-normalizar el nombre en código; si quieres un slug distinto al del archivo, renombra el archivo.

useParams en App Router, getStaticPaths en Pages Router

En el App Router, el componente de página recibe params como prop. Desde un componente cliente, el hook useParams de next/navigation devuelve el mismo objeto. En Server Components, los params se pasan directamente:

// app/blog/[slug]/page.tsx (Server Component)
export default async function Page({
  params,
}: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await fetchPost(slug);
  if (!post) notFound();
  return <Article post={post} />;
}

Atento: params es una Promise en Next 15+. Hacer await es obligatorio; tratarlo como objeto síncrono compila pero avisa en runtime.

En el Pages Router, getStaticPaths cumple el papel de generateStaticParams y getStaticProps carga los datos. La forma es más verbosa pero el modelo es el mismo: elige los slugs en build, carga los datos por slug, renderiza una vez. La flag fallback controla el comportamiento en runtime para slugs desconocidos ('blocking' es el default seguro).

Ejemplo completo

tsx
// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";

interface Post {
  slug: string;
  title: string;
  content: string;
}

async function fetchAllPosts(): Promise<Post[]> {
  // Reemplaza con tu CMS, DB o lectura de archivos.
  const res = await fetch("https://cms.example.com/posts", {
    next: { revalidate: 3600 },
  });
  return res.json();
}

async function fetchPost(slug: string): Promise<Post | null> {
  const all = await fetchAllPosts();
  return all.find((p) => p.slug === slug) ?? null;
}

// Pre-renderiza cada slug conocido en build.
export async function generateStaticParams() {
  const posts = await fetchAllPosts();
  return posts.map((p) => ({ slug: p.slug }));
}

// Rechaza slugs desconocidos como 404 (apto para export estático).
export const dynamicParams = false;

export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await fetchPost(slug);
  if (!post) notFound();

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

¿Solo necesitas el resultado?

Cuando estás sembrando un blog o doc site en Next.js y quieres una lista limpia de slugs para tu CMS o tu carpeta MDX, el generador de slugs en aldeacode.com produce la forma kebab-case determinista. Pega tus títulos borrador, copia los slugs, guárdalos como fuente de verdad en tu capa de contenido. generateStaticParams los lee, el runtime los renderiza y no vuelves a re-slugificar en el frontend.

Abrir Generador de Slugs para URLs →

Preguntas frecuentes

¿Conviene slugificar títulos en la app Next.js o en el CMS?

En el CMS, siempre. Dos slugifiers divergen en detalles sutiles y en el momento en que discrepan, los enlaces se rompen. Guarda el slug como campo del registro, exponlo por la API, léelo desde Next.js sin más transformación.

¿Qué hace dynamicParams = false?

Le dice al App Router que devuelva 404 ante cualquier slug que no estuviera en la salida de generateStaticParams. Con dynamicParams = true (default), los slugs desconocidos se renderizan en servidor bajo demanda. False es correcto para catálogos cerrados; true es correcto para contenido que crece tras el deploy.

¿Por qué hay que hacer await de params en Next 15?

Los params pasaron a ser async para soportar streaming y pre-rendering parcial. El runtime devuelve una Promise; hacer await la desempaqueta. El acceso síncrono antiguo aún compila pero loguea aviso, y futuras versiones quitan el fallback. Actualiza las páginas existentes ya.