Next.jsでWebサイトやブログを構築していると、「SEO設定をどう実装すればいいのか」という疑問にぶつかることがあります。特に、App Routerが標準となった現在、「next-seoライブラリを使うべきか」「Next.js組み込みのgenerateMetadataで十分なのか」という判断に迷う方が多いのではないでしょうか。
検索して出てくる記事の多くは、Pages Router時代のnext-seoコンポーネントの使い方を紹介したものです。App Router移行後に同じコードを書こうとして「なぜか動かない」という経験をした方もいるかもしれません。
さらに、next-seo自体もv7でAPIが大きく変わり、以前のコードがそのまま使えなくなっています。「何が変わったのか」「自分のプロジェクトには何が必要なのか」が分からず、実装を先送りにしてしまうケースも少なくありません。
本記事では、next-seo v7の最新API仕様と、App Router時代のSEO実装の正しいアプローチを整理します。Pages RouterとApp Routerそれぞれの正解を具体的なコード例で示しますので、自分のプロジェクトに即適用できる内容を目指しています。
next-seoとは
next-seoは、Next.jsプロジェクトでSEO関連のメタタグや構造化データを管理するためのオープンソースライブラリです。GitHubで公開されており、2026年4月時点での最新バージョンはv7.2.0です。
インストールは以下のコマンドで行います。
npm install next-seo
# または
yarn add next-seo
pnpm add next-seov7での大きな変更点
2024年10月にリリースされたv7は、v6以前と比べてAPIが大きく変わりました。最も重要な変更点を表にまとめます。
| 項目 | v6以前 | v7以降 |
|---|---|---|
| Pages Routerのimport先 | next-seo | next-seo/pages |
| Pages Routerの主要API | <NextSeo> コンポーネント | generateNextSeo() 関数 |
| App Router対応 | 制限あり | JSON-LDコンポーネントは useAppDir={true} で使用 |
| 標準メタタグ(App Router) | next-seoで設定 | Next.js組み込みのMetadata APIを推奨 |
この変更により、App Routerを使っている場合、next-seoはJSON-LD(構造化データ)の挿入に特化して使い、通常のメタタグ管理はNext.js組み込みのgenerateMetadataに任せるというアーキテクチャが推奨されています。
App RouterとPages Router、どちらを使うべきか
まず、自分のプロジェクトがApp RouterとPages Routerのどちらを使っているかを確認しましょう。
- App Router: Next.js 13以降で導入。
app/ディレクトリにファイルを置く。現在のデフォルト - Pages Router: Next.js 12以前から存在する旧来の方式。
pages/ディレクトリにファイルを置く
2026年現在、新規プロジェクトではApp Routerがデフォルト選択です。既存プロジェクトのPages RouterからApp Routerへの移行も進んでいます。
自分のプロジェクトがどちらを使っているかによって、SEO実装の方法が異なります。
App RouterでのSEO実装
App Routerを使っているプロジェクトでは、Next.js組み込みのMetadata APIを使ってSEOを実装します。next-seoライブラリは、JSON-LD(構造化データ)が必要な場合のみ補助的に使います。
静的メタデータの設定
ページやレイアウトのファイルで metadata オブジェクトをエクスポートするだけで、静的なメタタグを設定できます。
// app/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: {
template: '%s | サイト名',
default: 'サイト名',
},
description: 'サイトの説明文',
openGraph: {
type: 'website',
locale: 'ja_JP',
url: 'https://example.com',
siteName: 'サイト名',
},
twitter: {
card: 'summary_large_image',
site: '@yourtwitterhandle',
},
};title.template を使うと、各ページのタイトルに「ページ名 | サイト名」という形式が自動適用されます。
動的メタデータの設定(ブログ記事等)
ブログ記事のように、ページごとに異なるメタ情報が必要な場合は generateMetadata 関数を使います。
// app/tech-blog/[slug]/page.tsx
import type { Metadata } from 'next';
type Props = {
params: { slug: string };
};
// ブログ記事のデータをCMSから取得する関数(例)
async function getPost(slug: string) {
const res = await fetch(`https://your-cms.io/api/posts/${slug}`);
return res.json();
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.metaDescription,
alternates: {
canonical: `https://example.com/tech-blog/${params.slug}`,
},
openGraph: {
title: post.title,
description: post.metaDescription,
images: [
{
url: post.eyecatch.url,
width: 1200,
height: 630,
alt: post.title,
},
],
},
};
}
export default async function BlogPostPage({ params }: Props) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
{/* 記事本文 */}
</article>
);
}ポイントは、generateMetadata と default export の両方で同じデータ(getPost)を使っていることです。Next.js はリクエストのデデュープ(重複排除)を行うため、同じエンドポイントへのfetchは1回のリクエストにまとめられます。
Canonical URLの設定
alternates.canonical を使ってCanonical URLを設定します。
export const metadata: Metadata = {
alternates: {
canonical: 'https://example.com/page',
},
};App RouterではMetadata APIでよい理由
「next-seoを使わなくていいの?」という疑問を持つ方もいると思います。App Routerでは、Next.js組み込みのMetadata APIが充実しており、next-seoが従来提供していた機能(title、description、OGP、Twitter Card、Canonical)のほとんどをカバーしています。
外部ライブラリへの依存を減らし、Next.jsの公式機能として長期サポートが期待できるという点でも、App RouterではMetadata APIの使用が推奨されます。
App RouterでJSON-LDが必要な場合のnext-seo活用
ブログ記事に「BlogPosting」などの構造化データ(JSON-LD)を追加したい場合は、next-seoのJSON-LDコンポーネントを活用できます。App Routerで使う場合は useAppDir={true} プロップが必要です。
// app/tech-blog/[slug]/page.tsx
import { ArticleJsonLd } from 'next-seo';
export default async function BlogPostPage({ params }: Props) {
const post = await getPost(params.slug);
return (
<>
<ArticleJsonLd
useAppDir={true}
type="BlogPosting"
url={`https://example.com/tech-blog/${post.slug}`}
title={post.title}
images={[post.eyecatch.url]}
datePublished={post.publishedAt}
dateModified={post.updatedAt}
authorName={post.author.name}
description={post.metaDescription}
/>
<article>
<h1>{post.title}</h1>
{/* 記事本文 */}
</article>
</>
);
}JSON-LDは検索結果でのリッチスニペット(記事の公開日、著者名など)表示につながるため、ブログサイトでは積極的に設定することをおすすめします。
Pages RouterでのSEO実装(next-seo v7)
Pages Routerを使っている既存プロジェクトでは、next-seo v7を使います。ただし、v7ではAPIが変わっているため、古いコードをそのまま使うとエラーになる場合があります。
v7でのimport先の変更
v7からPages Router向けの関数は next-seo/pages からインポートします。
// v6以前(v7では動作しない)
import { NextSeo, DefaultSeo } from 'next-seo';
// v7の正しい書き方
import { generateNextSeo, generateDefaultSeo } from 'next-seo/pages';サイト全体のデフォルトSEO設定
_app.tsx(または _app.js)でサイト全体のデフォルトSEOを設定します。
// pages/_app.tsx
import Head from 'next/head';
import { generateDefaultSeo } from 'next-seo/pages';
import type { AppProps } from 'next/app';
const DEFAULT_SEO = {
titleTemplate: '%s | サイト名',
defaultTitle: 'サイト名',
description: 'サイトの説明文',
openGraph: {
type: 'website',
locale: 'ja_JP',
url: 'https://example.com/',
siteName: 'サイト名',
},
twitter: {
handle: '@yourtwitterhandle',
site: '@yourtwitterhandle',
cardType: 'summary_large_image',
},
};
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<>
<Head>{generateDefaultSeo(DEFAULT_SEO)}</Head>
<Component {...pageProps} />
</>
);
}ページごとのSEO設定
個別ページでは generateNextSeo 関数を使い、<Head> コンポーネントでラップします。
// pages/tech-blog/[slug].tsx
import Head from 'next/head';
import { generateNextSeo } from 'next-seo/pages';
import { GetStaticProps } from 'next';
type Props = {
post: {
title: string;
metaDescription: string;
slug: string;
eyecatch: { url: string };
};
};
export default function BlogPostPage({ post }: Props) {
return (
<>
<Head>
{generateNextSeo({
title: post.title,
description: post.metaDescription,
canonical: `https://example.com/tech-blog/${post.slug}`,
openGraph: {
title: post.title,
description: post.metaDescription,
images: [
{
url: post.eyecatch.url,
width: 1200,
height: 630,
alt: post.title,
},
],
},
})}
</Head>
<article>
<h1>{post.title}</h1>
{/* 記事本文 */}
</article>
</>
);
}
export const getStaticProps: GetStaticProps = async ({ params }) => {
const post = await getPost(params?.slug as string);
return { props: { post } };
};v6からv7へのマイグレーション
既存プロジェクトがv6以前のnext-seoを使っている場合、v7へのアップグレード時には以下の対応が必要です。
- importの変更:
import { NextSeo } from 'next-seo'→import { generateNextSeo } from 'next-seo/pages' - コンポーネントから関数へ:
<NextSeo title="..." />→generateNextSeo({ title: '...' }) <Head>でのラップ:generateNextSeoの結果を<Head>コンポーネントでラップする
next-seoの必要性まとめ
「next-seoは今でも必要か?」という疑問への回答を整理します。
| 状況 | next-seoの必要性 | 推奨実装 |
|---|---|---|
| App Router(新規プロジェクト) | JSON-LDのみ必要な場合に有用 | Metadata API + next-seo(JSON-LDのみ、useAppDir={true}) |
| App Router(Pages Routerから移行) | 基本的に不要 | Metadata APIへの移行を推奨 |
| Pages Router(既存プロジェクト) | 有用 | next-seo v7(next-seo/pagesからimport) |
| Pages Router(新規プロジェクト) | 有用 | next-seo v7 または App Router への移行を検討 |
まとめ
Next.jsでのSEO実装は、App RouterとPages Routerで方法が大きく異なります。本記事のポイントを振り返ります。
- App Router: Next.js組み込みの
metadataオブジェクトまたはgenerateMetadata関数を使う。next-seoはJSON-LDが必要な場合のみuseAppDir={true}フラグを付けて活用する - Pages Router: next-seo v7を使う。v7からはimport先が
next-seo/pagesに変わり、generateNextSeo関数ベースのAPIになっている - next-seo v7の最大の変更点:
<NextSeo>コンポーネントがgenerateNextSeo関数に変わった(Pages Router)
まず自分のプロジェクトがApp RouterとPages Routerのどちらを使っているかを確認し、本記事のコード例を参考に実装してみてください。
