Metadatos

Next.js tiene una API de Metadatos que puede utilizarse para definir los metadatos de tu aplicación (por ejemplo, etiquetas meta y link dentro del elemento head de tu HTML) para mejorar el SEO y la capacidad de compartir en la web.

Hay dos formas de agregar metadatos a tu aplicación:

  • Metadatos basados en configuración: Exporta un objeto estático metadata o una función dinámica generateMetadata en un archivo layout.js o page.js.
  • Metadatos basados en archivos: Agrega archivos especiales estáticos o generados dinámicamente a segmentos de ruta.

Con ambas opciones, Next.js generará automáticamente los elementos <head> relevantes para tus páginas. También puedes crear imágenes OG dinámicas usando el constructor ImageResponse.

Metadatos estáticos

Para definir metadatos estáticos, exporta un objeto Metadata desde un archivo layout.js o page.js estático.

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: '...',
  description: '...',
}

export default function Page() {}
export const metadata = {
  title: '...',
  description: '...',
}

export default function Page() {}

Para todas las opciones disponibles, consulta la Referencia de API.

Metadatos dinámicos

Puedes usar la función generateMetadata para obtener metadatos que requieran valores dinámicos.

import type { Metadata, ResolvingMetadata } from 'next'

type Props = {
  params: { id: string }
  searchParams: { [key: string]: string | string[] | undefined }
}

export async function generateMetadata(
  { params, searchParams }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {
  // leer parámetros de ruta
  const id = params.id

  // obtener datos
  const product = await fetch(`https://.../${id}`).then((res) => res.json())

  // opcionalmente acceder y extender (en lugar de reemplazar) metadatos padre
  const previousImages = (await parent).openGraph?.images || []

  return {
    title: product.title,
    openGraph: {
      images: ['/some-specific-page-image.jpg', ...previousImages],
    },
  }
}

export default function Page({ params, searchParams }: Props) {}
export async function generateMetadata({ params, searchParams }, parent) {
  // leer parámetros de ruta
  const id = params.id

  // obtener datos
  const product = await fetch(`https://.../${id}`).then((res) => res.json())

  // opcionalmente acceder y extender (en lugar de reemplazar) metadatos padre
  const previousImages = (await parent).openGraph?.images || []

  return {
    title: product.title,
    openGraph: {
      images: ['/some-specific-page-image.jpg', ...previousImages],
    },
  }
}

export default function Page({ params, searchParams }) {}

Para todos los parámetros disponibles, consulta la Referencia de API.

Nota importante:

  • Tanto los metadatos estáticos como los dinámicos a través de generateMetadata solo son compatibles en Componentes de Servidor.
  • Las solicitudes fetch se memorizan automáticamente para los mismos datos en generateMetadata, generateStaticParams, Layouts, Páginas y Componentes de Servidor. Se puede usar cache de React si fetch no está disponible.
  • Next.js esperará a que se complete la obtención de datos dentro de generateMetadata antes de transmitir la interfaz de usuario al cliente. Esto garantiza que la primera parte de una respuesta transmitida incluya las etiquetas <head>.

Metadatos basados en archivos

Estos archivos especiales están disponibles para metadatos:

Puedes usarlos para metadatos estáticos o generar estos archivos programáticamente con código.

Para implementación y ejemplos, consulta la Referencia de API de Archivos de Metadatos y Generación Dinámica de Imágenes.

Comportamiento

Los metadatos basados en archivos tienen mayor prioridad y anularán cualquier metadato basado en configuración.

Campos predeterminados

Hay dos etiquetas meta predeterminadas que siempre se agregan, incluso si una ruta no define metadatos:

<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

Nota importante: Puedes sobrescribir la etiqueta meta viewport predeterminada.

Orden

Los metadatos se evalúan en orden, comenzando desde el segmento raíz hasta el segmento más cercano al segmento final page.js. Por ejemplo:

  1. app/layout.tsx (Layout raíz)
  2. app/blog/layout.tsx (Layout anidado de blog)
  3. app/blog/[slug]/page.tsx (Página de blog)

Fusión

Siguiendo el orden de evaluación, los objetos Metadata exportados desde múltiples segmentos en la misma ruta se fusionan superficialmente para formar la salida final de metadatos de una ruta. Las claves duplicadas se reemplazan según su orden.

Esto significa que los metadatos con campos anidados como openGraph y robots que se definen en un segmento anterior son sobrescritos por el último segmento que los define.

Sobrescribir campos

app/layout.js
export const metadata = {
  title: 'Acme',
  openGraph: {
    title: 'Acme',
    description: 'Acme es...',
  },
}
app/blog/page.js
export const metadata = {
  title: 'Blog',
  openGraph: {
    title: 'Blog',
  },
}

// Salida:
// <title>Blog</title>
// <meta property="og:title" content="Blog" />

En el ejemplo anterior:

  • title de app/layout.js es reemplazado por title en app/blog/page.js.
  • Todos los campos openGraph de app/layout.js son reemplazados en app/blog/page.js porque app/blog/page.js establece metadatos openGraph. Nota la ausencia de openGraph.description.

Si deseas compartir algunos campos anidados entre segmentos mientras sobrescribes otros, puedes extraerlos en una variable separada:

app/shared-metadata.js
export const openGraphImage = { images: ['http://...'] }
app/page.js
import { openGraphImage } from './shared-metadata'

export const metadata = {
  openGraph: {
    ...openGraphImage,
    title: 'Inicio',
  },
}
app/about/page.js
import { openGraphImage } from '../shared-metadata'

export const metadata = {
  openGraph: {
    ...openGraphImage,
    title: 'Acerca de',
  },
}

En el ejemplo anterior, la imagen OG se comparte entre app/layout.js y app/about/page.js mientras que los títulos son diferentes.

Heredar campos

app/layout.js
export const metadata = {
  title: 'Acme',
  openGraph: {
    title: 'Acme',
    description: 'Acme es...',
  },
}
app/about/page.js
export const metadata = {
  title: 'Acerca de',
}

// Salida:
// <title>Acerca de</title>
// <meta property="og:title" content="Acme" />
// <meta property="og:description" content="Acme es..." />

Notas

  • title de app/layout.js es reemplazado por title en app/about/page.js.
  • Todos los campos openGraph de app/layout.js son heredados en app/about/page.js porque app/about/page.js no establece metadatos openGraph.

Generación Dinámica de Imágenes

El constructor ImageResponse te permite generar imágenes dinámicas usando JSX y CSS. Esto es útil para crear imágenes de redes sociales como imágenes Open Graph, tarjetas de Twitter y más.

ImageResponse usa el Edge Runtime, y Next.js agrega automáticamente los encabezados correctos para imágenes en caché en el edge, lo que ayuda a mejorar el rendimiento y reduce la recomputación.

Para usarlo, puedes importar ImageResponse desde next/og:

app/about/route.js
import { ImageResponse } from 'next/og'

export const runtime = 'edge'

export async function GET() {
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 128,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          textAlign: 'center',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        ¡Hola mundo!
      </div>
    ),
    {
      width: 1200,
      height: 600,
    }
  )
}

