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.