Metadata y Scripts
NextJS tiene una API built-in para manejar metadata (title, description, Open Graph, etc.) y cargar scripts externos de forma optimizada.
Metadata API
Metadata estatica
Exporta un objeto metadata desde tu page.tsx o layout.tsx:
tsx
// app/layout.tsx
import type { Metadata } from "next"
export const metadata: Metadata = {
title: {
default: "Mi Tienda",
template: "%s | Mi Tienda", // Las paginas hijas usan esto
},
description: "La mejor tienda en linea de Mexico",
metadataBase: new URL("https://mitienda.com"),
openGraph: {
type: "website",
locale: "es_MX",
siteName: "Mi Tienda",
},
}
tsx
// app/productos/page.tsx
import type { Metadata } from "next"
export const metadata: Metadata = {
title: "Productos", // Resultado: "Productos | Mi Tienda"
description: "Todos nuestros productos disponibles",
}
Metadata dinamica
Para paginas con contenido dinamico, usa generateMetadata:
tsx
// 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.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.image, width: 1200, height: 630 }],
},
twitter: {
card: "summary_large_image",
},
}
}
Metadata comunes
tsx
export const metadata: Metadata = {
// Basicos
title: "Titulo de la pagina",
description: "Descripcion para Google",
// Open Graph (Facebook, LinkedIn, WhatsApp)
openGraph: {
title: "Titulo para compartir",
description: "Descripcion para compartir",
images: ["/og-image.png"],
locale: "es_MX",
type: "website",
},
// Twitter/X
twitter: {
card: "summary_large_image",
title: "Titulo para Twitter",
description: "Descripcion para Twitter",
},
// Robots
robots: {
index: true,
follow: true,
},
// Canonical URL
alternates: {
canonical: "https://mitienda.com/productos",
},
// Verificacion
verification: {
google: "tu-codigo-de-google",
},
}
Structured Data (JSON-LD)
Para rich snippets en Google, agrega JSON-LD:
tsx
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
const jsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
description: post.excerpt,
datePublished: post.publishedAt,
author: {
"@type": "Person",
name: "Tu Nombre",
},
}
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
</>
)
}
Sitemap y robots.txt
Sitemap dinamico
tsx
// app/sitemap.ts
import type { MetadataRoute } from "next"
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts()
const blogUrls = posts.map((post) => ({
url: `https://mitienda.com/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: "monthly" as const,
priority: 0.7,
}))
return [
{
url: "https://mitienda.com",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 1.0,
},
...blogUrls,
]
}
Robots.txt
tsx
// app/robots.ts
import type { MetadataRoute } from "next"
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
disallow: ["/api/", "/admin/"],
},
sitemap: "https://mitienda.com/sitemap.xml",
}
}
Scripts con next/script
Para cargar scripts externos (analytics, chat widgets, etc.):
tsx
import Script from "next/script"
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="es">
<body>
{children}
{/* Se carga despues de que la pagina es interactiva */}
<Script
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX"
strategy="afterInteractive"
/>
{/* Se carga cuando el browser esta idle */}
<Script
src="https://widget-de-chat.js"
strategy="lazyOnload"
/>
</body>
</html>
)
}
Estrategias:
beforeInteractive— Antes del hidratacion (casi nunca lo necesitas)afterInteractive— Despues de que la pagina carga (default, para analytics)lazyOnload— Cuando el browser no tiene nada que hacer (chat widgets, ads)
Ejemplo: SEO completo para un blog
tsx
// app/layout.tsx
import type { Metadata } from "next"
export const metadata: Metadata = {
metadataBase: new URL("https://miblog.com"),
title: {
default: "Mi Blog de Tecnologia",
template: "%s | Mi Blog",
},
description: "Articulos sobre desarrollo web en espanol",
openGraph: {
type: "website",
locale: "es_MX",
siteName: "Mi Blog",
images: [{ url: "/og-default.png", width: 1200, height: 630 }],
},
twitter: {
card: "summary_large_image",
creator: "@tuusuario",
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
},
},
}
Con esta configuracion, todas las paginas heredan el SEO base. Las paginas hijas pueden sobreescribir lo que necesiten con su propio metadata o generateMetadata.