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
-
Google Search Console
- Submit sitemap
- Check indexing status
- Monitor search performance
-
PageSpeed Insights
- Test Core Web Vitals
- Get optimization recommendations
- https://pagespeed.web.dev/
-
Rich Results Test
- Validate structured data
- Preview how Google displays your page
- https://search.google.com/test/rich-results
-
Twitter Card Validator
- Test Twitter cards
- https://cards-dev.twitter.com/validator
-
OpenGraph.xyz
- Preview Open Graph images
- https://www.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.