Rutas Dinámicas
Usa la sintaxis de corchetes para crear rutas que coincidan con segmentos de URL dinámicos - parámetros individuales, segmentos catch-all y patrones catch-all opcionales.
Busca en todas las páginas de la documentación
Usa la sintaxis de corchetes para crear rutas que coincidan con segmentos de URL dinámicos - parámetros individuales, segmentos catch-all y patrones catch-all opcionales.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de referencia rápida - lista para copiar y pegar.
app/
├── blog/[slug]/page.tsx # /blog/hello-world → { slug: "hello-world" }
├── docs/[...path]/page.tsx # /docs/a/b/c → { path: ["a", "b", "c"] }
└── shop/[[...categories]]/page.tsx # /shop o /shop/a/b → { categories: ["a", "b"] } o {}
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>Post: {slug}</h1>;
}Cuándo usarlo: Cualquier ruta donde la URL contiene una variable - IDs de productos, nombres de usuario, rutas de documentación, o prefijos de idioma.
// app/blog/[slug]/page.tsx - Segmento dinámico individual
import { notFound } from "next/navigation";
interface Post {
slug: string;
title: string;
content: string;
}
async function getPost(slug: string): Promise<Post | null> {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600 },
});
if (!res.ok) return null;
return res.json();
}
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// Generación estática para slugs conocidos
export async function generateStaticParams() {
const posts: Post[] = await fetch("https://api.example.com/posts").then((r) =>
r.json()
);
return posts.map((post) => ({ slug: post.slug }));
}// app/docs/[...path]/page.tsx - Ruta catch-all
export default async function DocsPage({
params,
}: {
params: Promise<{ path: string[] }>;
}) {
const { path } = await params;
// /docs/getting-started/install → path = ["getting-started", "install"]
const fullPath = path.join("/");
return (
<div>
<h1>Docs: {fullPath}</h1>
<p>Segmentos: {path.length}</p>
</div>
);
}// app/shop/[[...categories]]/page.tsx - Catch-all opcional
export default async function ShopPage({
params,
}: {
params: Promise<{ categories?: string[] }>;
}) {
const { categories } = await params;
if (!categories || categories.length === 0) {
return <h1>Todos los Productos</h1>;
}
return (
<div>
<h1>Shop: {categories.join(" > ")}</h1>
<p>Filtrando por {categories.length} categorías</p>
</div>
);
}[slug] coincide con un segmento único. /blog/hello coincide, /blog/hello/comments no.[...path] coincide con uno o más segmentos. /docs/a coincide, /docs/a/b/c coincide, pero /docs solo NO coincide.[[...path]] coincide con cero o más segmentos. Igual que catch-all, pero también coincide con la ruta base (/shop sin segmentos).Promise en Next.js 15+. Debes await params en Server Components o use(params) en Client Components.generateStaticParams pre-renderiza rutas dinámicas en tiempo de compilación. Devuelve un array de objetos de parámetros, y Next.js genera una página estática para cada uno.app/[lang]/blog/[slug]/page.tsx produce params: { lang, slug }.not-found.tsx cuando ninguna página coincide, o cuando llamas notFound().// Múltiples segmentos dinámicos
// app/[locale]/blog/[slug]/page.tsx
export default async function LocalizedPost({
params,
}: {
params: Promise<{ locale: string; slug: string }>;
}) {
const { locale, slug } = await params;
return <h1>{locale}: {slug}</h1>;
}
export async function generateStaticParams() {
return [
{ locale: "en", slug: "hello" },
{ locale: "fr", slug: "bonjour" },
];
}// generateStaticParams con parámetros del padre
// app/[category]/[product]/page.tsx
export async function generateStaticParams({
params,
}: {
params: { category: string };
}) {
const products = await getProductsByCategory(params.category);
return products.map((p) => ({ product: p.slug }));
}// Ruta dinámica con generateMetadata
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post?.title ?? "Not Found",
description: post?.content?.slice(0, 160),
};
}// Tipos de parámetros por patrón
type SingleParam = { slug: string }; // [slug]
type CatchAllParam = { path: string[] }; // [...path]
type OptionalCatchAll = { categories?: string[] }; // [[...categories]]
type MultiParam = { locale: string; slug: string }; // [locale]/[slug]
// Todos envueltos en Promise para las páginas de Next.js 15+
type PageProps = {
params: Promise<SingleParam>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
// Tipo de retorno de generateStaticParams
// Devuelve objetos de parámetros SIN el envoltorio Promise
type StaticParams = SingleParam[];[...path] NO coincide con la ruta base. /docs devolverá 404 a menos que tengas app/docs/page.tsx por separado. Usa [[...path]] si necesitas la base./product/42 da { id: "42" }, no un número. Analiza manualmente.generateStaticParams se ejecuta en tiempo de compilación. Si tu fuente de datos no está disponible durante la compilación, la compilación falla. Usa dynamicParams = true (el valor predeterminado) para permitir renderizado bajo demanda./blog/about coincide con app/blog/about/page.tsx antes de app/blog/[slug]/page.tsx.{ params: { slug } } directamente en la firma de la función ya no funciona en Next.js 15+. Siempre await.dynamicParams = false devuelve 404 para parámetros no listados. Solo funcionarán los caminos devueltos por generateStaticParams.// Optar por no renderizado dinámico bajo demanda
export const dynamicParams = false;| Enfoque | Cuándo Usarlo |
|---|---|
Ruta estática (app/about/page.tsx) | La URL es fija y conocida de antemano |
Grupos de ruta (group) | Organizar sin agregar segmentos de URL |
| Reescrituras de middleware | Mapear URLs personalizadas a rutas dinámicas existentes |
searchParams en lugar de params de ruta | Filtrado u ordenamiento que no necesita URLs únicas |
[slug] coincide exactamente con un segmento de URL (p. ej., /blog/hello)[...path] coincide con uno o más segmentos (p. ej., /docs/a/b/c) pero no con la ruta base[[...path]] coincide con cero o más segmentos, incluida la ruta base (p. ej., /shop o /shop/a/b)Catch-all [...path] requiere al menos un segmento. /docs solo no coincide. Crea un app/docs/page.tsx separado para la ruta base o cambia a catch-all opcional [[...path]].
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>{slug}</h1>;
}Debes await params porque ahora son una Promise.
dynamicParams = falseSí. Incluso si la URL contiene un número como /product/42, el valor del parámetro es el string "42". Debes analizarlo manualmente (p. ej., Number(id) o parseInt(id)).
Las rutas estáticas tienen prioridad. /blog/about coincide con app/blog/about/page.tsx antes de app/blog/[slug]/page.tsx.
Solo funcionarán los caminos devueltos por generateStaticParams. Cualquier otro camino dinámico devuelve un 404.
export const dynamicParams = false;import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return { title: post?.title ?? "Not Found" };
}type SingleParam = { slug: string };
type CatchAllParam = { path: string[] };
type OptionalCatchAll = { categories?: string[] };
type MultiParam = { locale: string; slug: string };
// Todos envueltos en Promise para props de página
type PageProps = {
params: Promise<SingleParam>;
};No en Next.js 15+. El patrón de desestructuración síncrono { params: { slug } } ya no funciona porque params ahora es una Promise. Siempre await params dentro del cuerpo de la función.
// app/[category]/[product]/page.tsx
export async function generateStaticParams({
params,
}: {
params: { category: string };
}) {
const products = await getProductsByCategory(params.category);
return products.map((p) => ({ product: p.slug }));
}Los parámetros del segmento padre se pasan al generateStaticParams del hijo.
Llama notFound() desde next/navigation para disparar el not-found.tsx más cercano.
import { notFound } from "next/navigation";
const post = await getPost(slug);
if (!post) notFound();Revisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