SEO en Next.js: Metadata API, Open Graph y Sitemap
Optimiza el SEO de tu app Next.js. Metadata API, Open Graph, Twitter Cards, sitemap dinamico, robots.txt y structured data con JSON-LD.
SEO en Next.js: Metadata API, Open Graph y Sitemap
El SEO en Next.js ya no requiere paquetes externos como next-seo. El App Router tiene una Metadata API nativa que genera meta tags, Open Graph, Twitter Cards y mas. Combinado con sitemap dinamico y structured data, tienes todo lo que Google necesita.
Metadata API basica
Exporta un objeto metadata desde cualquier page.tsx o layout.tsx:
// app/page.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Mi App - Inicio",
description: "Descripcion que aparece en Google. 150-160 caracteres ideal.",
keywords: ["nextjs", "react", "typescript"],
};Next.js genera automaticamente:
<title>Mi App - Inicio</title>
<meta name="description" content="Descripcion que aparece en Google..." />
<meta name="keywords" content="nextjs, react, typescript" />Metadata dinamica (paginas con [slug])
Para paginas dinamicas como posts de blog:
// 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,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
type: "article",
publishedTime: post.publishedAt,
images: [{ url: post.image, width: 1200, height: 630 }],
},
};
}Template de titulo
Define un template en el layout root para no repetir el nombre del sitio:
// app/layout.tsx
export const metadata: Metadata = {
title: {
default: "Mi App",
template: "%s | Mi App", // "Sobre Mi | Mi App"
},
};Open Graph y Twitter Cards
Controla como se ve tu pagina al compartirla:
export const metadata: Metadata = {
openGraph: {
type: "website",
title: "Mi App",
description: "Descripcion para redes sociales",
url: "https://miapp.com",
siteName: "Mi App",
locale: "es_ES",
images: [{
url: "https://miapp.com/og-image.jpg",
width: 1200,
height: 630,
alt: "Preview de Mi App",
}],
},
twitter: {
card: "summary_large_image",
title: "Mi App",
description: "Descripcion para Twitter",
images: ["https://miapp.com/og-image.jpg"],
},
};La imagen OG debe ser de 1200x630 px. Es lo que aparece como preview en Twitter, LinkedIn, WhatsApp y Slack.
Sitemap dinamico
// app/sitemap.ts
import type { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts(); // tu funcion para obtener posts
const blogUrls = posts.map((post) => ({
url: `https://miapp.com/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: "monthly" as const,
priority: 0.8,
}));
return [
{
url: "https://miapp.com",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 1,
},
{
url: "https://miapp.com/blog",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.9,
},
...blogUrls,
];
}Next.js lo sirve automaticamente en /sitemap.xml. Para mas detalles, revisa la guia de sitemap automatico en Next.js.
robots.txt
// app/robots.ts
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: "*",
allow: "/",
disallow: ["/api/", "/admin/"],
},
],
sitemap: "https://miapp.com/sitemap.xml",
};
}Structured Data (JSON-LD)
Google usa structured data para rich results. Para un blog, BlogPosting y FAQPage son los mas utiles:
// En tu page.tsx
const articleSchema = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
description: post.description,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
author: {
"@type": "Person",
name: "Rod Alexanderson",
},
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(articleSchema),
}}
/>
{/* Tu contenido */}
</>
);FAQPage para featured snippets
const faqSchema = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map((faq) => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: {
"@type": "Answer",
text: faq.answer,
},
})),
};Las FAQs con structured data tienen mas probabilidad de aparecer en "People Also Ask" de Google.
Checklist de SEO
| Elemento | Donde | Verificar |
|---|---|---|
| Title unico | Cada page.tsx | < 60 caracteres |
| Meta description | Cada page.tsx | 150-160 caracteres |
| Open Graph image | Paginas importantes | 1200x630px |
| Canonical URL | Paginas con contenido duplicado | alternates.canonical |
| Sitemap | /sitemap.xml | Todas las paginas incluidas |
| robots.txt | /robots.txt | No bloquea paginas importantes |
| JSON-LD | Posts del blog | BlogPosting + FAQPage |
| Headings | Contenido | H1 unico, H2/H3 jerarquicos |
Siguiente paso
El SEO tecnico es la base. Para que tu contenido rankee, necesitas contenido de calidad con buena estructura. Si deployeas en Vercel, la guia de deploy en Vercel cubre la configuracion completa incluyendo dominios y analytics.
Preguntas frecuentes
Como agrego meta tags en Next.js?
Con la Metadata API. Exportas un objeto metadata o una funcion generateMetadata desde tu page.tsx o layout.tsx. Next.js genera automaticamente los meta tags en el HTML.
Como genero un sitemap dinamico en Next.js?
Crea un archivo app/sitemap.ts que exporte una funcion. Esa funcion lee tus paginas (de la base de datos, del filesystem, etc.) y devuelve un array con las URLs. Next.js lo sirve como XML en /sitemap.xml automaticamente.
Que es Open Graph y por que importa?
Open Graph son meta tags que controlan como se ve tu pagina cuando alguien la comparte en redes sociales. Sin ellos, Twitter/LinkedIn/WhatsApp muestran un preview generico. Con ellos, muestran tu titulo, descripcion e imagen custom.
Necesito structured data (JSON-LD) en Next.js?
No es obligatorio, pero ayuda. Google usa structured data para entender mejor tu contenido y mostrarlo en rich results (FAQ, breadcrumbs, articulos). Para un blog, BlogPosting y FAQPage son los schemas mas utiles.