Menu

メタデータシステム

メタデータとは?

メタデータは、ウェブページのコンテンツを説明する情報で、HTMLの<head>タグ内に含まれており、SEOやソーシャルメディア共有にとって非常に重要です。

例:

<head>
  <title>Nexty.dev - Build and ship your SaaS faster</title>
  <meta name="description" content="Next.js SaaS starter template..." />
  <meta property="og:title" content="Nexty.dev" />
  <meta property="og:image" content="https://nexty.dev/og.png" />
  <link rel="canonical" href="https://nexty.dev/" />
  <link rel="alternate" hreflang="en-US" href="https://nexty.dev/" />
  <link rel="alternate" hreflang="zh-CN" href="https://nexty.dev/zh" />
</head>

Nexty.devメタデータシステム

コア機能:constructMetadata()

場所:lib/metadata.ts

目的:

  • すべてのページのメタデータを統一的に生成
  • 多言語を自動的に処理
  • カノニカルタグとhreflangタグを自動生成
  • Open GraphとTwitterカードを自動生成

関数パラメータ

lib/metadata.ts
type MetadataProps = {
  page?: string              // ページ名(非推奨、設定不要)
  title?: string             // ページタイトル
  description?: string       // ページ説明
  images?: string[]          // OG画像(相対パスまたは絶対パス)
  noIndex?: boolean          // インデックスを防ぐかどうか(デフォルトはfalse)
  locale?: Locale            // 現在の言語(en、zh、ja)
  path?: string              // ページパス(/で始まる必要がある)
  canonicalUrl?: string      // カノニカルURL(オプション)
  availableLocales?: string[] // 利用可能な言語リスト(オプション)
  useDefaultOgImage?: boolean // デフォルトはtrue
}

1. title - ページタイトル

title: "About Us"

生成ルール:

  • ホームページ: {title} - {siteConfig.tagLine}
    • 例:Nexty.dev - Build and ship your SaaS faster
  • その他のページ: {title} | {siteConfig.name}
    • 例:About Us | Nexty.dev

指定しない場合、多言語ファイルのデフォルト値が使用されます(Home名前空間のtitle)。

2. description - ページ説明

description: "Learn more about our mission and team."

目的:

  • 検索結果に表示される
  • ソーシャルメディアカードに表示される

指定しない場合、多言語ファイルのデフォルト値が使用されます。

3. images - OG画像

images: post.featuredImageUrl ? [post.featuredImageUrl] : [],

サポートされている形式:

  • 相対パス:["images/about.png"]https://nexty.dev/images/about.png
  • 絶対パス:["https://cdn.example.com/about.png"]

ほとんどの場合、個別のページに画像を設定する必要はありません。システムはページの言語に基づいてデフォルト画像を自動的に選択します。例えば:

  • 英語:/og.png
  • 中国語:/og_zh.png
  • 日本語:/og_ja.png

4. noIndex - インデックスを防ぐ

noIndex: true

目的:

  • <meta name="robots" content="noindex, nofollow" />を生成
  • 検索エンジンにページをインデックスしないよう指示

使用例:

  • プレミアムコンテンツページ
  • ログイン後のみアクセス可能なページ
  • テストページ
  • 404ページ

5. locale - 現在の言語

現在の言語識別子を渡すことで、メタデータ内の言語情報と必要な多言語URLを自動的に処理します。

locale: "zh" // en、zh、ja

目的:

  • URL生成に影響
  • OG画像の選択に影響
  • <html lang="zh">属性に影響

6. path - ページパス

path/で始まる必要があり、言語プレフィックスを含めてはいけません。例えば:

path: "/about"

constructMetadata()は、localepathを組み合わせて多言語情報とURLを自動的に処理します。

目的:

  • カノニカルURLの生成に使用され、検索エンジンがページの正規版を識別し、重複コンテンツの問題を回避するのに役立つ
  • hreflangリンクの生成に使用され、検索エンジンがページの言語と地域版を理解し、異なる地域のユーザーに正しい言語版を提供できるようにする

7. canonicalUrl - カノニカルURL

canonicalUrl: "/blogs"

使用例:

複数のURLが同じコンテンツを指している場合、例えば/blogs/blogs?ref=docsの場合、検索エンジンに正規版のURLが/blogsであることを伝える必要があります。これにより、検索エンジンがパラメータ付きURLを新しいページとして扱い、SEOの権威が分散するのを防ぎます。

