Next.js: SEO & メタデータ API
最終更新:2026-08-26
SEO はウェブサイトにとって無料のトラフィックエンジンです。正しいメタデータにより、Google がサイトをインデックスする速度が 3 倍速くなります。
1. 学習目標
generateMetadata()でページタイトル、説明、OpenGraph、Twitter メタデータを動的に生成metadata静的エクスポートと静的ルートのメタデータ- JSON-LD 構造化データ(BreadcrumbList / Article / FAQPage)
- サイトマップと
robots.txtの動的生成 - hreflang マルチリンガル SEO 設定
@vercel/og+ Satori: OG 画像の動的生成
2. コンテンツ運用マネージャーの実話
(1) 課題: ローンチから 3 か月経っても Google は 5 ページしかインデックスしていない
Bob は TaskFlow のコンテンツ運用を担当しています。ブログをローンチして 3 か月、50 記事を執筆しました:
「Google Search Console には、インデックスされているのはわずか 5 ページと表示されています。調査すると、すべてのページの
<title>が 'TaskFlow' になっており、meta description もなく、Open Graph 画像もありません。そのため、SNS で共有するとプレーンなリンクとして表示されます。」
| 問題 | 影響 | 定量化 |
|---|---|---|
| 重複タイトル | 検索エンジンがページを区別できない | 50 件中 5 件のみインデックス |
| OG 画像なし | SNS 共有にプレビュー画像なし | 共有クリック率: 0.3% |
| JSON-LD なし | リッチメディアサマリーなし | 検索結果に追加情報なし |
| サイトマップなし | Google が深いページをクロールできない | インデックス遅延 30 日 |
(2) Next.js メタデータ API の解決策
generateMetadata()を使用して各ページに一意の SEO タグと JSON-LD 構造化データを生成します。
// app/blog/[slug]/page.tsx — 記事 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 ブログ`,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.ogImage, width: 1200, height: 630 }]
}
}
}
(3) 効果
| 指標 | 最適化前 | 最適化後 | 改善 |
|---|---|---|---|
| Google インデックス率 | 10% | 98% | 9.8倍 |
| SNS 共有クリック率 | 0.3% | 2.8% | 9.3倍 |
| リッチメディア検索結果 | ❌ なし | ✅ パンくず + 記事カード | — |
| ページ発見速度 | 30 日 | < 24h | 30倍 |
3. 静的メタデータと動的メタデータ
(1) 静的エクスポート
// app/about/page.tsx — 静的ページのメタデータ
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'TaskFlow について - チームコラボレーションプラットフォーム',
description: 'TaskFlow は 10,000 以上のチームの効率的なコラボレーションを支援します。プロジェクト管理、リアルタイムコラボレーション、インテリジェント分析機能を提供します。',
keywords: ['チームワーク', 'プロジェクト管理', 'TaskFlow', 'SaaS'],
authors: [{ name: 'TaskFlow チーム', url: 'https://taskflow.io' }]
}
(2) 動的生成
// app/products/[id]/page.tsx — 動的メタデータ
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 製品`,
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) メタデータフィールド早見表
| フィールド | 目的 | 例 |
|---|---|---|
title |
ページタイトル / <title> |
'製品詳細 - TaskFlow' |
description |
検索エンジンサマリー | 'TaskFlow プロジェクト管理ツール...' |
openGraph |
Facebook / LinkedIn 共有 | { title, description, images } |
twitter |
X (Twitter) カード | { card: 'summary_large_image' } |
alternates.canonical |
重複コンテンツ防止の正規 URL | https://taskflow.io/page |
alternates.languages |
hreflang マルチリンガル | { 'en': '...' } |
robots |
クローラー指示 | { index: true, follow: true } |
▶ サンプル: ブログ記事の完全な SEO
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 ブログ`,
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', 'プロジェクト管理'],
alternates: { canonical: `https://taskflow.io/blog/${slug}` },
robots: { index: true, follow: true }
}
}
Output:
Defines TypeScript type(s): Post.
▶ サンプル: robots メタタグでウェブクローラーを制御
Output:
Defines TypeScript type(s): Post.
// app/private/dashboard/page.tsx — 検索エンジンによるインデックスを防止
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'ダッシュボード - TaskFlow',
robots: {
index: false, // インデックスしない
follow: false, // リンクを追跡しない
noarchive: true, // キャッシュしない
nosnippet: true, // サマリーを表示しない
nocache: true // キャッシュしない
}
}
export default function DashboardPage() {
return <h1>プライベートダッシュボード</h1>
}
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet" />
Output:
Browser renders: "プライベートダッシュボード" heading. Search engines will not index this page — the <meta name="robots" content="noindex, nofollow, noarchive, nosnippet"> tag prevents crawling.
4. JSON-LD 構造化データ
(1) 3 つの一般的なスキーマ
graph TB
A[JSON-LD 構造化データ] --> B[BreadcrumbList<br/>パンくずナビゲーション]
A --> C[Article<br/>記事詳細]
A --> D[FAQPage<br/>よくある質問]
B --> E[検索結果にパスを表示]
C --> F[ナレッジパネル + カバー画像]
D --> G[検索結果に Q&A を直接表示]
style A fill:#cce5ff
style B fill:#d4edda
style C fill:#d4edda
style D fill:#d4edda
| スキーマ | ユースケース | 検索結果の表示 |
|---|---|---|
| BreadcrumbList | 全ページ | パンくずトレイルを表示 (ホーム > 製品 > 詳細) |
| Article | ブログ記事 | 記事カード (タイトル + サマリー + カバー画像 + 公開日) |
| FAQPage | ヘルプセンター / FAQ | 質問と回答のリストを表示 (折りたたみ可能) |
| Product | 製品ページ | 価格 + 在庫 + 星評価 |
(2) JSON-LD 注入関数
// lib/jsonld.ts — JSON-LD 生成ツール
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 }
}
}
▶ サンプル: 記事ページ JSON-LD + パンくず
Output:
TypeScript module executes successfully.
// app/blog/[slug]/page.tsx — 完全な 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: 'ホーム', url: 'https://taskflow.io' },
{ name: 'ブログ', 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 注入 */}
<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 ▶ サンプル: 記事ページ JSON-LD + パンくず component UI as described in the section.
5. サイトマップと robots.txt
(1) 動的サイトマップ生成
// app/sitemap.ts — 動的サイトマップ(全ページを自動的に含む)
import type { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://taskflow.io'
// 静的ページ
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 }
]
// 動的ブログ記事(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
}))
// マルチリンガルページ
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'
}
}
▶ サンプル: マルチリンガルサイトマップの検証
# 生成された sitemap.xml の抜粋
<?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. 動的 OG 画像の生成
(1) @vercel/og アーキテクチャ
graph LR
A[SNS でリンクを共有] --> B[ウェブクローラーが OG 画像をリクエスト]
B --> C[@vercel/og Edge Function]
C --> D[Satori + React]
D --> E[JSX を PNG としてレンダリング]
E --> F[1200×630 OG 画像]
F --> G[Facebook / X / LinkedIn で表示]
style C fill:#cce5ff
style D fill:#d4edda
style E fill:#fff3cd
| ライブラリ | 機能 | 説明 |
|---|---|---|
@vercel/og |
OG 画像生成 Edge Function | サーバーオーバーヘッドゼロ |
Satori |
JSX → SVG 変換 | 1 ms 未満 |
resvg-wasm |
SVG → PNG レンダリング | ~5ms |
(2) インストールと基本例
npm install @vercel/og
// app/og/route.tsx — OG 画像生成 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 }}>チームコラボレーションプラットフォーム</p>
</div>
),
{ width: 1200, height: 630 }
)
}
▶ サンプル: 動的記事 OG 画像
Output:
Renders the GET component UI.
// app/blog/[slug]/og/route.tsx — 特集記事 OG 画像
import { ImageResponse } from '@vercel/og'
export const runtime = 'edge'
export async function GET(req: Request, { params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
// 記事情報を取得
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'
}}>
{/* タグ */}
<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>
{/* タイトル */}
<h1 style={{ fontSize: 52, margin: '20px 0', lineHeight: 1.2 }}>
{post.title}
</h1>
{/* 著者 + 日付 */}
<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>
{/* ロゴ */}
<div style={{ position: 'absolute', bottom: 40, right: 60, fontSize: 28, fontWeight: 'bold' }}>
TaskFlow ブログ
</div>
</div>
),
{ width: 1200, height: 630 }
)
}
Output:
Fetches data server-side and renders a list of items from post.
Visible content: ))} | TaskFlow ブログ
/blog/[slug]/og/route.tsx にあり、最終的なリンクは https://taskflow.io/blog/nextjs-seo-guide/og です。このアドレスを generateMetadata で参照するだけです。
7. 完全なサンプル: マルチリンガル SEO + OG 画像の総合実装
// app/layout.tsx — ルートレイアウト(デフォルト)SEO + マルチリンガル
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: { template: '%s | TaskFlow', default: 'TaskFlow - チームコラボレーションプラットフォーム' },
description: 'TaskFlow は世界中の 10,000 以上のチームの効率的なコラボレーションを支援します。プロジェクト管理、リアルタイムコラボレーション、AI インテリジェント分析を提供します。',
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 — 完全なサイトマップ
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 — 記事ページがすべての 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 構造化データ */}
<script type="application/ld+json" dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'ホーム', item: 'https://taskflow.io' },
{ '@type': 'ListItem', position: 2, name: 'ブログ', item: 'https://taskflow.io/blog' },
{ '@type': 'ListItem', position: 3, name: slug }
]
})
}} />
<h1>記事タイトル</h1>
<div>記事内容...</div>
</article>
)
}
❓ よくある質問
generateMetadata() と metadata のエクスポートの違いは何ですか?metadata は静的エクスポートで、静的ルート(/about など)に使用されます。generateMetadata() は非同期関数で、params / searchParams に基づいて動的にメタデータを生成し、動的ルート(/blog/[slug] など)に使用されます。この 2 つを同時に使用することはできません。<title> / <meta name="description">)は基本的な SEO タグです。JSON-LD(<script type="application/ld+json">)は構造化データで、Google がリッチスニペット(パンくず、星評価、折りたたみ可能な FAQ)を表示できるようにします。この 2 つは補完的であり、両方を実装する必要があります。@vercel/og は Edge Runtime(エッジコンピューティング)で実行され、各画像の生成には ~5 ms しかかからず、ほぼゼロのオーバーヘッドです。本番環境では、CDN キャッシング(Cache-Control: public, max-age=31536000, immutable)を有効にすることをお勧めします。generateMetadata() の alternates.languages で設定します。Next.js は自動的に <link rel="alternate" hreflang="en" href="..."> をページの <head> に注入します。サイトマップにも対応する xhtml:link タグを含める必要があります。changeFrequency と priority フィールドは Google のランキングに影響しますか?lastModified フィールドを正確にすることが推奨されます。これはクローラーが再クロールするかどうかを判断する上で重要です。📖 まとめ
generateMetadata()は動的 SEO を実装し、title、description、OpenGraph、Twitter のすべてのフィールドをサポートします- JSON-LD 構造化データ(BreadcrumbList / Article / FAQPage)により、検索結果にリッチスニペットが表示されます
- サイトマップと robots.txt ファイルは
app/sitemap.ts/app/robots.tsで動的に生成され、マルチリンガル代替リンクをサポートします - hreflang マルチリンガル SEO を
alternates.languagesで設定し、検索エンジンが対応する地域のユーザーに正しい言語バージョンのページを表示するようにします @vercel/og+ Satori は Edge Runtime で 1200×630 の OG 画像を動的に生成し、各記事に一意の共有プレビューを提供します- 包括的 SEO 戦略: メタデータ (基本) + JSON-LD (強化) + サイトマップ (発見) + OG 画像 (SNS)
📝 練習問題
-
基本問題 (⭐): 静的ルート
/aboutにmetadataエクスポートを設定し、title、description、Open Graph、Twitter カードを含めてください。 -
応用問題 (⭐⭐): 記事詳細ページの完全な SEO を実装してください:
generateMetadata()でタイトル、説明、OG タグ、Twitter カードを動的に生成し、Article JSON-LD 構造化データを注入し、サイトマップにすべての記事 URL をリストアップします。 -
発展問題 (⭐⭐⭐): 完全な OG 画像生成システムを構築してください:
@vercel/ogで各ブログ記事に 1200×630 の共有画像(タイトル、著者、タグを含む)を生成し、generateMetadata()で画像 URL を参照し、最後に Google リッチリザルトテストで JSON-LD と OG タグの正しさを検証します。