0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js App RouterでのSEO対策とi18n実装:動的メタデータと多言語対応

Last updated at Posted at 2025-02-01

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 メタデータの構造化

メタデータの構造化では以下の点に注意する:

  1. タイトルの最適化

    • 重要な情報から順に配置
    • パイプ(|)による適切な区切り
    • サイト名を含めてブランド認知を向上
  2. 説明文の構造化

    • 情報の明確な区分け
    • 必要な情報のみを含める
    • 検索意図に合致する構造
  3. キーワードの最適化

    • 動的生成と静的キーワードの組み合わせ
    • 検索パターンを考慮
    • 自然な形での配置

3.2 多言語対応での注意点

  1. URL構造の設計

    • 言語コードを含むURL設計
    • 適切なcanonical URL
    • x-defaultの設定
  2. コンテンツの最適化

    • 各言語に適した説明文
    • 文化的な配慮
    • キーワードの適切な翻訳

3.3 OpenGraphの設定

OpenGraph設定では以下の点に注意する:

  1. 基本設定

    • type設定の適切な選択
    • siteName, titleの一貫性
    • 説明文の最適化
  2. 画像の取り扱い

    • 適切なサイズと比率の設定
    • alt属性の設定
    • 画像未設定時の対応

4. パフォーマンスの最適化

4.1 データフェッチの最適化

  1. キャッシュの活用

    const getProductDataCached = cache(async (id: string) => {
      // データフェッチロジック
    });
    
    • 重複リクエストの防止
    • レスポンスタイムの改善
  2. エラーハンドリング

    • 404ページの適切な表示
    • ユーザー体験の維持
    • クローラビリティの確保

4.2 レンダリングの最適化

  1. メタデータ生成の分離

    • ロジックの明確な分離
    • 再利用性の向上
    • メンテナンス性の向上
  2. 非同期処理の最適化

    • 並列処理による高速化
    • リソースの効率的な使用

まとめ

Next.js App RouterでのSEO対策と多言語対応について、以下の実装ポイントを解説した:

  • 動的メタデータ生成の実装方法
  • 多言語対応での注意点と最適化方法
  • SEO最適化のベストプラクティス
  • パフォーマンス最適化の手法

これらの実装により、SEOに最適化された多言語対応のWebアプリケーションを構築できる。

参考文献

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?