超便利！Next.js 13.4で OGP 画像を自動生成する方法

Webサイトを運用する上で必須のOGP画像をいちいち自分で生成するのは面倒なので、Next.jsでいい感じに作成することができたので解説します。

# OGP画像とは

> OGP画像の「OGP」とは「Open Graph Protcol」の略で、FacebookやTwitterなどのSNSでホームページのURLをシェアした際に表示される情報のことです。 ホームページのタイトルやURL、概要に加えて、OGP画像として設定されている画像を表示させる仕組みです。

引用元：https://onca.co.jp/column/design/p9242/#:~:text=OGP%E7%94%BB%E5%83%8F%E3%81%AE%E3%80%8COGP%E3%80%8D%E3%81%A8,%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%95%E3%81%9B%E3%82%8B%E4%BB%95%E7%B5%84%E3%81%BF%E3%81%A7%E3%81%99%E3%80%82

当ブログをSlackで共有すると、以下のように表示されます。

こちらの画像の部分が、OGP画像になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692555/b3c3ca1f-7267-1826-759b-fafa315a2c0a.png)


# Next.js13.4でOGP画像を生成する
以下のような画像を生成するとします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692555/9ff63522-5e33-4475-e763-a1f23d071cf7.png)


そして、そのコードは以下のようになります。

``` tsx
import { ImageResponse } from "next/server";

export const revalidate = "force-cache";
export const runtime = "nodejs";

export const alt = "OGP画像";
export const size = {
  width: 1200,
  height: 630,
};
export const contentType = "image/png";

export default async function Image() {
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 48,
          background: "white",
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          justifyContent: "center",
          width: "100%",
          height: "100%",
        }}
      >
        <div
          style={{ height: 40, backgroundColor: "#5AC8D8", width: "100%" }}
        />
        <h1
          style={{
            flex: 1,
            maxWidth: "80%",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
          }}
        >
          これはOGP画像です。
        </h1>
        <div
          style={{ height: 40, backgroundColor: "#5AC8D8", width: "100%" }}
        />
      </div>
    ),
    {
      ...size,
    }
  );
}
```

では、要点を確認していきましょう。

まずは前提として、`/app/**/opengraph-image.tsx`にファイルを配置することで生成することができます。また生成した画像は、ファイルパスでマッピングされているとおりに配置され、そのURLにアクセスすることで確認できます。

例：`/app/hoge/opengraph-image.tsx`→`/hoge/opengraph-image`

``` tsx
export const revalidate = "force-cache";
export const runtime = "nodejs";

export const alt = "OGP画像";
export const size = {
  width: 1200,
  height: 630,
};
export const contentType = "image/png";
```

`revalidate`や`runtime`はNext.jsの**Route Segment Config** （ https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config ）のオプションと同じものになります。

`alt`はimgタグなどにつけるalt属性と同じものになります。
このOGP画像を設定したページのHeadを見てみると、以下のように`og:image:alt`に設定されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692555/580b7937-c9a5-cdf2-1362-a474184de5bf.png)


`size`は画像のサイズになります。ここで設定した値の比率が維持されます。

`contentType`は画像の種類を設定できます。（https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types）

そして、この機能の主となるのは`ImageResponse`です。

``` tsx
new ImageResponse(
    (
      <div
        style={{
          fontSize: 48,
          background: "white",
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          justifyContent: "center",
          width: "100%",
          height: "100%",
        }}
      >
        <div
          style={{ height: 40, backgroundColor: "#5AC8D8", width: "100%" }}
        />
        <h1
          style={{
            flex: 1,
            maxWidth: "80%",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
          }}
        >
          これはOGP画像です。
        </h1>
        <div
          style={{ height: 40, backgroundColor: "#5AC8D8", width: "100%" }}
        />
      </div>
    ),
    {
      ...size,
    }
  );
```

`ImageResponse`は`next/server`からインポートするクラスで、第一引数が`element: ReactElement<any, string | JSXElementConstructor<any>>`で、例にもあるとおり表示したいレイアウトを入れます。ここでの注意点が`display: grid`は使用できないようです。

また第二引数は`options?: (ImageOptions & ResponseInit) | undefined)`で詳細には以下のようなものが入ります。

