Metadata - SEO y compartir
Metadata es la información sobre tu página que ven los motores de búsqueda y redes sociales. Un buen metadata mejora tu SEO y hace que tus links se vean bien al compartirlos.
¿Qué es metadata?
Son tags HTML en el <head>
de tu página:
<head>
<title>Mi Tienda - Los mejores productos</title>
<meta name="description" content="Compra productos de calidad...">
<meta property="og:image" content="https://mitienda.com/og-image.jpg">
</head>
Estos tags no los ve el usuario, pero sí:
- Google (para ranking)
- Facebook (cuando compartes un link)
- Twitter (preview de links)
- WhatsApp (preview de links)
Metadata estático
Para páginas con metadata fijo:
// app/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Mi Tienda - Los mejores productos',
description: 'Compra productos de alta calidad a los mejores precios',
}
export default function HomePage() {
return <div>Contenido</div>
}
NextJS automáticamente genera:
<head>
<title>Mi Tienda - Los mejores productos</title>
<meta name="description" content="Compra productos de alta calidad...">
</head>
Metadata dinámico
Para páginas con contenido dinámico:
// app/productos/[id]/page.tsx
import { Metadata } from 'next'
type Props = {
params: { id: string }
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
// Obtener datos del producto
const producto = await fetch(`https://api.mitienda.com/productos/${params.id}`)
.then(res => res.json())
return {
title: `${producto.nombre} - Mi Tienda`,
description: producto.descripcion,
openGraph: {
images: [producto.imagen],
},
}
}
export default function ProductoPage({ params }: Props) {
return <div>Página del producto</div>
}
NextJS espera a que generateMetadata
termine antes de enviar el HTML. Esto significa que Google ve el metadata correcto.
Propiedades básicas
title
export const metadata = {
title: 'Mi Página',
}
Genera:
<title>Mi Página</title>
description
export const metadata = {
description: 'Esta es una descripción de mi página para SEO',
}
Genera:
<meta name="description" content="Esta es una descripción...">
Longitud ideal
- Title: 50-60 caracteres (lo que muestra Google)
- Description: 150-160 caracteres
Si es más largo, Google lo corta con "..."
keywords (obsoleto)
export const metadata = {
keywords: ['tienda', 'productos', 'ecommerce'],
}
No lo uses. Google no usa keywords desde 2009. Es spam.
authors
export const metadata = {
authors: [{ name: 'Juan Pérez', url: 'https://juanperez.com' }],
}
creator
export const metadata = {
creator: 'Juan Pérez',
}
Template de títulos
Para evitar repetir el nombre del sitio:
// app/layout.tsx
export const metadata = {
title: {
default: 'Mi Tienda',
template: '%s | Mi Tienda',
},
}
// app/productos/page.tsx
export const metadata = {
title: 'Productos', // Resultado: "Productos | Mi Tienda"
}
// app/contacto/page.tsx
export const metadata = {
title: 'Contacto', // Resultado: "Contacto | Mi Tienda"
}
Absolute title
Para páginas que quieres título completo:
// app/page.tsx
export const metadata = {
title: {
absolute: 'Mi Tienda - Los mejores productos online',
},
// Ignora el template, usa este título exacto
}
Open Graph (Facebook, WhatsApp)
Open Graph es metadata para redes sociales:
export const metadata = {
title: 'Mi Producto Increíble',
description: 'Este producto es genial porque...',
openGraph: {
title: 'Mi Producto Increíble',
description: 'Este producto es genial porque...',
url: 'https://mitienda.com/productos/1',
siteName: 'Mi Tienda',
images: [
{
url: 'https://mitienda.com/og-image.jpg',
width: 1200,
height: 630,
alt: 'Mi Producto Increíble',
},
],
locale: 'es_ES',
type: 'website',
},
}
Vista previa cuando compartes en Facebook/WhatsApp:
┌─────────────────────────────────┐
│ [Imagen 1200x630] │
├─────────────────────────────────┤
│ Mi Producto Increíble │
│ Este producto es genial porque..│
│ mitienda.com │
└─────────────────────────────────┘
Tamaños de imagen recomendados
- Facebook/WhatsApp: 1200x630px
- Twitter: 1200x675px
- LinkedIn: 1200x627px
Formato: JPG o PNG, máximo 8 MB
Twitter Cards
export const metadata = {
twitter: {
card: 'summary_large_image',
title: 'Mi Producto Increíble',
description: 'Este producto es genial porque...',
creator: '@mitienda',
images: ['https://mitienda.com/twitter-image.jpg'],
},
}
Tipos de card:
'summary'
: Imagen cuadrada pequeña'summary_large_image'
: Imagen grande (recomendado)'app'
: Para promover apps'player'
: Para videos
Metadata para ecommerce
// app/productos/[id]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const producto = await getProducto(params.id)
return {
title: `${producto.nombre} - Mi Tienda`,
description: `Compra ${producto.nombre} por solo $${producto.precio}. ${producto.descripcionCorta}`,
openGraph: {
title: producto.nombre,
description: producto.descripcionCorta,
url: `https://mitienda.com/productos/${producto.id}`,
siteName: 'Mi Tienda',
images: [
{
url: producto.imagen,
width: 1200,
height: 630,
alt: producto.nombre,
},
],
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: producto.nombre,
description: producto.descripcionCorta,
images: [producto.imagen],
},
// Para Google Shopping
other: {
'product:price:amount': producto.precio.toString(),
'product:price:currency': 'USD',
'product:availability': producto.enStock ? 'in stock' : 'out of stock',
'product:condition': 'new',
},
}
}
Metadata para blog
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug)
return {
title: post.titulo,
description: post.extracto,
authors: [{ name: post.autor }],
openGraph: {
title: post.titulo,
description: post.extracto,
url: `https://miblog.com/blog/${post.slug}`,
siteName: 'Mi Blog',
images: [post.imagenDestacada],
type: 'article',
publishedTime: post.fechaPublicacion,
authors: [post.autor],
},
twitter: {
card: 'summary_large_image',
title: post.titulo,
description: post.extracto,
images: [post.imagenDestacada],
},
}
}
Icons y favicons
// app/layout.tsx
export const metadata = {
icons: {
icon: '/favicon.ico',
shortcut: '/favicon-16x16.png',
apple: '/apple-touch-icon.png',
other: {
rel: 'apple-touch-icon-precomposed',
url: '/apple-touch-icon-precomposed.png',
},
},
}
Estructura recomendada:
public/
├── favicon.ico (32x32)
├── favicon-16x16.png
├── favicon-32x32.png
├── apple-touch-icon.png (180x180)
└── android-chrome-192x192.png
Con route handlers
// app/icon.png/route.tsx
import { ImageResponse } from 'next/og'
export function GET() {
return new ImageResponse(
(
<div
style={{
fontSize: 24,
background: 'black',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
}}
>
MT
</div>
),
{
width: 32,
height: 32,
}
)
}
Robots y indexación
robots.txt
// app/robots.ts
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: '/admin/',
},
sitemap: 'https://mitienda.com/sitemap.xml',
}
}
Genera:
User-Agent: *
Allow: /
Disallow: /admin/
Sitemap: https://mitienda.com/sitemap.xml
Meta robots
export const metadata = {
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
}
No index (páginas privadas)
// app/admin/page.tsx
export const metadata = {
robots: {
index: false,
follow: false,
},
}
Sitemap
// app/sitemap.ts
import { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const productos = await fetch('https://api.mitienda.com/productos')
.then(res => res.json())
const productosUrls = productos.map((producto) => ({
url: `https://mitienda.com/productos/${producto.id}`,
lastModified: producto.updatedAt,
changeFrequency: 'daily' as const,
priority: 0.8,
}))
return [
{
url: 'https://mitienda.com',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
{
url: 'https://mitienda.com/productos',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 0.9,
},
...productosUrls,
]
}
Genera automáticamente: https://mitienda.com/sitemap.xml
Canonical URLs
Para evitar contenido duplicado:
export const metadata = {
alternates: {
canonical: 'https://mitienda.com/productos/camisa-azul',
},
}
Si tienes múltiples URLs para el mismo contenido:
https://mitienda.com/productos/1
https://mitienda.com/p/1
https://mitienda.com/productos/camisa-azul
Define una como canonical para decirle a Google cuál es la "oficial".
Alternate languages
Para sitios multiidioma:
export const metadata = {
alternates: {
canonical: 'https://mitienda.com/productos/1',
languages: {
'es-ES': 'https://mitienda.com/es/productos/1',
'en-US': 'https://mitienda.com/en/products/1',
'pt-BR': 'https://mitienda.com/pt/produtos/1',
},
},
}
Verification (verificación)
Para verificar tu sitio con Google, Bing, etc:
export const metadata = {
verification: {
google: 'google123abc',
yandex: 'yandex123abc',
yahoo: 'yahoo123abc',
other: {
me: ['email@example.com', 'https://example.com'],
},
},
}
App Links (deep linking)
Para apps móviles:
export const metadata = {
appLinks: {
ios: {
url: 'https://mitienda.com/productos/1',
app_store_id: 'app_store_id',
},
android: {
package: 'com.example.android/products/1',
app_name: 'Mi Tienda',
},
web: {
url: 'https://mitienda.com/productos/1',
},
},
}
Viewport y theme color
// app/layout.tsx
import { Metadata, Viewport } from 'next'
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
}
export const metadata: Metadata = {
themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#ffffff' },
{ media: '(prefers-color-scheme: dark)', color: '#000000' },
],
}
Metadata completo (ejemplo)
// app/productos/[id]/page.tsx
import { Metadata } from 'next'
type Props = {
params: { id: string }
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const producto = await fetch(`https://api.mitienda.com/productos/${params.id}`)
.then(res => res.json())
const url = `https://mitienda.com/productos/${params.id}`
return {
title: `${producto.nombre} - Mi Tienda`,
description: `Compra ${producto.nombre} por solo $${producto.precio}. ${producto.descripcionCorta}`,
keywords: [producto.nombre, producto.categoria, 'tienda online'],
authors: [{ name: 'Mi Tienda' }],
openGraph: {
title: producto.nombre,
description: producto.descripcionCorta,
url,
siteName: 'Mi Tienda',
images: [
{
url: producto.imagen,
width: 1200,
height: 630,
alt: producto.nombre,
},
],
locale: 'es_ES',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: producto.nombre,
description: producto.descripcionCorta,
creator: '@mitienda',
images: [producto.imagen],
},
robots: {
index: producto.visible,
follow: true,
},
alternates: {
canonical: url,
},
}
}
export default async function ProductoPage({ params }: Props) {
const producto = await getProducto(params.id)
return (
<div>
<h1>{producto.nombre}</h1>
<p>${producto.precio}</p>
</div>
)
}
Herramientas para probar
Open Graph
- Facebook Sharing Debugger: developers.facebook.com/tools/debug
- Pega tu URL y ve cómo se verá al compartir
- Twitter Card Validator: cards-dev.twitter.com/validator
- Ve preview de tu Twitter Card
- Rich Results Test: search.google.com/test/rich-results
- Ve cómo Google ve tu página
- Post Inspector: linkedin.com/post-inspector
- Preview de links en LinkedIn
Mejores prácticas SEO
1. Title único por página
// ❌ Mal: Mismo title en todas
export const metadata = {
title: 'Mi Tienda',
}
// ✅ Bien: Title descriptivo único
export const metadata = {
title: 'Camisa Azul de Algodón - Talla M | Mi Tienda',
}
2. Description que invita a click
// ❌ Mal: Genérico
description: 'Venta de productos online'
// ✅ Bien: Específico y con llamado a acción
description: 'Camisa azul de 100% algodón. Envío gratis en compras sobre $50. ¡Compra ahora con 20% de descuento!'
3. Imágenes Open Graph de calidad
// ❌ Mal: Imagen muy pequeña o sin especificar dimensiones
images: ['https://mitienda.com/logo.png']
// ✅ Bien: 1200x630, con alt text
images: [
{
url: 'https://mitienda.com/og-images/producto-1.jpg',
width: 1200,
height: 630,
alt: 'Camisa azul de algodón sobre fondo blanco',
},
]
4. URLs canónicas
// Siempre define canonical
alternates: {
canonical: 'https://mitienda.com/productos/camisa-azul',
}
5. Sitemap actualizado
Regenera tu sitemap cuando agregues/elimines páginas:
// Sitemap dinámico que se actualiza automáticamente
export default async function sitemap() {
const productos = await getProductosActivos()
return productos.map(p => ({
url: `https://mitienda.com/productos/${p.id}`,
lastModified: p.updatedAt,
}))
}
Errores comunes
Error: Metadata duplicado
// ❌ Mal: Metadata en layout Y en página duplica
// app/layout.tsx
export const metadata = {
title: 'Mi Tienda',
description: 'Descripción global',
}
// app/page.tsx
export const metadata = {
title: 'Mi Tienda', // Se sobrescribe
description: 'Descripción global', // Se sobrescribe
}
Solución: Usa template en layout, valores específicos en páginas.
Error: generateMetadata sin await
// ❌ Mal: Olvidas await
export async function generateMetadata({ params }) {
const producto = fetch(`/api/productos/${params.id}`).then(r => r.json())
return {
title: producto.nombre, // Error: producto es Promise
}
}
// ✅ Bien: Con await
export async function generateMetadata({ params }) {
const producto = await fetch(`/api/productos/${params.id}`).then(r => r.json())
return {
title: producto.nombre, // Funciona
}
}
Error: Imagen Open Graph sin dimensiones
// ❌ Mal: Sin width/height
openGraph: {
images: ['https://mitienda.com/image.jpg'],
}
// ✅ Bien: Con dimensiones
openGraph: {
images: [
{
url: 'https://mitienda.com/image.jpg',
width: 1200,
height: 630,
},
],
}
Recursos
- Google Search Central: Guías de SEO oficiales
- Schema.org: Structured data
- Open Graph Protocol: Documentación oficial
- Twitter Cards: Documentación de Twitter
Resumen
Metadata en NextJS:
- Define con
export const metadata
(estático) - O con
export async function generateMetadata
(dinámico) - Incluye title, description, Open Graph, Twitter
- Genera sitemap y robots.txt con route handlers
SEO básico:
- Title único: 50-60 caracteres
- Description: 150-160 caracteres
- Open Graph image: 1200x630px
- Canonical URLs
- Sitemap actualizado
Herramientas:
- Facebook Sharing Debugger
- Twitter Card Validator
- Google Rich Results Test
Regla de oro: Cada página debe tener title, description, e imagen Open Graph únicos y descriptivos. Esto mejora tu SEO y hace que tus links se vean bien al compartir.