通常、手動設定は不要で、システムは自動的にpathを参照して設定します。手動設定が必要なのは、現在のページを別のURLに向ける必要がある場合のみです。

8. availableLocales - 利用可能な言語リスト

availableLocales: ["en", "zh"]  // 英語と中国語版のみ利用可能

使用例:

ページにすべての言語版がない場合(ブログ記事など)。

目的:

  • 存在する言語版に対してのみhreflangリンクを生成
  • 404ページへのリンク生成を回避

例:ブログ記事

// どの言語にこの記事があるかを検出
const availableLocales: string[] = []
for (const checkLocale of LOCALES) {
  const post = await getPostBySlug(slug, checkLocale)
  if (post) availableLocales.push(checkLocale)
}
 
// 存在する言語に対してのみhreflangを生成
constructMetadata({
  // ...
  availableLocales: availableLocales.length > 0 ? availableLocales : undefined,
})

このパラメータを指定しない場合、すべての言語に対してhreflangリンクが生成されます。

9. useDefaultOgImage - デフォルトOG画像を使用するかどうか

useDefaultOgImageはデフォルトでtrueで、constructMetadataから自動的にマッチングされたOG Imageを使用することを意味します。

動的ページでコンテンツに基づいてOG Imageのスタイルとコンテンツを自動生成したい場合は、useDefaultOgImagefalseに設定する必要があります。同時に、動的ページと同じディレクトリにopengraph-image.tsxファイルを作成し、コンテンツに適応したOG Imageスタイルを開発します。

生成されるメタデータコンテンツ

constructMetadata()は以下のコンテンツを生成します:

1. 基本メタタグ

<title>About Us | Nexty.dev</title>
<meta name="description" content="Learn more about our mission..." />
<meta name="keywords" content="" />
<meta name="author" content="nexty.dev" />

2. カノニカルURL

<link rel="canonical" href="https://nexty.dev/about" />

3. 多言語hreflang

<link rel="alternate" hreflang="en-US" href="https://nexty.dev/about" />
<link rel="alternate" hreflang="zh-CN" href="https://nexty.dev/zh/about" />
<link rel="alternate" hreflang="ja-JP" href="https://nexty.dev/ja/about" />
<link rel="alternate" hreflang="x-default" href="https://nexty.dev/about" />

x-defaultの目的:

  • ユーザーの言語が一致しない場合に、どのバージョンを表示するかを検索エンジンに伝える
  • 通常はデフォルト言語(英語)を指す

4. Open Graphタグ

<meta property="og:type" content="website" />
<meta property="og:title" content="About Us | Nexty.dev" />
<meta property="og:description" content="Learn more about..." />
<meta property="og:url" content="https://nexty.dev/about" />
<meta property="og:site_name" content="Nexty.dev" />
<meta property="og:locale" content="en" />
<meta property="og:image" content="https://nexty.dev/og.png" />
<meta property="og:image:alt" content="About Us | Nexty.dev" />

5. Twitter Cardタグ

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="About Us | Nexty.dev" />
<meta name="twitter:description" content="Learn more about..." />
<meta name="twitter:site" content="https://nexty.dev/about" />
<meta name="twitter:image" content="https://nexty.dev/og.png" />
<meta name="twitter:creator" content="@judewei_dev" />

6. Robotsタグ

<!-- noIndex = false(デフォルト) -->
<meta name="robots" content="index, follow" />
 
<!-- noIndex = true -->
<meta name="robots" content="noindex, nofollow" />

constructMetadata()使用例

シナリオ1:ホームページ

app/[locale]/layout.tsx
export async function generateMetadata({
  params,
}: MetadataProps): Promise<Metadata> {
  const { locale } = await params
  const t = await getTranslations({ locale, namespace: 'Home' })
  
  return constructMetadata({
    title: t('title'), // 多言語ファイルから読み込み
    description: t('description'), // 多言語ファイルから読み込み
    locale: locale as Locale, // 現在の言語識別子を渡して、メタデータ内の言語情報と必要な多言語URLを自動処理
    path: '/', // ホームページパス
  })
}

シナリオ2:通常ページ

