GameCraftGameCraft

SEO Optimization

Complete guide to SEO best practices in Next.js 15+ App Router with metadata API, sitemap, and robots.txt

SEO Optimization

Complete guide for SEO best practices in Next.js 15+ App Router. Learn how to optimize your ProductReady app for search engines using modern Next.js features.

Next.js 15+ SEO: The App Router provides built-in SEO features through the Metadata API, making SEO configuration type-safe and straightforward.


Quick Overview

What you'll learn:

  • ✅ Metadata API for page titles and descriptions
  • ✅ Open Graph and Twitter Card optimization
  • ✅ Dynamic metadata for database-driven content
  • ✅ Sitemap generation
  • ✅ Robots.txt configuration
  • ✅ Structured data (JSON-LD)
  • ✅ Performance optimization for SEO

Why SEO matters:

  • 🎯 Organic traffic - Get discovered without paid ads
  • 📈 Better rankings - Appear higher in search results
  • 🔗 Social sharing - Rich previews on Twitter, LinkedIn, etc.
  • 💰 Lower acquisition costs - Free marketing channel

Metadata API Basics

Static Metadata

The simplest way to add SEO metadata to any page:

// src/app/about/page.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'About Us - ProductReady',
  description: 'Learn about ProductReady - the fastest way to ship AI-powered SaaS applications.',
  keywords: ['AI', 'SaaS', 'Next.js', 'boilerplate'],
};

export default function AboutPage() {
  return <div>About content...</div>;
}

Layout Metadata (Shared)

Set default metadata in layout.tsx to apply across all pages:

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

export const metadata: Metadata = {
  title: {
    default: 'ProductReady - Ship AI Apps Fast',
    template: '%s | ProductReady', // Page title | ProductReady
  },
  description: 'Build production-grade, fully-documented, market-ready applications equipped with AI agents.',
  keywords: ['Next.js', 'AI', 'SaaS', 'TypeScript', 'tRPC', 'Drizzle'],
  authors: [{ name: 'Your Name' }],
  creator: 'Your Company',
  
  // Verification tokens
  verification: {
    google: 'your-google-verification-code',
    yandex: 'your-yandex-verification-code',
  },
  
  // Additional metadata
  category: 'technology',
};

Template usage:

  • Page with title: "Dashboard" → displays as "Dashboard | ProductReady"
  • Page with title: "Settings" → displays as "Settings | ProductReady"

Dynamic Metadata

For pages with database-driven content (blog posts, products, user profiles):

// src/app/posts/[slug]/page.tsx
import { Metadata } from 'next';
import { db } from '~/db';
import { posts } from '~/db/schema';
import { eq } from 'drizzle-orm';

type Props = {
  params: { slug: string };
};

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  // Fetch post from database
  const post = await db.query.posts.findFirst({
    where: eq(posts.slug, params.slug),
  });

  if (!post) {
    return {
      title: 'Post Not Found',
    };
  }

  return {
    title: post.title,
    description: post.excerpt || post.content.substring(0, 160),
    authors: [{ name: post.authorName }],
    
    // Open Graph
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      publishedTime: post.createdAt.toISOString(),
      authors: [post.authorName],
      images: [
        {
          url: post.coverImage || '/og-default.png',
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
    },
    
    // Twitter Card
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage || '/og-default.png'],
    },
  };
}

export default function PostPage({ params }: Props) {
  // Page component...
}

Open Graph & Social Sharing

Complete Open Graph Setup

// src/app/layout.tsx or specific page
export const metadata: Metadata = {
  // ... other metadata
  
  openGraph: {
    type: 'website',
    locale: 'en_US',
    url: 'https://productready.com',
    siteName: 'ProductReady',
    title: 'ProductReady - Ship AI Apps Fast',
    description: 'Build production-grade, fully-documented, market-ready applications equipped with AI agents.',
    images: [
      {
        url: 'https://productready.com/og-image.png',
        width: 1200,
        height: 630,
        alt: 'ProductReady - Ship AI Apps Fast',
      },
    ],
  },
  
  twitter: {
    card: 'summary_large_image',
    site: '@yourhandle',
    creator: '@yourhandle',
    title: 'ProductReady - Ship AI Apps Fast',
    description: 'Build production-grade, fully-documented, market-ready applications equipped with AI agents.',
    images: ['https://productready.com/og-image.png'],
  },
};

OG Image Guidelines

Dimensions:

  • Open Graph: 1200 × 630px (1.91:1 ratio)
  • Twitter Large Card: 1200 × 628px
  • Twitter Summary: 300 × 157px

