Next.js
Metadata y SEO en App Router
Explica Metadata API, generateMetadata, títulos, canonicals, Open Graph, robots, JSON-LD y caché para describir correctamente cada ruta del App Router.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Next.js
Explica Metadata API, generateMetadata, títulos, canonicals, Open Graph, robots, JSON-LD y caché para describir correctamente cada ruta del App Router.
La Metadata API describe cada documento para navegadores, buscadores y redes sociales. Next.js puede componer metadata estática, generarla desde datos y transmitirla junto con el árbol. Esto facilita SEO técnico, pero no garantiza rastreo, indexación ni posicionamiento: el contenido y la accesibilidad del sitio siguen siendo determinantes.
root layout metadata
↓ inherited
nested layout metadata
↓ inherited/replaced
page metadata
↓
<head> tags for final routeLos campos se heredan por segmentos. Campos anidados como openGraph pueden reemplazarse completamente cuando un descendiente define su propio objeto; comparte constantes si necesitas preservar partes.
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Servicios de desarrollo web",
description: "Aplicaciones web, sistemas de gestión y MVPs construidos de principio a fin.",
};Solo se soporta en Server Components. No exportes metadata desde un archivo "use client".
Root layout:
export const metadata: Metadata = {
metadataBase: new URL("https://nicoo.dev"),
title: {
default: "Nicolás Garzón | Desarrollador Full Stack",
template: "%s | Nicolás Garzón",
},
description: "Desarrollo aplicaciones web de principio a fin.",
};Page:
export const metadata: Metadata = {
title: "Proyectos",
};Resultado: Proyectos | Nicolás Garzón.
Usa absolute cuando una página necesita ignorar el template.
Permite resolver URLs relativas:
export const metadata: Metadata = {
metadataBase: new URL("https://nicoo.dev"),
alternates: {
canonical: "/portfolio",
},
openGraph: {
images: ["/seo/og-portfolio.png"],
},
};No derives el dominio canónico de un Host no validado. Usa configuración segura por entorno.
import type { Metadata, ResolvingMetadata } from "next";
import { notFound } from "next/navigation";
export async function generateMetadata(
{ params }: PageProps<"/portfolio/[slug]">,
parent: ResolvingMetadata,
): Promise<Metadata> {
const { slug } = await params;
const project = await getProjectBySlug(slug);
if (!project) notFound();
const previousImages = (await parent).openGraph?.images ?? [];
return {
title: project.title,
description: project.seoDescription,
alternates: {
canonical: `/portfolio/${project.slug}`,
},
openGraph: {
title: project.title,
description: project.seoDescription,
type: "article",
images: project.ogImage
? [project.ogImage, ...previousImages]
: previousImages,
},
};
}params es asíncrono en el modelo actual. parent permite extender metadata, pero espera solo lo necesario.
generateMetadata y la page pueden consultar la misma entidad. fetch idéntico se memoiza, o usa React.cache para ORM/SDK:
export const getProjectBySlug = cache(async (slug: string) => {
return projectRepository.findPublishedBySlug(slug);
});No guardes el resultado en una variable global; mezcla requests/tenants.
Next.js puede transmitir metadata dinámica después del contenido inicial para navegadores compatibles, inyectando tags cuando se resuelve. Bots que necesitan metadata en el <head> pueden recibir comportamiento bloqueante según la lista htmlLimitedBots.
Normalmente no configures esa lista: sobrescribirla incorrectamente puede afectar streaming y TTFB. Verifica un bot real antes de cambiarla.
Debe describir la página y diferenciarla. Evita repetir keywords o usar el mismo título en todo el sitio.
Resume valor/contenido de esa URL. Los buscadores pueden mostrar otro fragmento según la consulta; no es garantía de snippet.
Longitudes no son límites rígidos. Prioriza claridad y evita cortar información esencial.
alternates: {
canonical: `/portfolio/${project.slug}`,
}Indica la URL preferida entre versiones equivalentes.
No uses canonical para ocultar páginas realmente diferentes. Si una URL vieja no debe utilizarse, redirect permanente suele ser más claro.
Asegura que:
alternates: {
canonical: "/services",
languages: {
"es-CO": "/es/servicios",
"en-US": "/en/services",
},
}Cada versión debe enlazar a las demás, incluido su propio idioma. No declares hreflang hacia traducciones incompletas o redirects inesperados.
robots: {
index: false,
follow: false,
googleBot: {
index: false,
follow: false,
},
}Útil para preview, resultados internos o contenido duplicado. noindex requiere que el crawler pueda acceder a la página para leerlo; bloquearla en robots.txt puede impedir que vea la directiva.
No uses SEO como control de acceso. Contenido privado necesita autenticación.
openGraph: {
type: "website",
locale: "es_CO",
url: "/portfolio",
siteName: "Nicolás Garzón",
title: "Proyectos",
description: "Aplicaciones web y sistemas de gestión.",
images: [
{
url: "/seo/og-portfolio.png",
width: 1200,
height: 630,
alt: "Vista de proyectos desarrollados por Nicolás Garzón",
},
],
},
twitter: {
card: "summary_large_image",
title: "Proyectos",
description: "Aplicaciones web y sistemas de gestión.",
images: ["/seo/og-portfolio.png"],
},Redes cachean agresivamente. Cambiar la imagen no actualiza necesariamente un share ya rastreado; utiliza sus debuggers y versiona URL si es necesario.
Puedes usar metadata o file conventions:
app/favicon.ico
app/icon.png
app/apple-icon.pngLas convenciones detectan tamaño/tipo. Para iconos generados, params son asíncronos bajo Next.js 16.
Viewport tiene una API separada:
import type { Viewport } from "next";
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#ffffff" },
{ media: "(prefers-color-scheme: dark)", color: "#09090b" },
],
};No deshabilites zoom con maximumScale=1 o userScalable=no; perjudica accesibilidad.
Metadata API no tiene un campo universal para JSON-LD. Renderiza un script en Server Component:
const jsonLd = {
"@context": "https://schema.org",
"@type": "Person",
name: "Nicolás Garzón",
url: "https://nicoo.dev",
};
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
}}
/>El replace reduce inyección de etiquetas al serializar datos no confiables. Valida los campos.
Structured data puede habilitar rich results, pero no los garantiza. Usa tipos que reflejen contenido visible.
Si generateMetadata usa datos externos con Cache Components, aplica use cache a la lectura cuando sea reutilizable o asegura que el árbol tiene una boundary/dinámica compatible.
No hagas una consulta distinta solo para SEO si puede compartir el loader autorizado de la page.
generateMetadata puede llamar notFound() o redirect(). Si una entidad no existe, evita producir metadata de un recurso falso.
No captures estas excepciones internas con un catch genérico.
Next.js facilita tags; no crea relevancia automáticamente.
Home:
canonical /
title absoluto de marca
Person JSON-LD
OG defaultPortfolio:
title "Proyectos"
canonical /portfolio
CollectionPage JSON-LD opcional
OG de colecciónDetalle:
project title/description from CMS
canonical slug
project image
noindex for draft/previewLa descripción SEO debe ser un campo editorial o una derivación controlada, no cortar arbitrariamente texto rico.
<head> en HTML y DevTools.curl con bots representativos.No es compatible; mantenla en servidor.
Comparte constantes o extiende explícitamente.
Host injection/URLs incorrectas.
Crawler no puede ver el noindex.
XSS.
Duplicación y baja utilidad.
Los buscadores modernos no dependen de esa lista como antes.
openGraph hijo puede perder campos del padre?<?Robots, sitemap y manifest publica políticas de rastreo, inventario de URLs y capacidades instalables del sitio.