``` tsx
options: {
    width?: number = 1200
    height?: number = 630
    emoji?: 'twemoji' | 'blobmoji' | 'noto' | 'openmoji' = 'twemoji',
    fonts?: {
      name: string,
      data: ArrayBuffer,
      weight: number,
      style: 'normal' | 'italic'
    }[]
    debug?: boolean = false
 
    // Options that will be passed to the HTTP response
    status?: number = 200
    statusText?: string
    headers?: Record<string, string>
  },
)
```

参照：https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image

# OGP画像を動的に生成する

記事などのOGP画像のタイトルをAPIから取得したい場合は、以下のように記述できます。

``` tsx
import { ImageResponse } from "next/server";
import { getArticleDetail } from "src/app/articles/[id]/requests";

export const revalidate = "force-cache";
export const runtime = "nodejs";

export const alt = "Article Detail";
export const size = {
  width: 1200,
  height: 630,
};
export const contentType = "image/png";

export default async function Image({ params }: { params: { id: string } }) {
  const article = await getArticleDetail(params.id);

  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 48,
          background: "white",
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          justifyContent: "center",
          width: "100%",
          height: "100%",
        }}
      >
        <div
          style={{ height: 40, backgroundColor: "#5AC8D8", width: "100%" }}
        />
        <h1
          style={{
            flex: 1,
            maxWidth: "80%",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
          }}
        >
          {article.title}
        </h1>
        <div
          style={{ height: 40, backgroundColor: "#5AC8D8", width: "100%" }}
        />
      </div>
    ),
    {
      ...size,
    }
  );
}
```

ほぼそのままなのですが、取得したタイトルを以下のように生成することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692555/5a66c039-81cb-a0fe-cd5f-4a06f6d1ded7.png)
ちなみに、ファイル名を`twitter-image.tsx`にすることでTwitter用の画像を作成できます。

# 詰まったポイント

## Slackでの表示がうまくいかなかった

Slackで記事のリンクを共有した場合に、OGP画像がうまく表示されない現象が起きました。

調べたところ、`og:description`をmetaタグに設定すればうまくいくとのことだったのですが、それだけではうまくいきませんでした。そこで`og:url`も追加してみたところ、うまく表示させることができました。

## Twitter用のOGP画像がうまく表示されなかった

TwitterのOGP画像も、`twitter-image.tsx`がうまく生成されていることを確認した上で、twitterに記事のリンクを入りつけて確認してみてもうまく表示されませんでした。

これに関しても、`twitter:title`、`twitter:description`をmetaタグに追加することで、うまく表示されました。

ちなみに、デフォルトだと`twitter:card`が`summary`になっているので、`summary_large_image`に変えておくと、大きく表示されるようになるのでおすすめです。

# 補足

Next.jsでmetaタグを設定するには、以下のように記述することができます。

↓静的

``` tsx
export const metadata: Metadata = {
  title: '...',
  description: '...',
};
```

↓動的

``` tsx
export async function generateMetadata(
  { params, searchParams }: Props,
  parent?: ResolvingMetadata,
): Promise<Metadata> {
  // read route params
  const id = params.id;
 
  // fetch data
  const product = await fetch(`https://.../${id}`).then((res) => res.json());
 
  // optionally access and extend (rather than replace) parent metadata
  const previousImages = (await parent).openGraph?.images || [];
 
  return {
    title: product.title,
    openGraph: {
      images: ['/some-specific-page-image.jpg', ...previousImages],
    },
  };
}
```

今回の場合は以下のように設定してます。

```tsx
export const generateMetadata = async ({ params }: PageProps) => {
  const article = await getArticleDetail(params.id);

  return {
    title: `${article.title} - Coopello Blog`,
    description: article.description,
    openGraph: {
      title: article.title,
      description: article.description,
      url: `https://coopello-blog.vercel.app/articles/${params.id}`,
    },
    twitter: {
      title: article.title,
      card: "summary_large_image",
      description: article.description,
    },
  };
};
```

# 最後に
## ブログ始めました！
まだまだ記事は増えていくので、ぜひブックマークを！！

https://coopello-blog.vercel.app/

## 勉強会主催始めました！
渋谷を中心に開催していこうと思うので、情報交換、仲間探しにぜひ！

https://coopello.connpass.com/