Best practices:

  • Use high-quality images (PNG or JPG)
  • Keep text readable at small sizes
  • Include your brand logo
  • Test with OpenGraph.xyz

Dynamic OG Images with next/og

Generate OG images dynamically:

// src/app/api/og/route.tsx
import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';

export const runtime = 'edge';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const title = searchParams.get('title') || 'ProductReady';

  return new ImageResponse(
    (
      <div
        style={{
          height: '100%',
          width: '100%',
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          backgroundColor: '#0a0a0a',
          fontSize: 60,
          fontWeight: 700,
          color: 'white',
        }}
      >
        <h1>{title}</h1>
        <p style={{ fontSize: 30, color: '#888' }}>ProductReady</p>
      </div>
    ),
    {
      width: 1200,
      height: 630,
    }
  );
}

Use in metadata:

export const metadata: Metadata = {
  openGraph: {
    images: [`/api/og?title=${encodeURIComponent('My Page Title')}`],
  },
};

Sitemap Generation

Automatic Sitemap

Create sitemap.ts in your app directory:

// src/app/sitemap.ts
import { MetadataRoute } from 'next';
import { db } from '~/db';
import { posts } from '~/db/schema';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const baseUrl = 'https://productready.com';

  // Static pages
  const staticPages: MetadataRoute.Sitemap = [
    {
      url: baseUrl,
      lastModified: new Date(),
      changeFrequency: 'daily',
      priority: 1,
    },
    {
      url: `${baseUrl}/about`,
      lastModified: new Date(),
      changeFrequency: 'monthly',
      priority: 0.8,
    },
    {
      url: `${baseUrl}/pricing`,
      lastModified: new Date(),
      changeFrequency: 'weekly',
      priority: 0.9,
    },
  ];

  // Dynamic pages from database
  const allPosts = await db.select({
    slug: posts.slug,
    updatedAt: posts.updatedAt,
  }).from(posts);

  const postPages: MetadataRoute.Sitemap = allPosts.map((post) => ({
    url: `${baseUrl}/posts/${post.slug}`,
    lastModified: post.updatedAt,
    changeFrequency: 'weekly',
    priority: 0.7,
  }));

  return [...staticPages, ...postPages];
}

Generated sitemap will be available at /sitemap.xml

Multiple Sitemaps

For large sites, split into multiple sitemaps:

// src/app/sitemap.ts (index)
import { MetadataRoute } from 'next';

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: 'https://productready.com/posts-sitemap.xml',
      lastModified: new Date(),
    },
    {
      url: 'https://productready.com/pages-sitemap.xml',
      lastModified: new Date(),
    },
  ];
}

Robots.txt

Static Robots.txt

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

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      {
        userAgent: '*',
        allow: '/',
        disallow: ['/api/', '/admin/', '/dashboard/'],
      },
      {
        userAgent: 'Googlebot',
        allow: '/',
        disallow: ['/api/', '/admin/'],
      },
    ],
    sitemap: 'https://productready.com/sitemap.xml',
  };
}

Disallow patterns:

  • /api/* - API endpoints (no SEO value)
  • /admin/* - Admin pages (private)
  • /dashboard/* - User dashboards (private)
  • /*.pdf - PDFs (if you don't want them indexed)

Structured Data (JSON-LD)

Add rich snippets for better search results:

Organization Schema

// src/components/structured-data/organization.tsx
export function OrganizationSchema() {
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'Organization',
    name: 'ProductReady',
    url: 'https://productready.com',
    logo: 'https://productready.com/logo.png',
    description: 'Build production-grade, fully-documented, market-ready applications equipped with AI agents',
    sameAs: [
      'https://twitter.com/productready',
      'https://github.com/productready/productready',
    ],
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}

Article Schema (Blog Posts)

// src/app/posts/[slug]/page.tsx
function ArticleSchema({ post }: { post: Post }) {
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: post.title,
    description: post.excerpt,
    image: post.coverImage,
    datePublished: post.createdAt.toISOString(),
    dateModified: post.updatedAt.toISOString(),
    author: {
      '@type': 'Person',
      name: post.authorName,
    },
    publisher: {
      '@type': 'Organization',
      name: 'ProductReady',
      logo: {
        '@type': 'ImageObject',
        url: 'https://productready.com/logo.png',
      },
    },
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}

export default function PostPage({ post }: Props) {
  return (
    <>
      <ArticleSchema post={post} />
      {/* Page content */}
    </>
  );
}

