Complete Next.js SEO Guide: Metadata API + Structured Data Best Practices

It’s 3 AM, and I’m staring at my Google Search Console dashboard. That big, glaring “0” is so painful it makes me want to smash my computer.
It’s been 23 days since the product launched. Two months of development, countless all-nighters, a meticulously crafted UI, smooth user experience—all rendered meaningless by this cold, hard zero. What’s even more frustrating? When I shared the link on Twitter, the preview area was completely blank. Not even a decent cover image.
“Doesn’t Next.js have built-in SSR? Why is the SEO still terrible?” After digging through the official docs, I realized a harsh truth: SSR does not equal SEO-friendly. If your meta tags are misconfigured, structured data isn’t set up, and you don’t understand the Open Graph protocol—search engines will treat you like thin air.
I bet you’ve felt this pain too: pouring your heart into a product only to have it be unsearchable and look unprofessional when shared. The only option seems to be burning money on paid ads. But here’s the thing—if you master Next.js 15’s Metadata API and a few key configurations, all these problems can be solved.
This article will walk you through: how to use the Metadata API to give each page unique meta tags, how to configure structured data for better search results, and how to achieve perfect social media preview cards. Most importantly, I’ll show you the 5 most common SEO pitfalls, helping you avoid the mistakes I made.
Why Is Your Next.js Website’s SEO So Bad?
SSR ≠ SEO-Friendly
To be honest, I used to think this way too. Use Next.js, get server-side rendering, HTML goes straight to crawlers—perfect SEO, right?
Naive.
Later, I looked at a friend’s project source code. Opening the browser DevTools and checking the HTML, every single page had <title> set to “My App”, and <meta name="description"> was either missing or identical across all pages. It’s like opening a boutique but having a sign that just says “Store”—customers have no idea what you’re selling.
Next.js provides SSR capability, but configuring meta tags is entirely your responsibility. Without proper configuration, you’re just serving empty HTML shells, and search engines have no clue what your pages are about.
5 Fatal SEO Mistakes
I’ve seen too many developers make these mistakes, myself included:
1. All pages share the same title and description
The most common error. Either hardcoding a single title in _document.tsx or not writing one at all. The result? Google finds your homepage, about page, and product page, and they all display identical titles and descriptions.
Imagine walking into a bookstore where every book has the same title on its cover. Would you buy one?
2. Forgetting to configure canonical URLs, causing duplicate content
This pitfall is particularly insidious. Your site might have pagination (?page=2), filters (?category=tech), sorting (?sort=date), and other URL parameters. These are different views of the same page, but search engines think they’re separate pages and penalize you for “duplicate content.”
I had a client’s blog lose 40% of its traffic because of this. After adding canonical URLs, traffic recovered within two weeks.
3. No structured data, missing out on rich snippets
Ever noticed when searching for “apple pie recipe,” some results directly show ratings (4.8 stars), cooking time (45 minutes), and calories (320kcal)? Google didn’t guess those—websites actively tell Google through structured data (JSON-LD).
According to research, websites that implement structured data see an average 20-30% increase in click-through rates. What does that mean? Your traffic increases by a third, and you only need to add a few lines of code.
4. Images without alt attributes, wasting image search traffic
Many developers think alt attributes are just for blind people and have nothing to do with SEO. Wrong.
Google Image Search is a massive traffic source. If your images have clear alt descriptions, they can rank in image search. I’ve seen a design asset website where 30% of traffic comes from Google Image Search, simply because they wrote proper alt text for every image.
5. No sitemap.xml and robots.txt
These files tell search engines “what pages can be crawled” and “which cannot.” Without a sitemap, Google might take months to discover your new articles. With a sitemap submitted to Search Console, new pages can be indexed within days.
Plus, Next.js App Router now supports auto-generating sitemap.ts and robots.ts—you don’t even have to write them manually. Why not configure them?
Mastering the Metadata API (Next.js 15)
Next.js 15’s Metadata API is this framework’s greatest gift to SEO. Before, you had to manually write <Head> on every page. Now you just export an object or function, and Next.js handles it automatically.
Static metadata: For fixed content pages
The simplest scenario: your “About Us” page, privacy policy page—content that barely changes. Just export a metadata object in page.tsx:
// app/about/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'About Us - TechBlog',
description: 'We are a group of tech enthusiasts sharing frontend, backend, and DevOps practical experience.',
keywords: ['Tech Blog', 'Frontend Development', 'Next.js', 'React'],
authors: [{ name: 'John Doe' }],
openGraph: {
title: 'About Us - TechBlog',
description: 'Tech blog sharing development practical experience',
url: 'https://yourdomain.com/about',
siteName: 'TechBlog',
images: [
{
url: 'https://yourdomain.com/og-about.jpg',
width: 1200,
height: 630,
}
],
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'About Us - TechBlog',
description: 'Tech blog sharing development experience',
images: ['https://yourdomain.com/og-about.jpg'],
},
}
export default function AboutPage() {
return <div>About us content...</div>
}See? Type-safe, IDE auto-completion, no worrying about typos. Plus, Next.js automatically deduplicates—if multiple places define the same meta tags, it intelligently merges them.
Best Practices:
- Keep
titleunder 60 characters, anything beyond gets truncated in search results - Keep
descriptionbetween 150-160 characters, the ideal length for Google search result snippets openGraph.imagesshould be 1200x630 pixels, this size works universally on Twitter, Facebook, and LinkedIn
Dynamic metadata: The savior for blogs and product pages
The real power is the generateMetadata function. For instance, your blog post pages—each article has different titles and descriptions. You can’t hardcode them, right?
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { getPostBySlug } from '@/lib/posts'
export async function generateMetadata(
{ params }: { params: { slug: string } }
): Promise<Metadata> {
// Fetch article data from database or CMS
const post = await getPostBySlug(params.slug)
return {
title: `${post.title} - TechBlog`,
description: post.excerpt,
authors: [{ name: post.author }],
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
}
}
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug)
return <article>{post.content}</article>
}Now every article has unique SEO information. Google crawls complete HTML without needing client-side JavaScript.
Key Technique: metadataBase
Notice how image URLs in the code above are absolute paths? If your image paths are relative (like /images/cover.jpg), you need to configure metadataBase in the root layout:
// app/layout.tsx
export const metadata: Metadata = {
metadataBase: new URL('https://yourdomain.com'),
}With this configured, all relative paths automatically become complete URLs. Otherwise, Open Graph fetching fails and social shares have no images.
Template system: Unify title format across all pages
Ever noticed many websites have page titles in the format “Page Name | Site Name”? Like “Home | TechBlog”, “About Us | TechBlog”.
Manually writing the suffix for every page is tedious. Use title.template to handle it:
// app/layout.tsx (root layout)
export const metadata: Metadata = {
title: {
template: '%s | TechBlog',
default: 'TechBlog - Tech Blog',
},
description: 'Tech blog sharing frontend, backend, DevOps practical experience',
metadataBase: new URL('https://yourdomain.com'),
}Then child pages only need the page name:
// app/about/page.tsx
export const metadata: Metadata = {
title: 'About Us', // Final render: "About Us | TechBlog"
}Don’t want the suffix on the homepage? Use title.absolute:
// app/page.tsx
export const metadata: Metadata = {
title: {
absolute: 'TechBlog - Tech Blog Homepage', // Won't apply template
},
}This system helps you maintain title consistency, and it’s incredibly easy to change—just modify the template once in the root layout, and all pages update automatically.
Structured Data (Schema.org) Makes You Stand Out
What Is Structured Data? Why Is It Important?
Ever searched for “how to make apple pie”?
Look closely at the search results—some recipes directly display ratings (4.8 stars), cooking time (45 minutes), calories (320kcal), and even step previews. Others just have titles and descriptions.
The difference? Structured data.
Structured data is a standard format (JSON-LD) used to tell search engines: “This is a blog post by XXX, published on XXX” or “This is a product, priced at XXX, rated XXX.” When search engines get this information, they can display rich snippets in search results—those cards with ratings, prices, and author info.
The data doesn’t lie: websites that implement structured data see an average 20-30% increase in click-through rates. Your traffic goes up by a third, and the investment cost is practically zero.
Common Schema Types
Schema.org defines hundreds of types, but for most websites, these are the commonly used ones:
- Organization - Company/organization information (place on homepage)
- BlogPosting - Blog posts (add to every article)
- Product - Product information (essential for e-commerce, includes price, rating, stock)
- FAQPage - FAQ pages (can expand directly in search results)
- LocalBusiness - Local businesses (restaurants, salons, etc., displays address, hours, phone)
We’ll focus on the first two since they’re most needed for blogs and business websites.
Implementing JSON-LD in Next.js
Next.js 15’s <Script> component makes implementing structured data incredibly simple. I typically create a generic component:
// components/StructuredData.tsx
import Script from 'next/script'
type StructuredDataProps = {
data: object
}
export default function StructuredData({ data }: StructuredDataProps) {
return (
<Script
id="structured-data"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
)
}Then use it in pages:
Example 1: Organization (Company Information)
// app/layout.tsx (root layout)
import StructuredData from '@/components/StructuredData'
const organizationData = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'TechBlog',
url: 'https://yourdomain.com',
logo: 'https://yourdomain.com/logo.png',
sameAs: [
'https://twitter.com/yourusername',
'https://github.com/yourcompany',
'https://linkedin.com/company/yourcompany',
],
contactPoint: {
'@type': 'ContactPoint',
email: 'hello@yourdomain.com',
contactType: 'Customer Service',
},
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<StructuredData data={organizationData} />
</body>
</html>
)
}Example 2: BlogPosting (Blog Article)
// app/blog/[slug]/page.tsx
import StructuredData from '@/components/StructuredData'
import { getPostBySlug } from '@/lib/posts'
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug)
const articleData = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
image: post.coverImage,
author: {
'@type': 'Person',
name: post.author,
url: `https://yourdomain.com/author/${post.authorSlug}`,
},
publisher: {
'@type': 'Organization',
name: 'TechBlog',
logo: {
'@type': 'ImageObject',
url: 'https://yourdomain.com/logo.png',
},
},
datePublished: post.publishedAt,
dateModified: post.updatedAt,
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `https://yourdomain.com/blog/${post.slug}`,
},
}
return (
<>
<article>{post.content}</article>
<StructuredData data={articleData} />
</>
)
}The code looks lengthy, but it’s really just telling Google your article’s metadata in a standard format. Configure once, then copy-paste for everything else.
Validating Your Structured Data
How do you know if it’s configured correctly? Use these two tools:
Google Rich Results Test (https://search.google.com/test/rich-results)
- Enter your page URL, Google tells you what rich media can be displayed
- If there are errors, it clearly indicates what’s wrong
Schema Markup Validator (https://validator.schema.org/)
- Checks if JSON-LD format meets Schema.org standards
- Stricter than Google’s tool, recommend testing with both
The most common error I’ve seen is forgetting to configure publisher (required for BlogPosting) or image URLs not being absolute paths. Validation tools directly tell you what’s missing—fix it and test again.
Open Graph and Twitter Cards in Practice
Why Is Social Sharing Important?
Ever had this experience: sharing an article on Twitter or Facebook, and the preview is either completely blank or shows the wrong image (like a website logo or some random decorative graphic)?
It looks unprofessional. Meanwhile, properly configured websites automatically display beautiful cover images, titles, and descriptions when shared—click-through rates can be 2-3 times higher.
Configuring Open Graph and Twitter Cards lets you control how your links look on social media.
Open Graph Protocol Explained
Open Graph was originally a standard created by Facebook, now supported by all major platforms including Twitter, LinkedIn, Slack, and Discord.
When we discussed the Metadata API earlier, we actually already configured the openGraph object. But let’s dive deeper into the core fields:
export const metadata: Metadata = {
openGraph: {
// Required fields
title: 'Article Title', // Title displayed on social media
description: 'Article summary', // Summary, around 150 characters
url: 'https://yourdomain.com/article', // URL of this content
siteName: 'TechBlog', // Site name
// Images (most important!)
images: [
{
url: 'https://yourdomain.com/og-image.jpg',
width: 1200,
height: 630, // Recommended size 1200x630
alt: 'Image description (accessibility and SEO)',
},
],
// Content type
type: 'article', // Use 'article' for articles, 'website' for other pages
// Article-specific fields (if type is article)
publishedTime: '2025-01-15T08:00:00.000Z',
modifiedTime: '2025-01-16T10:30:00.000Z',
authors: ['John Doe', 'Jane Smith'],
tags: ['Next.js', 'SEO', 'Frontend Development'],
// Localization (if multi-language versions exist)
locale: 'en_US',
alternateLocale: ['zh_CN', 'ja_JP'],
},
}Key Point: Image Size
1200x630 pixels is the golden ratio (1.91:1), displays perfectly across all platforms:
- Facebook, LinkedIn: Full display
- Twitter: Crops to 2:1 but still looks good
- Slack, Discord: Also compatible
Image size cannot exceed 8MB, or the build will fail.
Twitter Cards Configuration
Twitter has its own meta tag system. While it falls back to Open Graph, it’s best to configure separately:
export const metadata: Metadata = {
twitter: {
card: 'summary_large_image', // Large image mode (recommended)
site: '@yourusername', // Site's Twitter account
creator: '@authorusername', // Author's Twitter account
title: 'Article Title',
description: 'Article summary',
images: ['https://yourdomain.com/twitter-image.jpg'],
},
}The card field has two values:
summary: Small image mode (image on left, text on right)summary_large_image: Large image mode (image fills upper half of card, recommended)
Twitter image limit: Cannot exceed 5MB (stricter than OG).
Dynamically Generating Social Share Images (Advanced)
Manually creating a 1200x630 cover image for every article? Exhausting.
Next.js 13.3+ supports dynamically generating OG images with code, perfect for blogs:
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
import { getPostBySlug } from '@/lib/posts'
export const runtime = 'edge'
export const alt = 'Blog post cover'
export const size = {
width: 1200,
height: 630,
}
export const contentType = 'image/png'
export default async function Image({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug)
return new ImageResponse(
(
<div
style={{
fontSize: 60,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
padding: '80px',
}}
>
<h1 style={{ fontSize: 72, fontWeight: 'bold', textAlign: 'center' }}>
{post.title}
</h1>
<p style={{ fontSize: 36, marginTop: 20, opacity: 0.9 }}>
by {post.author}
</p>
</div>
),
{
...size,
}
)
}Now every article’s share image is dynamically generated based on the title! No manual design needed.
If you want custom fonts or background images, you can further customize. Check out Next.js official docs on next/og for details.
Testing and Validating Social Sharing
After configuration, don’t rush to post. Test with these tools first:
Facebook Sharing Debugger (https://developers.facebook.com/tools/debug/)
- Enter URL to see how Facebook fetches and displays it
- Note: Facebook caches OG info. If you changed the code, click “Scrape Again” to refresh cache
Twitter Card Validator (https://cards-dev.twitter.com/validator)
- Enter URL to preview Twitter card effect
- Note: Post-2023, this tool requires Twitter developer account, but you can test by posting directly on Twitter
LinkedIn Post Inspector (https://www.linkedin.com/post-inspector/)
- LinkedIn’s preview tool, can also refresh cache
A pit I’ve fallen into: After changing OG images, Facebook still shows the old one. You must go to Sharing Debugger to refresh cache, or you’ll question your sanity.
Other Essential SEO Configurations
The Metadata API, structured data, and Open Graph we covered are the core of SEO, but there are a few more configurations equally important—don’t skip them.
sitemap.xml - Tell Search Engines What Pages You Have
A sitemap is an XML file listing all your website’s page URLs. Google and Bing crawlers read this file to understand your site structure and speed up indexing.
Next.js App Router makes this super simple—just create an app/sitemap.ts:
// app/sitemap.ts
import { MetadataRoute } from 'next'
import { getAllPosts } from '@/lib/posts'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts()
const baseUrl = 'https://yourdomain.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,
},
]
// Dynamically generate blog post pages
const blogPages: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly' as const,
priority: 0.7,
}))
return [...staticPages, ...blogPages]
}Next.js automatically generates this file at https://yourdomain.com/sitemap.xml.
After configuration, remember to do these two things:
- Submit to Google Search Console (https://search.google.com/search-console)
- Submit to Bing Webmaster Tools (https://www.bing.com/webmasters)
After submission, new page indexing speed can improve by over 50%. I used to blog without submitting a sitemap—new articles took two weeks to be indexed. After submitting, they appeared on Google in three days.
robots.txt - Control Crawler Access
robots.txt tells search engine crawlers “which pages can be crawled and which cannot.”
Similarly, Next.js App Router supports code-based generation:
// app/robots.ts
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*', // Applies to all crawlers
allow: '/', // Allow crawling all pages
disallow: ['/admin', '/api', '/private'], // Disallow these paths
},
],
sitemap: 'https://yourdomain.com/sitemap.xml', // Point to sitemap
}
}This generates https://yourdomain.com/robots.txt with content roughly like:
User-agent: *
Allow: /
Disallow: /admin
Disallow: /api
Disallow: /private
Sitemap: https://yourdomain.com/sitemap.xmlCommon Scenarios:
- Admin pages (
/admin) definitely shouldn’t be crawled by search engines - API routes (
/api) also don’t need crawling - Draft, preview pages can use
noindexmeta tags orDisallowrules
canonical URL - Avoid Duplicate Content Penalties
Canonical URLs tell search engines: “This page has multiple URLs, but this is the authentic one.”
Typical scenarios:
- Pagination:
/blog?page=1,/blog?page=2 - Filters:
/products?category=tech - Sorting:
/products?sort=price
These are different views of the same page. Without declaring canonical, search engines think they’re separate pages and dilute your authority.
Configuring canonical in Next.js:
// app/blog/page.tsx
export const metadata: Metadata = {
alternates: {
canonical: 'https://yourdomain.com/blog',
},
}Or generate dynamically:
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
return {
alternates: {
canonical: `https://yourdomain.com/blog/${params.slug}`,
},
}
}If your page has multi-language versions, you can also use alternates.languages to tell Google:
export const metadata: Metadata = {
alternates: {
canonical: 'https://yourdomain.com/blog/nextjs-seo',
languages: {
'zh-CN': 'https://yourdomain.com/zh/blog/nextjs-seo',
'ja-JP': 'https://yourdomain.com/ja/blog/nextjs-seo',
},
},
}Image Optimization - next/image + alt Attribute
Many developers ignore image SEO, but Google Image Search is actually a huge traffic source.
Two Key Points:
- Use
next/imageinstead of<img>
import Image from 'next/image'
<Image
src="/cover.jpg"
alt="Complete Next.js SEO Guide Cover"
width={1200}
height={630}
priority // Add this for above-the-fold images for priority loading
/>next/image automatically handles these optimizations:
- Lazy loading (below-the-fold images load later)
- WebP format conversion (smaller file size)
- Responsive sizing (loads appropriate size based on device)
- Prevents CLS (Cumulative Layout Shift, one of Core Web Vitals metrics)
- Must Write
altAttribute
alt isn’t optional—it’s both an SEO requirement and an accessibility requirement (screen readers read alt).
Good alt Descriptions:
- ✅ “Next.js Metadata API configuration code example”
- ✅ “Blog post rich snippet display in Google search results”
Poor alt Descriptions:
- ❌ “Image”
- ❌ “screenshot.png”
- ❌ No alt
Google Image Search ranks images based on alt content. I’ve seen a design asset website where 30% of traffic comes from image search, simply because they wrote proper alt text for every image.
Real-World Example - Complete Blog SEO Configuration
We’ve covered so much theory and code snippets. Now let’s integrate them and see how a complete blog project should configure SEO.
Project Structure
Suppose we’re building a tech blog with Next.js 15 App Router. The directory structure looks roughly like this:
app/
├── layout.tsx # Root layout - global configuration
├── page.tsx # Homepage
├── about/page.tsx # About page
├── blog/
│ ├── page.tsx # Blog list page
│ └── [slug]/
│ ├── page.tsx # Blog detail page
│ └── opengraph-image.tsx # Dynamic OG image generation (optional)
├── sitemap.ts # Sitemap generation
└── robots.ts # Robots.txt generationComplete Code Examples
1. Root Layout - Global SEO Configuration
// app/layout.tsx
import { Metadata } from 'next'
import StructuredData from '@/components/StructuredData'
export const metadata: Metadata = {
metadataBase: new URL('https://yourdomain.com'), // Must configure for relative path concatenation
title: {
template: '%s | TechBlog', // Child page title template
default: 'TechBlog - Frontend Development Tech Blog',
},
description: 'Tech blog sharing Next.js, React, TypeScript practical experience',
keywords: ['Next.js', 'React', 'TypeScript', 'Frontend Development', 'Tech Blog'],
authors: [{ name: 'John Doe', url: 'https://yourdomain.com/about' }],
openGraph: {
type: 'website',
siteName: 'TechBlog',
locale: 'en_US',
},
twitter: {
card: 'summary_large_image',
site: '@yourusername',
},
}
// Organization structured data (global, place in root layout)
const organizationData = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'TechBlog',
url: 'https://yourdomain.com',
logo: 'https://yourdomain.com/logo.png',
sameAs: [
'https://twitter.com/yourusername',
'https://github.com/yourcompany',
],
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<StructuredData data={organizationData} />
</body>
</html>
)
}2. Homepage - Static metadata
// app/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: {
absolute: 'TechBlog - Frontend Development Tech Blog', // Doesn't apply template
},
description: 'Sharing Next.js, React, TypeScript and other frontend tech practical experience to help developers improve skills',
openGraph: {
title: 'TechBlog - Frontend Development Tech Blog',
description: 'Sharing frontend tech practical experience',
url: 'https://yourdomain.com',
images: [
{
url: 'https://yourdomain.com/og-home.jpg',
width: 1200,
height: 630,
alt: 'TechBlog homepage cover',
},
],
},
}
export default function HomePage() {
return <div>Homepage content...</div>
}3. Blog Detail Page - Dynamic metadata + Structured Data
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getPostBySlug } from '@/lib/posts'
import StructuredData from '@/components/StructuredData'
// Dynamically generate metadata
export async function generateMetadata(
{ params }: { params: { slug: string } }
): Promise<Metadata> {
const post = await getPostBySlug(params.slug)
if (!post) return {}
return {
title: post.title, // Applies template, becomes "Article Title | TechBlog"
description: post.excerpt,
keywords: post.tags,
authors: [{ name: post.author }],
openGraph: {
title: post.title,
description: post.excerpt,
url: `https://yourdomain.com/blog/${post.slug}`,
images: [post.coverImage],
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
alternates: {
canonical: `https://yourdomain.com/blog/${post.slug}`,
},
}
}
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug)
if (!post) notFound()
// BlogPosting structured data
const articleData = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
image: post.coverImage,
datePublished: post.publishedAt,
dateModified: post.updatedAt || post.publishedAt,
author: {
'@type': 'Person',
name: post.author,
},
publisher: {
'@type': 'Organization',
name: 'TechBlog',
logo: {
'@type': 'ImageObject',
url: 'https://yourdomain.com/logo.png',
},
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `https://yourdomain.com/blog/${post.slug}`,
},
}
return (
<>
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
<StructuredData data={articleData} />
</>
)
}4. Sitemap and Robots
// app/sitemap.ts
import { getAllPosts } from '@/lib/posts'
export default async function sitemap() {
const posts = await getAllPosts()
const baseUrl = 'https://yourdomain.com'
const blogUrls = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly' as const,
priority: 0.7,
}))
return [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'daily' as const,
priority: 1,
},
{
url: `${baseUrl}/about`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
...blogUrls,
]
}
// app/robots.ts
export default function robots() {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/api', '/admin'],
},
sitemap: 'https://yourdomain.com/sitemap.xml',
}
}Post-Deployment Verification Checklist
After configuration, you must check before going live:
View HTML Source
- Press F12 in browser → Elements → Check
<head>tag - Confirm
<title>,<meta name="description">, OG tags are correctly rendered
- Press F12 in browser → Elements → Check
Test sitemap and robots
- Visit
https://yourdomain.com/sitemap.xmlto see if it generates properly - Visit
https://yourdomain.com/robots.txtto check content
- Visit
Validate Structured Data
- Use Google Rich Results Test to test a few pages
- Confirm no errors or warnings
Test Social Sharing
- Use Facebook Sharing Debugger and Twitter Card Validator to test
- Confirm images, titles, descriptions display correctly
Submit to Search Engines
- Submit sitemap to Google Search Console
- Submit sitemap to Bing Webmaster Tools
After completing these steps, your Next.js website SEO is fully configured.
Conclusion
If you’ve read this far, you should understand: Next.js’s SSR doesn’t equal SEO-friendly, but the tools Next.js 15 provides do make SEO configuration much simpler.
Let’s recap the core points:
- Metadata API lets you configure meta tags in a type-safe way—use metadata object for static pages, generateMetadata function for dynamic pages
- Structured data (JSON-LD) is the secret weapon for 20-30% CTR increases, easily implemented with the
<Script>component - Open Graph and Twitter Cards determine how your links display on social media—1200x630 is the universal image size
- sitemap.xml and robots.txt can be auto-generated with
.tsfiles—don’t forget to submit to Search Console - Image optimization with
next/image+ proper alt text—image search can also bring significant traffic
To be honest, these configurations seem tedious, but the ROI is extremely high. I’ve seen too many products with strong technology that struggle with traffic because SEO wasn’t done right—forced to burn money on ads. Meanwhile, properly configured websites enjoy consistent organic traffic at near-zero cost.
SEO isn’t mysticism—it’s science. Follow this article’s checklist step by step, validate with tools, and results will show—maybe not overnight, but in three months you’ll see clear traffic growth.
Don’t wait until traffic completely dries up to think about SEO. Open your project now and spend half a day configuring it. If you encounter issues, bookmark this article and refer back anytime.
If this article helped you, share it with other developer friends—help them avoid a few pitfalls. Consider it good karma.
FAQ
What's the relationship between SSR and SEO?
SSR doesn't equal SEO-friendly—you must actively configure Metadata API for search engines to correctly understand and index content.
What's the difference between Metadata API and Head component?
• Type-safe
• Supports static and dynamic metadata
• Automatically handles duplicate tags
Head component is React's approach:
• Requires manual management
• Error-prone
New projects should use Metadata API.
How do I configure metadata for dynamic routes?
Example:
export async function generateMetadata({ params }) {
const data = await getData(params.id)
return { title: data.title }
}
What size should Open Graph images be?
Image file size should be kept under 1MB, format JPEG or PNG.
Is structured data required?
Google, Bing, and other search engines support JSON-LD format structured data.
Do I need to manually create sitemap and robots.txt?
sitemap.ts can dynamically generate all page URLs, robots.ts can configure crawling rules.
How long until SEO optimization shows results?
Recommendations:
1) Submit sitemap to Google Search Console
2) Use Google Rich Results Test for validation
3) Continuously monitor Search Console data
4) Keep content updated
14 min read · Published on: Dec 19, 2025 · Modified on: Jan 22, 2026
Related Posts
Next.js E-commerce in Practice: Complete Guide to Shopping Cart and Stripe Payment Implementation

Next.js E-commerce in Practice: Complete Guide to Shopping Cart and Stripe Payment Implementation
Complete Guide to Next.js File Upload: S3/Qiniu Cloud Presigned URL Direct Upload

Complete Guide to Next.js File Upload: S3/Qiniu Cloud Presigned URL Direct Upload
Next.js Unit Testing Guide: Complete Jest + React Testing Library Setup


Comments
Sign in with GitHub to leave a comment