動的にメタ情報を設定し、SNS に表示するタイトルやサムネも動的に設定する方法

## 📎 はじめに

本記事では、Next.js の App Router で動的にメタ情報を設定し、SNS にリンクを貼った時にタイトルやサムネをいい感じに表示する方法をまとめます。

## 📎 やりたいこと

たとえば、X に YouTube のリンクを貼ると、以下のようにタイトルとサムネがいい感じに表示されます。これを自身のサービスでもやりたいと思います。

![](https://storage.googleapis.com/zenn-user-upload/035c5592f388-20250316.png)

## 📎 対象サービス

https://clipsearch.jp/

自身が運営するサービス、「ClipSearch」を対象に今回実践してみます。ClipSearch は、「YouTube の動画 URL から切り抜きやショート動画など関連動画を簡単に見つけるサービス」です。

詳細は以下の記事にまとめているので、ぜひご覧ください。

https://zenn.dev/yossyxp/articles/16e1a70c10ad84

## 📎 ディレクトリ構成

ClipSearch は Next.js の AppRouter で作っており、ディレクトリ構成は以下のようになっています。

```
/
└── src/
    ├── app/
    │   └── search/
    │       └── [videoId]/
    │           └── page.tsx
    ├── layout.tsx
    └── page.tsx
```

YouTube の URL 入力してその関連動画を表示するサービスなのでパスとして、`/search/${videoId}` としており、この videoId ごとにタイトルやサムネイルを変えることをゴールとします。

## 📎 実践

### 公式ドキュメント

メタ情報の設定について、公式ドキュメントを確認していきます。

https://nextjs.org/docs/app/api-reference/functions/generate-metadata

```ts
import type { Metadata } from 'next'
 
// either Static metadata
export const metadata: Metadata = {
  title: '...',
}
 
// or Dynamic metadata
export async function generateMetadata({ params }) {
  return {
    title: '...',
  }
}
```

`layout.tsx` や `page.tsx` に対して、静的なメタ情報は `metadata` を動的なものは `generateMetadata` を設定するようです。

今回は動的にしたいので、`generateMetadata` を使用するとよさそうですね。

### 実際のコード

以下を [videoId] 配下の `page.tsx` に記載することで title と description が個別に指定できました。

```ts
export async function generateMetadata({
  params,
}: {
  params: Promise<{ videoId: string }>;
}): Promise<Metadata> {
  const resolvedParams = await params;
  const { videoId } = resolvedParams;
  const data = await fetchVideoData(videoId);
  const videoData = data.items?.[0];

  if (!videoData) {
    return metadata;
  }

  const videoMetaData = {
    title: videoData.snippet.title,
    description: videoData.snippet.description
  };

  return videoMetaData;
}
```

※ `fetchVideoData` は videoId を元に YouTube の動画情報を取得するメソッドになっています。

### SNS にも表示したい

今回、SNS にもタイトルやサムネイルを表示したいので、 `videoMetaData` に `openGraph` と `twitter` を追加します。

```diff ts
const videoMetaData = {
  title: videoData.snippet.title,
  description: videoData.snippet.description,
+ openGraph: {
+   title: videoData.snippet.title,
+   description: videoData.snippet.description,
+   url: `https://clipsearch.jp/search/${videoId}`,
+   siteName: 'ClipSearch',
+   images: [
+     {
+       url: videoData.snippet.thumbnails.high.url,
+     },
+   ],
+ },
+ twitter: {
+   card: 'summary_large_image',
+   title: videoData.snippet.title,
+   description: videoData.snippet.description,
+   images: [
+     {
+       url: videoData.snippet.thumbnails.high.url,
+     },
+   ],
+ },
};
```

## 📎 全体のコード

最終的なコードは以下のようになりました。

```ts
export async function generateMetadata({
  params,
}: {
  params: Promise<{ videoId: string }>;
}): Promise<Metadata> {
  const resolvedParams = await params;
  const { videoId } = resolvedParams;
  const data = await fetchVideoData(videoId);
  const videoData = data.items?.[0];

  if (!videoData) {
    return metadata;
  }

  const videoMetaData = {
    title: videoData.snippet.title,
    description: videoData.snippet.description,
    openGraph: {
      title: videoData.snippet.title,
      description: videoData.snippet.description,
      url: `https://clipsearch.jp/search/${videoId}`,
      siteName: 'ClipSearch',
      images: [
        {
          url: videoData.snippet.thumbnails.high.url,
        },
      ],
    },
    twitter: {
      card: 'summary_large_image',
      title: videoData.snippet.title,
      description: videoData.snippet.description,
      images: [
        {
          url: videoData.snippet.thumbnails.high.url,
        },
      ],
    },
  };

  return videoMetaData;
}
```

そして、結果はこちらです！

![](https://storage.googleapis.com/zenn-user-upload/a8316a7a14aa-20250316.png)

サムネが目立つ表示となりました。

Qiita の記事でもリンクを貼るだけで以下のようになります。

https://clipsearch.jp/search/X1kkjAtW7gc

## 📎 さいごに

ここまで読んでいただき、ありがとうございました。
X でサービスの宣伝をする際に、リンクだけだと味気ないかつ目立たないなと思い、今回実践しました。
皆様の問題解決につながれば幸いです。