GEO for Next.js: Complete Implementation Guide

intermediate

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.

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