GEO con Next.js: Guía completa de implementación
intermediatePublicado 18 de abril de 2026 · Actualizado 13 de mayo de 2026
Next.js 15 implementa GEO de forma nativa: usa las convenciones de archivo robots.ts y sitemap.ts para el control de crawlers de IA, exporta metadata o generateMetadata() para las etiquetas head, y agrega JSON-LD via Script component. Crucialmente, permite OAI-SearchBot por separado de GPTBot en robots.ts — OAI-SearchBot es lo que usa ChatGPT Search para indexación en tiempo real.
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
getServerSidePropsogetStaticProps. 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
Next.js 15: robots.ts y sitemap.ts Nativos
Next.js 15 soporta robots.ts y sitemap.ts como convenciones de archivo nativas en el App Router.
robots.ts — Configuración de Crawlers de IA
// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{ userAgent: 'GPTBot', allow: '/' },
{ userAgent: 'OAI-SearchBot', allow: '/' }, // ChatGPT Search — separado de GPTBot
{ userAgent: 'PerplexityBot', allow: '/' },
{ userAgent: 'ClaudeBot', allow: '/' },
{ userAgent: 'Google-Extended', allow: '/' },
],
sitemap: 'https://tu-dominio.com/sitemap.xml',
}
}
Crítico: OAI-SearchBot (ChatGPT Search) es separado de GPTBot (entrenamiento). Bloquear GPTBot no permite ni bloquea automáticamente OAI-SearchBot. Permite ambos para visibilidad completa en ChatGPT.
Metadata en Streaming (Next.js 15)
generateMetadata() ahora soporta streaming — los metadatos se resuelven sin bloquear el renderizado de la UI.
Partial Prerendering y GEO
Para GEO, el shell estático es lo que ven los crawlers de IA. Asegúrate de que tu contenido principal esté en la parte estática, no dentro de los límites de <Suspense>.