ImageResponse se integra bien con otras API de Next.js, incluyendo Route Handlers y metadatos basados en archivos. Por ejemplo, puedes usar ImageResponse en un archivo opengraph-image.tsx para generar imágenes Open Graph en tiempo de compilación o dinámicamente en tiempo de solicitud.

ImageResponse admite propiedades CSS comunes incluyendo flexbox y posicionamiento absoluto, fuentes personalizadas, ajuste de texto, centrado e imágenes anidadas. Consulta la lista completa de propiedades CSS admitidas.

Nota importante:

  • Hay ejemplos disponibles en el Vercel OG Playground.
  • ImageResponse usa @vercel/og, Satori y Resvg para convertir HTML y CSS en PNG.
  • Solo se admite el Edge Runtime. El runtime predeterminado de Node.js no funcionará.
  • Solo se admiten flexbox y un subconjunto de propiedades CSS. Los diseños avanzados (por ejemplo, display: grid) no funcionarán.
  • Tamaño máximo de paquete de 500KB. El tamaño del paquete incluye tu JSX, CSS, fuentes, imágenes y cualquier otro recurso. Si excedes el límite, considera reducir el tamaño de los recursos u obtenerlos en tiempo de ejecución.
  • Solo se admiten formatos de fuente ttf, otf y woff. Para maximizar la velocidad de análisis de fuentes, se prefieren ttf u otf sobre woff.

JSON-LD

JSON-LD es un formato para datos estructurados que los motores de búsqueda pueden usar para entender tu contenido. Por ejemplo, puedes usarlo para describir una persona, un evento, una organización, una película, un libro, una receta y muchos otros tipos de entidades.

Nuestra recomendación actual para JSON-LD es renderizar datos estructurados como una etiqueta <script> en tus componentes layout.js o page.js. Por ejemplo:

export default async function Page({ params }) {
  const product = await getProduct(params.id)

  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: product.name,
    image: product.image,
    description: product.description,
  }

  return (
    <section>
      {/* Agrega JSON-LD a tu página */}
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      {/* ... */}
    </section>
  )
}
export default async function Page({ params }) {
  const product = await getProduct(params.id)

  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: product.name,
    image: product.image,
    description: product.description,
  }

  return (
    <section>
      {/* Agrega JSON-LD a tu página */}
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      {/* ... */}
    </section>
  )
}

Puedes validar y probar tus datos estructurados con la Prueba de Resultados Enriquecidos de Google o el Validador de Marcado Schema genérico.

Puedes tipar tu JSON-LD con TypeScript usando paquetes de la comunidad como schema-dts:

import { Product, WithContext } from 'schema-dts'

const jsonLd: WithContext<Product> = {
  '@context': 'https://schema.org',
  '@type': 'Product',
  name: 'Next.js Sticker',
  image: 'https://nextjs.org/imgs/sticker.png',
  description: 'Dynamic at the speed of static.',
}