GEO con Next.js: Guía completa de implementación

intermediate

Next.js App Router implementa GEO via la Metadata API: exporta una función generateMetadata() que retorna title, description, openGraph y articleDates. Añade JSON-LD via un componente Script con type='application/ld+json'. Next.js genera HTML estático en tiempo de build, que los crawlers de IA pueden leer.

GEO con Next.js: Guía completa de implementación

Next.js App Router implementa GEO via la Metadata API: exporta una función generateMetadata() que retorna title, description, openGraph y articleDates. Añade JSON-LD via un componente Script con type="application/ld+json". Next.js genera HTML estático en tiempo de build, que los crawlers de IA pueden leer.

Next.js es adecuado para GEO porque App Router realiza server-side rendering por defecto, produciendo HTML estático que los crawlers de IA pueden procesar sin ejecutar JavaScript.

Implementación de página (App Router)

// app/guia-geo/page.tsx
import type { Metadata } from 'next'
import Script from 'next/script'

export const metadata: Metadata = {
  title: 'Cómo implementar GEO en Next.js | Mi Sitio',
  description: 'GEO en Next.js requiere JSON-LD, Metadata API y SSR. Guía completa con ejemplos.',
  authors: [{ name: 'Mi Empresa', url: 'https://misitio.com/about' }],
  alternates: {
    canonical: 'https://misitio.com/guia-geo',
  },
  openGraph: {
    type: 'article',
    title: 'Cómo implementar GEO en Next.js',
    description: 'Guía técnica completa de GEO para Next.js',
    url: 'https://misitio.com/guia-geo',
    siteName: 'Mi Sitio',
    images: [
      {
        url: 'https://misitio.com/og/guia-geo.jpg',
        width: 1200,
        height: 630,
      },
    ],
    locale: 'es_ES',
    publishedTime: '2026-04-18T00:00:00Z',
    modifiedTime: '2026-04-18T00:00:00Z',
    authors: ['https://misitio.com/author/mi-empresa'],
    section: 'Guías técnicas',
    tags: ['GEO', 'Next.js', 'Optimización IA'],
  },
  robots: { index: true, follow: true },
}

export default function Page() {
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: 'Cómo implementar GEO en Next.js',
    description: 'Guía técnica completa de GEO para Next.js',
    author: {
      '@type': 'Organization',
      name: 'Mi Empresa',
    },
    publisher: {
      '@type': 'Organization',
      name: 'Mi Sitio',
      logo: {
        '@type': 'ImageObject',
        url: 'https://misitio.com/logo.png',
      },
    },
    datePublished: '2026-04-18T00:00:00Z',
    dateModified: '2026-04-18T00:00:00Z',
    mainEntityOfPage: {
      '@type': 'WebPage',
      '@id': 'https://misitio.com/guia-geo',
    },
  }

  return (
    <>
      <Script
        id="article-schema"
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <main>
        <article>
          <h1>Cómo implementar GEO en Next.js</h1>
          {/* Pirámide invertida: respuesta directa primero */}
          <p>
            GEO en Next.js se implementa via la Metadata API para meta tags y
            componentes Script para JSON-LD. App Router provee SSR por defecto,
            haciendo las páginas inmediatamente accesibles a los crawlers de IA.
          </p>
        </article>
      </main>
    </>
  )
}

Metadata dinámica con generateMetadata

Para páginas basadas en contenido (posts de blog, documentación), usa generateMetadata() para generar metadata dinámicamente desde tu fuente de datos:

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'

interface Props {
  params: { slug: string }
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const post = await getPost(params.slug)

  return {
    title: `${post.title} | Mi Sitio`,
    description: post.excerpt,
    alternates: { canonical: `https://misitio.com/blog/${params.slug}` },
    openGraph: {
      type: 'article',
      title: post.title,
      description: post.excerpt,
      url: `https://misitio.com/blog/${params.slug}`,
      publishedTime: post.publishedAt,
      modifiedTime: post.updatedAt,
      authors: [post.authorUrl],
      section: post.category,
      tags: post.tags,
    },
  }
}

Metadata del layout raíz

Establece defaults para todo el sitio en el layout raíz:

// app/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  metadataBase: new URL('https://misitio.com'),
  title: {
    default: 'Mi Sitio',
    template: '%s | Mi Sitio',
  },
  description: 'Descripción por defecto del sitio para páginas sin descripciones específicas.',
  openGraph: {
    siteName: 'Mi Sitio',
    locale: 'es_ES',
  },
  robots: {
    index: true,
    follow: true,
  },
}

robots.ts (App Router)

Crea /app/robots.ts para generación programática de robots.txt:

// app/robots.ts
import type { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      { userAgent: 'GPTBot', allow: '/' },
      { userAgent: 'OAI-SearchBot', allow: '/' },
      { userAgent: 'ClaudeBot', allow: '/' },
      { userAgent: 'Claude-User', allow: '/' },
      { userAgent: 'Claude-SearchBot', allow: '/' },
      { userAgent: 'PerplexityBot', allow: '/' },
      { userAgent: 'Google-Extended', allow: '/' },
      { userAgent: 'BingBot', allow: '/' },
      { userAgent: '*', allow: '/' },
    ],
    sitemap: 'https://misitio.com/sitemap.xml',
  }
}

sitemap.ts

// app/sitemap.ts
import type { MetadataRoute } from 'next'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts()

  return [
    {
      url: 'https://misitio.com',
      lastModified: new Date(),
      priority: 1,
    },
    ...posts.map(post => ({
      url: `https://misitio.com/blog/${post.slug}`,
      lastModified: new Date(post.updatedAt),
      priority: 0.8,
    })),
  ]
}

Pages Router (legacy)

Si usas Pages Router, usa getStaticProps o getServerSideProps (nunca solo CSR) e inyecta metadata via next/head:

// pages/guia-geo.tsx
import Head from 'next/head'

export default function GuiaGeo({ post }) {
  return (
    <>
      <Head>
        <title>{post.title} | Mi Sitio</title>
        <meta name="description" content={post.excerpt} />
        <meta property="og:type" content="article" />
        <meta property="article:published_time" content={post.publishedAt} />
        <meta property="article:modified_time" content={post.updatedAt} />
        <link rel="canonical" href={`https://misitio.com/${post.slug}`} />
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{
            __html: JSON.stringify({
              '@context': 'https://schema.org',
              '@type': 'Article',
              headline: post.title,
              datePublished: post.publishedAt,
              dateModified: post.updatedAt,
            })
          }}
        />
      </Head>
      <article>{/* contenido */}</article>
    </>
  )
}

Requisito: App Router usa SSR por defecto. Con Pages Router usar getServerSideProps o getStaticProps. Nunca CSR puro para contenido indexable.

Checklist GEO para Next.js

  • App Router con SSR (default) — nunca usar 'use client' en páginas SEO
  • Metadata API: template de title, description, alternates.canonical
  • openGraph con publishedTime y modifiedTime
  • JSON-LD via componente Script o dangerouslySetInnerHTML
  • robots.ts con los 8 crawlers de IA explícitamente permitidos
  • sitemap.ts con fechas lastModified de la fuente de contenido
  • metadataBase configurado en el layout raíz
  • Estructura de contenido en pirámide invertida en componentes de página
  • Core Web Vitals: LCP < 2.5s, INP < 200ms, CLS < 0.1