GEO for Next.js: Complete Implementation Guide
intermediatePublished April 18, 2026 · Updated May 13, 2026
Next.js 15 implements GEO natively: use robots.ts and sitemap.ts file conventions for AI crawler control, export metadata or generateMetadata() for structured head tags, and add JSON-LD via a Script component. Critically, allow OAI-SearchBot separately from GPTBot in robots.ts — OAI-SearchBot is what ChatGPT Search uses for real-time indexing.
GEO for Next.js: Complete Implementation Guide
Next.js App Router implements GEO via the Metadata API: export a generateMetadata() function that returns title, description, openGraph, and articleDates. Add JSON-LD via a Script component with type="application/ld+json". Next.js generates static HTML at build time, which AI crawlers can read.
Next.js is well-suited for GEO because App Router performs server-side rendering by default, producing static HTML that AI crawlers can process without executing JavaScript.
Page Implementation (App Router)
// app/geo-guide/page.tsx
import type { Metadata } from 'next'
import Script from 'next/script'
export const metadata: Metadata = {
title: 'How to Implement GEO in Next.js | My Site',
description: 'GEO in Next.js requires JSON-LD, Metadata API, and SSR. Complete guide with examples.',
authors: [{ name: 'My Company', url: 'https://yoursite.com/about' }],
alternates: {
canonical: 'https://yoursite.com/geo-guide',
},
openGraph: {
type: 'article',
title: 'How to Implement GEO in Next.js',
description: 'Complete technical GEO guide for Next.js',
url: 'https://yoursite.com/geo-guide',
siteName: 'My Site',
images: [
{
url: 'https://yoursite.com/og/geo-guide.jpg',
width: 1200,
height: 630,
},
],
locale: 'en_US',
publishedTime: '2026-04-18T00:00:00Z',
modifiedTime: '2026-04-18T00:00:00Z',
authors: ['https://yoursite.com/author/my-company'],
section: 'Technical Guides',
tags: ['GEO', 'Next.js', 'AI Optimization'],
},
robots: { index: true, follow: true },
}
export default function Page() {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: 'How to Implement GEO in Next.js',
description: 'Complete technical GEO guide for Next.js',
author: {
'@type': 'Organization',
name: 'My Company',
},
publisher: {
'@type': 'Organization',
name: 'My Site',
logo: {
'@type': 'ImageObject',
url: 'https://yoursite.com/logo.png',
},
},
datePublished: '2026-04-18T00:00:00Z',
dateModified: '2026-04-18T00:00:00Z',
mainEntityOfPage: {
'@type': 'WebPage',
'@id': 'https://yoursite.com/geo-guide',
},
}
return (
<>
<Script
id="article-schema"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<main>
<article>
<h1>How to Implement GEO in Next.js</h1>
{/* Inverted pyramid: direct answer first */}
<p>
GEO in Next.js is implemented via the Metadata API for meta tags and
Script components for JSON-LD. App Router provides SSR by default,
making pages immediately accessible to AI crawlers.
</p>
</article>
</main>
</>
)
}
Dynamic Metadata with generateMetadata
For content-driven pages (blog posts, documentation), use generateMetadata() to generate metadata dynamically from your data source:
// 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} | My Site`,
description: post.excerpt,
alternates: { canonical: `https://yoursite.com/blog/${params.slug}` },
openGraph: {
type: 'article',
title: post.title,
description: post.excerpt,
url: `https://yoursite.com/blog/${params.slug}`,
publishedTime: post.publishedAt,
modifiedTime: post.updatedAt,
authors: [post.authorUrl],
section: post.category,
tags: post.tags,
},
}
}
export default async function BlogPost({ params }: Props) {
const post = await getPost(params.slug)
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
description: post.excerpt,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
author: {
'@type': 'Person',
name: post.author.name,
url: post.author.url,
},
publisher: {
'@type': 'Organization',
name: 'My Site',
logo: { '@type': 'ImageObject', url: 'https://yoursite.com/logo.png' },
},
}
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<article dangerouslySetInnerHTML={{ __html: post.content }} />
</>
)
}
Root Layout Metadata
Set site-wide defaults in the root layout:
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
metadataBase: new URL('https://yoursite.com'),
title: {
default: 'My Site',
template: '%s | My Site',
},
description: 'Default site description for pages without specific descriptions.',
openGraph: {
siteName: 'My Site',
locale: 'en_US',
},
robots: {
index: true,
follow: true,
},
}
robots.txt (App Router)
Create /app/robots.ts for programmatic robots.txt generation:
// 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://yoursite.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://yoursite.com',
lastModified: new Date(),
priority: 1,
},
...posts.map(post => ({
url: `https://yoursite.com/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
priority: 0.8,
})),
]
}
Pages Router (Legacy)
If using Pages Router, use getStaticProps or getServerSideProps (never CSR only) and inject metadata via next/head:
// pages/geo-guide.tsx
import Head from 'next/head'
export default function GeoGuide({ post }) {
return (
<>
<Head>
<title>{post.title} | My Site</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://yoursite.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>{/* content */}</article>
</>
)
}
GEO Checklist for Next.js
- App Router with SSR (default) — never use
'use client'on SEO pages - Metadata API: title template, description, alternates.canonical
- openGraph with publishedTime and modifiedTime
- JSON-LD via Script component or dangerouslySetInnerHTML
- robots.ts with all 8 AI crawlers explicitly allowed
- sitemap.ts with lastModified dates from content source
- metadataBase set in root layout
- Inverted pyramid content structure in page components
- Core Web Vitals: LCP < 2.5s, INP < 200ms, CLS < 0.1
Next.js 15: Native robots.ts and sitemap.ts
Next.js 15 supports robots.ts and sitemap.ts as native file conventions in the App Router, replacing manual static file management.
robots.ts — AI Crawler Configuration
// 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 — separate from GPTBot
{ userAgent: 'PerplexityBot', allow: '/' },
{ userAgent: 'ClaudeBot', allow: '/' },
{ userAgent: 'Google-Extended', allow: '/' },
],
sitemap: 'https://your-domain.com/sitemap.xml',
}
}
Critical: OAI-SearchBot (ChatGPT Search) is separate from GPTBot (training). Blocking GPTBot does not automatically block or allow OAI-SearchBot. Allow both for full ChatGPT visibility.
sitemap.ts — Automatic Sitemap
// app/sitemap.ts
import type { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://your-domain.com',
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 1,
},
]
}
Streaming Metadata (Next.js 15)
generateMetadata() now supports streaming — metadata resolves without blocking the UI render:
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug) // no longer blocks initial render
return {
title: post.title,
description: post.description,
openGraph: {
title: post.title,
publishedTime: post.datePublished,
modifiedTime: post.dateModified,
},
}
}
Partial Prerendering and GEO
Next.js 15 introduces experimental Partial Prerendering (PPR). For GEO, the static shell is what AI crawlers see. Ensure your primary content is in the static portion, not inside <Suspense> boundaries:
// next.config.ts
const nextConfig = {
experimental: { ppr: true },
}
Content inside <Suspense> boundaries may not be indexed by GPTBot, PerplexityBot, or ClaudeBot.