app/[locale]/(basic-layout)/about/page.tsx
export async function generateMetadata({
  params,
}: MetadataProps): Promise<Metadata> {
  const { locale } = await params
  const t = await getTranslations({ locale, namespace: 'About' })
  
  return constructMetadata({
    title: t('title'),
    description: t('description'),
    locale: locale as Locale,
    path: `/about`,
  })
}

シナリオ3:カスタムOG画像の使用

export async function generateMetadata({
  params,
}: MetadataProps): Promise<Metadata> {
  const { locale } = await params
  const t = await getTranslations({ locale, namespace: 'Pricing' })
  
  return constructMetadata({
    title: t('title'),
    description: t('description'),
    locale: locale as Locale,
    path: `/pricing`,
    images: ['images/og/pricing.png'],  // カスタムOG画像
  })
}

シナリオ4:動的OG画像

export async function generateMetadata({
  params,
}: MetadataProps): Promise<Metadata> {
  const { slug } = await params;
  const result = await getProductBySlug(slug);
 
  if (!result.success || !result.data) {
    return constructMetadata({
      title: "404",
      description: "Product not found",
      noIndex: true,
      path: `/product/${slug}`,
    });
  }
 
  const product = result.data;
  const fullPath = `/product/${slug}`;
 
  return constructMetadata({
    title: product.name,
    description: product.tagline,
    path: fullPath,
    useDefaultOgImage: false, // 🔑 重要:デフォルト画像を無効化
    // Next.jsは自動的に同じディレクトリのopengraph-image.tsxファイルを検索して適用します。
  })
}

シナリオ5:ブログ記事(多言語)

app/[locale]/(basic-layout)/blogs/[slug]/page.tsx
export async function generateMetadata({
  params,
}: MetadataProps): Promise<Metadata> {
  const { locale, slug } = await params
  const { post } = await getPostBySlug(slug, locale)
  
  if (!post) {
    return constructMetadata({
      title: "404",
      description: "Page not found",
      noIndex: true,  // 404ページはインデックスしない
      locale: locale as Locale,
      path: `/blogs/${slug}`,
    })
  }
  
  // どの言語にこの記事があるかを検出
  const availableLocales: string[] = []
  for (const checkLocale of LOCALES) {
    const { post: localePost } = await getPostBySlug(slug, checkLocale)
    if (localePost) {
      availableLocales.push(checkLocale)
    }
  }
  
  return constructMetadata({
    title: post.title,
    description: post.description,
    images: post.featuredImageUrl ? [post.featuredImageUrl] : [],
    locale: locale as Locale,
    path: `/blogs/${post.slug.replace(/^\//, '')}`,
    availableLocales: availableLocales.length > 0 ? availableLocales : undefined,
  })
}

シナリオ6:プレミアム/制限付きコンテンツ

export async function generateMetadata({
  params,
}: MetadataProps): Promise<Metadata> {
  const { locale } = await params
  
  return constructMetadata({
    title: "Premium Dashboard",
    description: "Access your premium features.",
    locale: locale as Locale,
    path: `/dashboard/premium`,
    noIndex: true,  // 検索エンジンにプレミアムコンテンツをインデックスさせない
  })
}

シナリオ7:カノニカルで複数のURLを統合

// シナリオ1:/free-trialと/pricingが同じコンテンツを指しているが、検索エンジンに/pricingを正規URLとして認識させたい
// シナリオ2:/blogsと/blogs?ref=docsが同じページを指しているため、検索エンジンに/blogsが正規URLであることを伝える必要がある
 
export async function generateMetadata({
  params,
}: MetadataProps): Promise<Metadata> {
  const { locale } = await params
  
  return constructMetadata({
    title: "Start Your Free Trial",
    description: "Try all features for free.",
    locale: locale as Locale,
    path: "/free-trial",
    canonicalUrl: "/pricing",  // 正規URLとして/pricingを指定
  })
}

シナリオ8:外部CDNでOG画像を使用

export async function generateMetadata({
  params,
}: MetadataProps): Promise<Metadata> {
  const { locale } = await params
  
  return constructMetadata({
    title: "Case Study",
    description: "How we helped Company X achieve results.",
    locale: locale as Locale,
    path: "/case-studies/company-x",
    images: [
      "https://cdn.yourdomain.com/case-studies/company-x-og.png"  // 絶対URL
    ],
  })
}