Product Schema (SaaS/Pricing)

function ProductSchema() {
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'SoftwareApplication',
    name: 'ProductReady',
    applicationCategory: 'DeveloperApplication',
    operatingSystem: 'Web',
    offers: {
      '@type': 'Offer',
      price: '29.00',
      priceCurrency: 'USD',
      priceValidUntil: '2025-12-31',
    },
    aggregateRating: {
      '@type': 'AggregateRating',
      ratingValue: '4.8',
      ratingCount: '127',
    },
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}

Canonical URLs

Prevent duplicate content issues:

// src/app/posts/[slug]/page.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const canonicalUrl = `https://productready.com/posts/${params.slug}`;

  return {
    title: 'Post Title',
    alternates: {
      canonical: canonicalUrl,
    },
  };
}

Performance & Core Web Vitals

SEO ranking considers page speed and user experience:

Image Optimization

import Image from 'next/image';

// ✅ Good: Optimized with Next.js Image
<Image
  src="/hero.png"
  alt="ProductReady Dashboard"
  width={1200}
  height={630}
  priority // For above-the-fold images
  placeholder="blur"
  blurDataURL="data:image/png;base64,..."
/>

// ❌ Bad: Regular img tag
<img src="/hero.png" alt="Dashboard" />

Font Optimization

// src/app/layout.tsx
import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // Prevent invisible text flash
  variable: '--font-inter',
});

export default function RootLayout({ children }: Props) {
  return (
    <html lang="en" className={inter.variable}>
      <body>{children}</body>
    </html>
  );
}

Lazy Loading

import dynamic from 'next/dynamic';

// Load heavy components only when needed
const HeavyChart = dynamic(() => import('~/components/heavy-chart'), {
  loading: () => <p>Loading chart...</p>,
  ssr: false, // Skip server-side rendering if not needed
});

Internationalization (i18n) SEO

For multi-language sites:

// src/app/[lang]/layout.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  return {
    title: 'ProductReady - Ship AI Apps Fast',
    alternates: {
      canonical: `https://productready.com/${params.lang}`,
      languages: {
        'en-US': 'https://productready.com/en',
        'zh-CN': 'https://productready.com/zh',
        'ja-JP': 'https://productready.com/ja',
      },
    },
  };
}

SEO Checklist

Pre-Launch

  • Set up Google Search Console
  • Set up Google Analytics
  • Add verification meta tags
  • Create sitemap.xml
  • Create robots.txt
  • Add Open Graph images
  • Add structured data (JSON-LD)
  • Set canonical URLs
  • Optimize images (WebP, lazy loading)
  • Optimize fonts (next/font)

Per Page

  • Unique, descriptive title (50-60 chars)
  • Meta description (150-160 chars)
  • Open Graph metadata
  • Twitter Card metadata
  • Canonical URL
  • Structured data (if applicable)
  • Alt text for all images
  • Proper heading hierarchy (H1 → H2 → H3)

Performance

  • Core Web Vitals passing
  • Mobile-friendly (responsive)
  • HTTPS enabled
  • Fast page load (<3s)
  • No layout shift (CLS)
  • Fast interactivity (FID/INP)

Testing & Validation

Tools

  1. Google Search Console

    • Submit sitemap
    • Check indexing status
    • Monitor search performance
  2. PageSpeed Insights

  3. Rich Results Test

  4. Twitter Card Validator

  5. OpenGraph.xyz


Common Mistakes to Avoid

Missing or duplicate titles

// Bad: No title
export const metadata = {};

// Good: Unique title per page
export const metadata = {
  title: 'Unique Page Title',
};

Blocking search engines

// Bad: Blocks all crawlers
export const metadata = {
  robots: 'noindex, nofollow',
};

// Good: Allow indexing (or be selective)
export const metadata = {
  robots: 'index, follow',
};

Slow page loads

// Bad: Large unoptimized image
<img src="/huge-image.png" />

// Good: Optimized with Next.js
<Image src="/hero.png" width={800} height={600} alt="..." />

Next Steps

  • Monitor: Set up Google Search Console and Analytics
  • Optimize: Use PageSpeed Insights to improve Core Web Vitals
  • Content: Write high-quality, keyword-rich content
  • Backlinks: Get links from other reputable sites
  • Local SEO: If applicable, optimize for local search

SEO is a marathon, not a sprint. Focus on quality content, fast performance, and good user experience - the rankings will follow.

On this page