Next.js App RouterでのSEO対策とi18n実装:動的メタデータと多言語対応の実践ガイド
はじめに
Next.js 14のApp RouterとNext-intlを使用した国際化(i18n)対応のWebアプリケーションにおいて、SEOを最適化する実践的な実装方法を解説する。
特に動的生成ページでのメタデータ設定と多言語対応に焦点を当て、実際のECサイトを例に具体的な実装手順を説明する。
想定読者
- Next.jsのApp Routerの基本を理解している開発者
- TypeScriptの基本的な知識を持つエンジニア
- SEOの基礎知識がある実務者
開発環境
{
"next": "^14.2.22",
"next-intl": "^3.26.3",
"react": "^18",
"typescript": "^5"
}
1. プロジェクトの基本構成
1.1 ディレクトリ構造
src/
├── app/
│ └── [locale]/
│ └── product/
│ └── [id]/
│ ├── page.tsx
│ └── layout.tsx
├── lib/
│ └── metadata/
│ └── productMetadata.ts
├── api/
│ └── model/
│ └── Product.ts
└── i18n/
│ ├── routing.ts
│
└── messages/
├── en.json
└── ja.json
1.2 多言語ルーティングの実装
// src/i18n/routing.ts
import { defineRouting } from "next-intl/routing";
import { createNavigation } from "next-intl/navigation";
export const routing = defineRouting({
locales: ["en", "ja"],
defaultLocale: "ja",
});
export const { Link, redirect, usePathname, useRouter } = createNavigation(routing);
1.3 商品データ型の定義
// src/api/model/Product.ts
export interface Product {
id: string;
name: string;
code: string;
category: {
id: string;
name: string;
};
manufacturer?: string;
description: string;
images: {
small: string;
large: string;
};
price: number;
stock: number;
}
2. メタデータの動的生成
2.1 メタデータ生成ロジックの実装
// src/lib/metadata/productMetadata.ts
import { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import type { Product } from "@/api/model/Product";
import { routing } from "@/i18n/routing";
export async function generateProductMetadata(
productDataPromise: Promise<Product>,
locale: string
): Promise<Metadata> {
const [productData, t] = await Promise.all([
productDataPromise,
getTranslations({ locale, namespace: "metadataProduct" }),
]);
// SEO最適化:情報の構造化
const descriptionParts = [
productData.name,
`${productData.category.name} #${productData.code}`,
productData.manufacturer ? `${t("manufacturedBy")}${productData.manufacturer}` : "",
t("checkPrice")
].filter((item): item is string =>
item !== undefined && item !== null && item !== ""
);
const description = descriptionParts.join(" | ");
// 動的キーワード生成
const productSpecificKeywords = [
productData.name,
productData.category.name,
productData.manufacturer,
`${productData.name} ${t("price")}`,
`${productData.name} ${t("stock")}`,
`${productData.category.name} ${t("products")}`
].filter((item): item is string =>
item !== undefined && item !== null
);
const baseKeywords = t("keywords").split(", ");
const allKeywords = [...new Set([...productSpecificKeywords, ...baseKeywords])];
return {
title: `${productData.name} | ${productData.category.name} | ${t("siteTitle")}`,
description,
keywords: allKeywords.join(", "),
metadataBase: new URL("https://example.com"),
alternates: {
canonical: `/${locale}/product/${productData.id}`,
languages: {
"x-default": `/en/product/${productData.id}`,
...Object.fromEntries(
routing.locales.map((l) => [l, `/${l}/product/${productData.id}`])
),
},
},
openGraph: {
type: "website",
siteName: t("siteTitle"),
title: `${productData.name} | ${productData.category.name}`,
description,
locale,
alternateLocale: routing.locales.filter((l) => l !== locale),
images: productData.images.large ? [
{
url: productData.images.large,
width: 1200,
height: 630,
alt: productData.name,
},
] : undefined,
},
robots: {
index: true,
follow: true,
},
};
}
2.2 翻訳ファイルの設定
// messages/en.json
{
"metadataProduct": {
"siteTitle": "Example Store",
"manufacturedBy": "Manufactured by",
"checkPrice": "Check price and availability",
"price": "price",
"stock": "stock",
"products": "products",
"keywords": "online store, ecommerce, shopping, price comparison, stock check, product reviews, online shopping"
}
}
// messages/ja.json
{
"metadataProduct": {
"siteTitle": "Example Store",
"manufacturedBy": "製造元",
"checkPrice": "価格と在庫状況をチェック",
"price": "価格",
"stock": "在庫",
"products": "商品",
"keywords": "オンラインストア, eコマース, 通販, 価格比較, 在庫確認, 商品レビュー, オンラインショッピング"
}
}
2.3 ページコンポーネントでの実装
// app/[locale]/product/[id]/page.tsx
import { generateProductMetadata } from "@/lib/metadata/productMetadata";
import { cache } from 'react';
import { notFound } from 'next/navigation';
const getProductDataCached = cache(async (id: string) => {
const response = await fetch(`/api/products/${id}`);
if (!response.ok) {
notFound();
}
return response.json();
});
export async function generateMetadata({
params
}: {
params: { id: string; locale: string }
}) {
const productDataPromise = getProductDataCached(params.id);
return generateProductMetadata(productDataPromise, params.locale);
}
export default async function ProductPage({
params
}: {
params: { id: string }
}) {
const productData = await getProductDataCached(params.id);
// ページコンテンツの実装
}
3. SEO最適化のポイント
3.1 メタデータの構造化
メタデータの構造化では以下の点に注意する:
-
タイトルの最適化
- 重要な情報から順に配置
- パイプ(|)による適切な区切り
- サイト名を含めてブランド認知を向上
-
説明文の構造化
- 情報の明確な区分け
- 必要な情報のみを含める
- 検索意図に合致する構造
-
キーワードの最適化
- 動的生成と静的キーワードの組み合わせ
- 検索パターンを考慮
- 自然な形での配置
3.2 多言語対応での注意点
-
URL構造の設計
- 言語コードを含むURL設計
- 適切なcanonical URL
- x-defaultの設定
-
コンテンツの最適化
- 各言語に適した説明文
- 文化的な配慮
- キーワードの適切な翻訳
3.3 OpenGraphの設定
OpenGraph設定では以下の点に注意する:
-
基本設定
- type設定の適切な選択
- siteName, titleの一貫性
- 説明文の最適化
-
画像の取り扱い
- 適切なサイズと比率の設定
- alt属性の設定
- 画像未設定時の対応
4. パフォーマンスの最適化
4.1 データフェッチの最適化
-
キャッシュの活用
const getProductDataCached = cache(async (id: string) => { // データフェッチロジック });
- 重複リクエストの防止
- レスポンスタイムの改善
-
エラーハンドリング
- 404ページの適切な表示
- ユーザー体験の維持
- クローラビリティの確保
4.2 レンダリングの最適化
-
メタデータ生成の分離
- ロジックの明確な分離
- 再利用性の向上
- メンテナンス性の向上
-
非同期処理の最適化
- 並列処理による高速化
- リソースの効率的な使用
まとめ
Next.js App RouterでのSEO対策と多言語対応について、以下の実装ポイントを解説した:
- 動的メタデータ生成の実装方法
- 多言語対応での注意点と最適化方法
- SEO最適化のベストプラクティス
- パフォーマンス最適化の手法
これらの実装により、SEOに最適化された多言語対応のWebアプリケーションを構築できる。