SEO & Metadata API
SEO is a free traffic engine for websites—correct metadata can make Google index your site three times faster.
1. What You'll Learn
generateMetadata()Dynamically generate page title, description, OpenGraph, and Twitter metadatametadataStatic Export and Static Routing Metadata- JSON-LD Structured Data (BreadcrumbList / Article / FAQPage)
- Sitemap and Dynamic Generation with
robots.txt - hreflang Multilingual SEO Configuration
@vercel/og+ Satori: Dynamically Generate OG Images
2. A True Story of a Content Operations Manager
(1) Pain Point: Three months after launch, Google has only indexed five pages
Bob is in charge of content operations for TaskFlow. Three months after the blog launched, he had written 50 articles:
"Google Search Console shows that only 5 pages have been indexed. Upon inspection, I found that all pages
<title>have 'TaskFlow' as their title, no meta description, and no Open Graph image—so when shared on social media, they appear as plain links."
| Issue | Impact | Quantification |
|---|---|---|
| Duplicate title | Search engines cannot distinguish between pages | Only 5 out of 50 indexed |
| No OG image | No preview image for social sharing | Share click-through rate: 0.3% |
| No JSON-LD | No rich media summary | No additional information in search results |
| No Sitemap | Google Can't Crawl Deep Pages | Indexing Delay of 30 Days |
(2) The Next.js Metadata API Solution
Use
generateMetadata()to generate unique SEO tags and JSON-LD structured data for each page.
// app/blog/[slug]/page.tsx — News SEO
import type { Metadata } from 'next'
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
return {
title: `${post.title} - TaskFlow Blog`,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.ogImage, width: 1200, height: 630 }]
}
}
}
(3) Revenue
| Metric | Before Optimization | After Optimization | Improvement |
|---|---|---|---|
| Google Indexing Rate | 10% | 98% | 9.8x |
| Social Sharing Click-Through Rate | 0.3% | 2.8% | 9.3x |
| Rich Media Search Results | ❌ None | ✅ Breadcrumbs + Article Cards | — |
| Page Discovery Speed | 30 days | < 24h | 30x |
3. Static and Dynamic Metadata
(1) Static Export
// app/about/page.tsx — Static Page metadata
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'About TaskFlow - Team Collaboration Platform',
description: 'TaskFlow Help 10,000+ Highly Effective Team Collaboration,Provide Project Management、Real-time collaboration and intelligent analytics features。',
keywords: ['Teamwork', 'Project Management', 'TaskFlow', 'SaaS'],
authors: [{ name: 'TaskFlow Team', url: 'https://taskflow.io' }]
}
(2) Dynamic Generation
// app/products/[id]/page.tsx — News metadata
import type { Metadata, ResolvingMetadata } from 'next'
type Props = { params: Promise<{ id: string }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }> }
export async function generateMetadata({ params, searchParams }: Props, parent: ResolvingMetadata): Promise<Metadata> {
const { id } = await params
const product = await fetch(`https://api.taskflow.io/products/${id}`).then(r => r.json())
return {
title: `${product.name} - TaskFlow Products`,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
url: `https://taskflow.io/products/${id}`,
siteName: 'TaskFlow',
images: [
{
url: product.ogImage,
width: 1200,
height: 630,
alt: product.name
}
],
locale: 'zh_CN',
type: 'website'
},
twitter: {
card: 'summary_large_image',
title: product.name,
description: product.description,
images: [product.ogImage]
},
alternates: {
canonical: `https://taskflow.io/products/${id}`,
languages: {
'en': `https://taskflow.io/en/products/${id}`,
'ja': `https://taskflow.io/ja/products/${id}`,
'ar': `https://taskflow.io/ar/products/${id}`
}
}
}
}
(3) Metadata Field Quick Reference Table
| Field | Purpose | Example |
|---|---|---|
title |
Page Title / <title> |
'Product Details - TaskFlow' |
description |
Search Engine Summary | 'TaskFlow Project Management Tool...' |
openGraph |
Facebook / LinkedIn Sharing | { title, description, images } |
twitter |
X (Twitter) card | { card: 'summary_large_image' } |
alternates.canonical |
Authoritative URL to Prevent Duplicate Content | https://taskflow.io/page |
alternates.languages |
hreflang multilingual | { 'en': '...' } |
robots |
Reptile command | { index: true, follow: true } |
▶ Example: Complete SEO for a Blog Post
Output:
Async function executes and returns fetched data.
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
interface Post {
title: string
excerpt: string
ogImage: string
publishedAt: string
author: string
tags: string[]
}
async function getPost(slug: string): Promise<Post> {
const res = await fetch(`https://api.taskflow.io/blog/${slug}`, { next: { revalidate: 3600 } })
return res.json()
}
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
return {
title: `${post.title} | TaskFlow Blog`,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author],
images: [{ url: post.ogImage, width: 1200, height: 630 }]
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.ogImage]
},
keywords: [...post.tags, 'TaskFlow', 'Project Management'],
alternates: { canonical: `https://taskflow.io/blog/${slug}` },
robots: { index: true, follow: true }
}
}
Output:
Defines TypeScript type(s): Post.
▶ Example: Using the robots meta tag to control web crawlers
Output:
Defines TypeScript type(s): Post.
// app/private/dashboard/page.tsx — Prevent search engines from indexing
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Dashboard - TaskFlow',
robots: {
index: false, // Do not index
follow: false, // Do Not Track Links
noarchive: true, // Do not cache
nosnippet: true, // Do not display summary
nocache: true // Do not cache
}
}
export default function DashboardPage() {
return <h1>Private Dashboard</h1>
}
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet" />
Output:
Browser renders: "Private Dashboard" heading. Search engines will not index this page — the <meta name="robots" content="noindex, nofollow, noarchive, nosnippet"> tag prevents crawling.
4. JSON-LD Structured Data
(1) Three Common Schemas
graph TB
A[JSON-LD Structured Data] --> B[BreadcrumbList<br/>Breadcrumb Navigation]
A --> C[Article<br/>Article Details]
A --> D[FAQPage<br/>Frequently Asked Questions]
B --> E[Display the search results path]
C --> F[Knowledge Panel + Cover Image]
D --> G[Search results display Q&A directly]
style A fill:#cce5ff
style B fill:#d4edda
style C fill:#d4edda
style D fill:#d4edda
| Schema | Use Case | Search Results Display |
|---|---|---|
| BreadcrumbList | All Pages | Display breadcrumb trail (Home > Products > Details) |
| Article | Blog Post | Article Card (Title + Summary + Cover Image + Publication Date) |
| FAQ Page | Help Center / Frequently Asked Questions | Display a list of questions and answers (collapsible) |
| Product | Product Page | Price + Inventory + Star Rating |
(2) JSON-LD Injection Function
// lib/jsonld.ts — JSON-LD Generation Tools
export function breadcrumbJsonld(items: { name: string; url: string }[]) {
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: item.url
}))
}
}
export function articleJsonld(post: {
title: string
excerpt: string
url: string
ogImage: string
publishedAt: string
author: string
}) {
return {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
description: post.excerpt,
image: post.ogImage,
datePublished: post.publishedAt,
author: { '@type': 'Person', name: post.author },
publisher: { '@type': 'Organization', name: 'TaskFlow', logo: 'https://taskflow.io/logo.png' },
mainEntityOfPage: { '@type': 'WebPage', '@id': post.url }
}
}
▶ Example: Article Page JSON-LD + Breadcrumb
Output:
TypeScript module executes successfully.
// app/blog/[slug]/page.tsx — Complete SEO + JSON-LD
import { breadcrumbJsonld, articleJsonld } from '@/lib/jsonld'
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getPost(slug)
const url = `https://taskflow.io/blog/${slug}`
const breadcrumb = breadcrumbJsonld([
{ name: 'Home', url: 'https://taskflow.io' },
{ name: 'Blog', url: 'https://taskflow.io/blog' },
{ name: post.title, url }
])
const article = articleJsonld({
title: post.title,
excerpt: post.excerpt,
url,
ogImage: post.ogImage,
publishedAt: post.publishedAt,
author: post.author
})
return (
<>
{/* JSON-LD Inject */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumb) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(article) }}
/>
<article>
<h1>{post.title}</h1>
<p>{post.excerpt}</p>
<div>{post.content}</div>
</article>
</>
)
}
Output:
Renders the ▶ Example: Article Page JSON-LD + Breadcrumb component UI as described in the section.
5. Sitemap and robots.txt
(1) Dynamic Sitemap Generation
// app/sitemap.ts — News Sitemap(Automatically include all pages)
import type { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://taskflow.io'
// Static Page
const staticPages = [
{ url: baseUrl, lastModified: new Date(), changeFrequency: 'monthly' as const, priority: 1.0 },
{ url: `${baseUrl}/about`, lastModified: new Date(), changeFrequency: 'monthly' as const, priority: 0.8 },
{ url: `${baseUrl}/blog`, lastModified: new Date(), changeFrequency: 'weekly' as const, priority: 0.9 },
{ url: `${baseUrl}/pricing`, lastModified: new Date(), changeFrequency: 'monthly' as const, priority: 0.8 },
{ url: `${baseUrl}/contact`, lastModified: new Date(), changeFrequency: 'yearly' as const, priority: 0.5 }
]
// Dynamic Blog Posts (from API)
const posts = await fetch('https://api.taskflow.io/blog/posts').then(r => r.json())
const blogPages = posts.map((post: { slug: string; updatedAt: string }) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly' as const,
priority: 0.7
}))
// Multilingual Pages
const locales = ['en', 'ja', 'ar']
const localizedPages = locales.flatMap((locale) =>
staticPages.map((page) => ({
url: `${baseUrl}/${locale}${page.url.replace(baseUrl, '')}`,
lastModified: page.lastModified,
changeFrequency: page.changeFrequency,
priority: page.priority * 0.9,
alternates: {
languages: Object.fromEntries(
locales.map((l) => [l, `${baseUrl}/${l}${page.url.replace(baseUrl, '')}`])
)
}
}))
)
return [...staticPages, ...blogPages, ...localizedPages]
}
(2) robots.txt
// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/api/', '/admin/', '/_next/', '/dashboard']
},
{
userAgent: 'GPTBot',
disallow: '/'
}
],
sitemap: 'https://taskflow.io/sitemap.xml'
}
}
▶ Example: Multilingual Sitemap Validation
# Generated sitemap.xml Excerpt
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://taskflow.io/about</loc>
<lastmod>2026-07-06</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://taskflow.io/en/about"/>
<xhtml:link rel="alternate" hreflang="ja" href="https://taskflow.io/ja/about"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://taskflow.io/ar/about"/>
</url>
</urlset>
6. Generating Dynamic OG Images
(1) @vercel/og Architecture
graph LR
A[Share the link on social media] --> B[Web crawler requests OG Image]
B --> C[@vercel/og Edge Function]
C --> D[Satori + React]
D --> E[Render JSX as PNG]
E --> F[1200×630 OG Image]
F --> G[Facebook / X / LinkedIn Show]
style C fill:#cce5ff
style D fill:#d4edda
style E fill:#fff3cd
| Library | Function | Description |
|---|---|---|
@vercel/og |
OG Image Generation Edge Function | Zero Server Overhead |
Satori |
JSX → SVG Conversion | Less than 1 ms |
resvg-wasm |
SVG → PNG render | ~5ms |
(2) Installation and Basic Examples
npm install @vercel/og
// app/og/route.tsx — OG Image Generation API
import { ImageResponse } from '@vercel/og'
export const runtime = 'edge'
export async function GET() {
return new ImageResponse(
(
<div style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)',
color: 'white',
fontSize: 60,
fontWeight: 700,
padding: 40
}}>
<h1>TaskFlow</h1>
<p style={{ fontSize: 32, opacity: 0.9 }}>Team Collaboration Platform</p>
</div>
),
{ width: 1200, height: 630 }
)
}
▶ Example: Dynamic Article OG Image
Output:
Renders the GET component UI.
// app/blog/[slug]/og/route.tsx — Featured Articles OG Image
import { ImageResponse } from '@vercel/og'
export const runtime = 'edge'
export async function GET(req: Request, { params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
// Get Article Information
const post = await fetch(`https://api.taskflow.io/blog/${slug}`).then(r => r.json())
return new ImageResponse(
(
<div style={{
width: 1200,
height: 630,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: 60,
background: 'linear-gradient(135deg, #1e1b4b 0%, #4f46e5 100%)',
color: 'white'
}}>
{/* Tags */}
<div style={{ display: 'flex', gap: 8 }}>
{post.tags?.slice(0, 3).map((tag: string) => (
<span key={tag} style={{
padding: '4px 12px',
borderRadius: 20,
background: 'rgba(255,255,255,0.2)',
fontSize: 18
}}>{tag}</span>
))}
</div>
{/* Title */}
<h1 style={{ fontSize: 52, margin: '20px 0', lineHeight: 1.2 }}>
{post.title}
</h1>
{/* Author + Date */}
<div style={{ display: 'flex', gap: 16, fontSize: 22, opacity: 0.8 }}>
<span>{post.author}</span>
<span>{new Date(post.publishedAt).toLocaleDateString('zh-CN')}</span>
</div>
{/* Logo */}
<div style={{ position: 'absolute', bottom: 40, right: 60, fontSize: 28, fontWeight: 'bold' }}>
TaskFlow Blog
</div>
</div>
),
{ width: 1200, height: 630 }
)
}
Output:
Fetches data server-side and renders a list of items from post.
Visible content: ))} | TaskFlow Blog
/blog/[slug]/og/route.tsx, and the final link is https://taskflow.io/blog/nextjs-seo-guide/og. Simply reference this address in generateMetadata.
7. Complete Example: Comprehensive Implementation of Multilingual SEO + OG Images
// app/layout.tsx — Root Layout (Default) SEO + Multilingual
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: { template: '%s | TaskFlow', default: 'TaskFlow - Team Collaboration Platform' },
description: 'TaskFlow Helping the World 10,000+ Highly Effective Team Collaboration。Provide Project Management、Real-Time Collaboration、AI Intelligent Analysis。',
openGraph: {
siteName: 'TaskFlow',
type: 'website',
locale: 'zh_CN',
images: [{ url: 'https://taskflow.io/og-default.png', width: 1200, height: 630 }]
},
twitter: { card: 'summary_large_image', site: '@taskflow' },
robots: { index: true, follow: true },
alternates: {
canonical: 'https://taskflow.io',
languages: {
'en': 'https://taskflow.io/en',
'ja': 'https://taskflow.io/ja',
'ar': 'https://taskflow.io/ar'
}
}
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh">
<body>{children}</body>
</html>
)
}
// app/sitemap.ts — Complete Sitemap
import type { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://taskflow.io'
const locales = ['zh', 'en', 'ja', 'ar'] as const
const pages = ['', '/about', '/blog', '/pricing', '/contact']
return pages.flatMap((page) =>
locales.map((locale) => ({
url: `${baseUrl}/${locale}${page}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: page === '' ? 1.0 : 0.8,
alternates: {
languages: Object.fromEntries(locales.map((l) => [l, `${baseUrl}/${l}${page}`]))
}
}))
)
}
// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: '*', allow: '/', disallow: ['/api/', '/admin/', '/dashboard'] },
sitemap: 'https://taskflow.io/sitemap.xml'
}
}
// app/blog/[slug]/page.tsx — The article page consolidates all SEO
import type { Metadata } from 'next'
type Props = { params: Promise<{ slug: string }> }
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const post = await fetch(`https://api.taskflow.io/blog/${slug}`).then(r => r.json())
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author],
images: [{ url: `https://taskflow.io/blog/${slug}/og`, width: 1200, height: 630 }]
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [`https://taskflow.io/blog/${slug}/og`]
},
alternates: {
canonical: `https://taskflow.io/blog/${slug}`,
languages: {
en: `https://taskflow.io/en/blog/${slug}`,
ja: `https://taskflow.io/ja/blog/${slug}`,
ar: `https://taskflow.io/ar/blog/${slug}`
}
}
}
}
export default async function BlogPostPage({ params }: Props) {
const { slug } = await params
return (
<article>
{/* JSON-LD Structured Data */}
<script type="application/ld+json" dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://taskflow.io' },
{ '@type': 'ListItem', position: 2, name: 'Blog', item: 'https://taskflow.io/blog' },
{ '@type': 'ListItem', position: 3, name: slug }
]
})
}} />
<h1>Article Title</h1>
<div>Article Content...</div>
</article>
)
}
❓ FAQ
generateMetadata() and metadata?metadata is a static export used for static routing (such as /about). generateMetadata() is an asynchronous function that dynamically generates metadata based on params / searchParams and is used for dynamic routing (such as /blog/[slug]). The two cannot be used simultaneously.<title> / <meta name="description">) are basic SEO tags. JSON-LD (<script type="application/ld+json">) is structured data that enables Google to display rich snippets (breadcrumbs, star ratings, collapsible FAQs). The two are complementary and both need to be implemented.@vercel/og runs on Edge Runtime (edge computing), and each image takes only ~5 ms to generate, resulting in virtually zero overhead. In a production environment, we recommend enabling CDN caching (Cache-Control: public, max-age=31536000, immutable).generateMetadata() at alternates.languages. Next.js automatically injects <link rel="alternate" hreflang="en" href="..."> into the page <head>. You also need to include the corresponding xhtml:link tag in the sitemap.changeFrequency and priority fields in the sitemap affect Google rankings?lastModified field be accurate—this is important for crawlers to determine whether to recrawl the pages.📖 Summary
generateMetadata()Implements dynamic SEO, supporting all fields: title, description, OpenGraph, and Twitter- JSON-LD structured data (BreadcrumbList / Article / FAQPage) enables search results to display rich snippets
- The sitemap and robots.txt files are dynamically generated via
app/sitemap.ts/app/robots.tsand support multilingual alternative links. - Configure hreflang multilingual SEO in
alternates.languagesto ensure search engines display the correct language version of the page to users in the corresponding regions @vercel/og+ Satori dynamically generates 1200×630 OG images in Edge Runtime, allowing each article to have a unique share preview- Comprehensive SEO Strategy: Metadata (Basic) + JSON-LD (Enhanced) + Sitemap (Discovery) + OG Image (Social)
📝 Exercises
-
Basic Question (⭐): Configure the
metadataexport in the static route/about, including the title, description, Open Graph, and Twitter card. -
Advanced Exercise (⭐⭐): Implement full SEO for a news detail page:
generateMetadata()Dynamically generate the title, description, OG tags, and Twitter card, inject Article JSON-LD structured data, and list all article URLs in the sitemap. -
Challenge (⭐⭐⭐): Build a complete OG image generation system:
@vercel/ogGenerate a 1200×630 share image for each blog post, including the title, author, and tags; reference the image URL ingenerateMetadata(); and finally, use the Google Rich Results Test to verify the correctness of the JSON-LD and OG tags.



