Why Next.js Is an SEO Powerhouse (When Used Correctly)
Next.js App Router gives you server-side rendering by default, which means Google can crawl your content without executing JavaScript. Combined with the Metadata API, you have a best-in-class SEO foundation.
But most Next.js apps I audit are leaving significant SEO gains on the table.
1. The Metadata API — Do It Right
// app/layout.tsx — site-wide defaults
export const metadata: Metadata = {
metadataBase: new URL('https://yoursite.com'),
title: {
template: '%s | Your Brand',
default: 'Your Brand — Tagline',
},
description: 'Your site description (150-160 chars)',
openGraph: {
type: 'website',
locale: 'en_US',
url: 'https://yoursite.com',
siteName: 'Your Brand',
},
twitter: {
card: 'summary_large_image',
creator: '@yourhandle',
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
}
The metadataBase is critical. Without it, relative URLs in OG images won't resolve correctly.
2. Dynamic Metadata for Blog Posts
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug)
return {
title: post.seo_title || post.title,
description: post.seo_description || post.excerpt,
alternates: {
canonical: `/blog/${post.slug}`, // Prevents duplicate content
},
openGraph: {
title: post.title,
description: post.excerpt,
type: 'article',
publishedTime: post.published_at,
authors: [post.author.name],
images: [{
url: post.cover_image,
width: 1200,
height: 630,
alt: post.title,
}],
},
}
}
3. JSON-LD Schema Markup
Schema markup helps Google understand your content and can trigger rich results (star ratings, breadcrumbs, FAQs).
Organization Schema (homepage):
const schema = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'NineLab',
url: 'https://ninelab.ir',
logo: 'https://ninelab.ir/assets/logo/icon.png',
contactPoint: {
'@type': 'ContactPoint',
email: 'info@ninelab.ir',
contactType: 'customer service',
},
sameAs: [
'https://instagram.com/ninelab.ir',
'https://linkedin.com/in/ninelab.ir',
],
}
// In your page component:
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
Article Schema (blog posts):
const articleSchema = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
description: post.excerpt,
image: post.cover_image,
datePublished: post.published_at,
dateModified: post.updated_at,
author: {
'@type': 'Person',
name: post.author.name,
url: `https://ninelab.ir/team/${post.author.slug}`,
},
publisher: {
'@type': 'Organization',
name: 'NineLab',
logo: 'https://ninelab.ir/assets/logo/icon.png',
},
}
4. Dynamic Sitemap
// app/sitemap.ts
import { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts()
const blogUrls = posts.map(post => ({
url: `https://ninelab.ir/blog/${post.slug}`,
lastModified: new Date(post.updated_at),
changeFrequency: 'weekly' as const,
priority: 0.8,
}))
return [
{ url: 'https://ninelab.ir', lastModified: new Date(), priority: 1.0 },
{ url: 'https://ninelab.ir/blog', lastModified: new Date(), priority: 0.9 },
...blogUrls,
]
}
5. The robots.txt
// app/robots.ts
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/dashboard/', '/api/', '/login'],
},
],
sitemap: 'https://ninelab.ir/sitemap.xml',
}
}
The SEO Audit Checklist
Run through this after every launch:
