【動的OGP】Next.js + TypeScript + Vercelデプロイで動的OGPを実現する

こんにちは、[Yui](https://twitter.com/yui_active)です。

引き続き週イチ発信をしていきます。
今週は初めてNext.jsで動的OGPに挑戦したので、そのことについて書きます。

何もスタイルを当ててないので、ものすごくシンプルなものですが、入力した文字をそのままOGPにするというものです。

[![Image from Gyazo](https://i.gyazo.com/29fe9f575952ef21e2564381e654fbdd.gif)](https://gyazo.com/29fe9f575952ef21e2564381e654fbdd)

過去の週イチ発信は以下

- [【React + Typescriptで顔認識】tensorflowを使って画像にマスクをかけるアプリを作った](https://qiita.com/Yui_active/items/b8062b9f89f72c6865ed)
- [【React + Typescript】ボタン一つでコンポーネントのscssをコピーできるサイトを作った](https://qiita.com/Yui_active/items/1288fed0dcc24c71e159)
- [ui-componentsに18個のコンポーネントを追加した](https://qiita.com/yuiko_ito/items/c1d0599e1e7da7fa42e0)
- [【Nuxt.js × Tailwind CSS】ボタン一つで有名絵画風の画像にできるサービスをリリースした！](https://qiita.com/yuikoito/items/126b0e2d36a707bfd36e)
- [【GASでLINE Bot作成】現在地の近くのおすすめのごはん屋さんを教えてくれるLINE Botを作った](https://qiita.com/yuikoito/items/c1aef147cef4458f9d2e)

## 雛形作成

まずは雛形作成ということで、Next.js + TypeScriptの雛形を作成します。

```
npx create-next-app --ts
```

そして今回canvasでOGPの画像を作成するので、canvasをインポートします。
※バージョンを2.6.1にしている理由は後ほど書きます。

```
yarn add canvas@2.6.1
```

ページ構成は以下にしておきます。

```
fonts/
pages/
　├ api/
　│　└ ogp.ts
　├ [id]/
　│　└ index.tsx
　└ index.tsx
```

## OGP用の画像を作成する

まずはOGP用の画像を作成する部分を組みます。
OGP用のサイズは[1200*630が良いらしい](http://ogimage.tsmallfield.com/#:~:text=OGP%E7%94%BB%E5%83%8F%E3%81%AE%E6%9C%80%E9%81%A9%E3%81%AA%E7%94%BB%E5%83%8F%E3%82%B5%E3%82%A4%E3%82%BA%E3%81%AF%EF%BC%9F&text=%E6%9C%80%E4%BD%8E%E3%81%A7%E3%82%82%20600%20x%20315,%E8%BF%91%E3%81%A5%E3%81%91%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%97%E3%81%BE%E3%81%97%E3%82%87%E3%81%86%E3%80%82)ので1200*630で作ります。

フォントは日本語は文字化けする可能性が高いらしいので、NotoSansJPをfontsフォルダ内において読み込むことにします。

```react:api/ogp.ts
const createOgp = async (
  req: NextApiRequest,
  res: NextApiResponse
): Promise<void> => {
  const { id } = req.query;
  const WIDTH = 1200 as const;
  const HEIGHT = 630 as const;
  const canvas = createCanvas(WIDTH, HEIGHT);
  const ctx = canvas.getContext("2d");
  registerFont(path.resolve("./fonts/NotoSansJP-Regular.otf"), {
    family: "Noto",
  });
  ctx.fillStyle = "#FFF";
  ctx.fillRect(0, 0, WIDTH, HEIGHT);
  ctx.font = "60px ipagp";
  ctx.fillStyle = "#000000";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  const text = "入力した文字は" + String(id) + "なのねん";
  ctx.fillText(text, WIDTH / 2, HEIGHT / 2);
  const buffer = canvas.toBuffer();
  res.writeHead(200, {
    "Content-Type": "image/png",
    "Content-Length": buffer.length,
  });
  res.end(buffer, "binary");
}

export default createOgp;
```

yarn devで開いて、http://localhost:3000/api/ogp?id=hoge を見たときに以下のページが表示されていれば成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670446/d671e532-0f40-619e-3ff0-07391f452add.png)

後で気が付いたんですが、入力した文字をそのままクエリにするならidじゃなくて**text**とかで受け取った方が良いですね。まあ細かい部分は置いておいて、とりあえずOGP用の画像ができました。
それではこれをOGPに設置していきます。

## metaを書き換える

それではmeta情報を書き換えます。
Next.jsでは``<Head>タグ``で簡単に切り替えることができるらしいので、その中で直接metaを書きます。
動的OGPなので、idに関してはSSRで処理をしてpropsで渡します。

```react:pages/[id]/index.tsx
import React from "react";
import { GetServerSidePropsContext, GetServerSidePropsResult } from "next";
import Head from "next/head";

type Props = {
  id: string;
};

export const getServerSideProps = async (
  context: GetServerSidePropsContext
): Promise<GetServerSidePropsResult<Props>> => {
  if (typeof context.params?.id === "string") {
    return {
      props: {
        id: context.params?.id,
      },
    };
  } else {
    return {
      notFound: true,
    };
  }
};

const Page = ({ id }: Props) => {
  const baseUrl = process.env.NEXT_PUBLIC_BASE_URL ?? "";
  return (
    <>
      <Head>
        <meta
          property="og:image"
          key="ogImage"
          content={`${baseUrl}/api/ogp?id=${id}`}
        />
        <meta
          name="twitter:card"
          key="twitterCard"
          content="summary_large_image"
        />
        <meta
          name="twitter:image"
          key="twitterImage"
          content={`${baseUrl}/api/ogp?id=${id}`}
        />
      </Head>
      <div>
        <h1>入力した文字: {id}</h1>
      </div>
    </>
  );
};
export default Page;
```

これで、http://localhost:3000/hoge にアクセスをすると、先程のhttp://localhost:3000/api/ogp?id=hoge の画像がOGP画像として設置されます。

あとは必要に応じてindex.tsxで受け取った入力値に応じて個別ページに飛ばすなりしたらOKです。
（以下hogeと入力したら/hogeに飛ばすだけのもの）

```react:index.tsx
import { useState } from "react";
import styles from "../styles/Home.module.css";
import Link from "next/link";

const Home = () => {
  const [text, setText] = useState<string>();
  const inputHandler = (e: { target: { value: string } }) => {
    setText(e.target.value);
  };
  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <input type="text" onChange={inputHandler} />
        {text?.trim() && (
          <Link href={`/${text}`}>
            <a>OGPを作成する</a>
          </Link>
        )}
      </main>
    </div>
  );
};
export default Home;
```

## Vercelにデプロイする

あとはVercelにデプロイをするだけなのですが、普通にデプロイするとcanvasが動きませんでした。
[issue](https://github.com/vercel/vercel/issues/3460#issuecomment-583072848)に書いてあるとおり、ビルド時にyum installする必要があるみたいです。

そこでビルド時のスクリプトを変えます。

```package.json
...
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "now-build": "yum install libuuid-devel libmount-devel && cp /lib64/{libuuid,libmount,libblkid}.so.1 node_modules/canvas/build/Release/ && yarn build",
    "start": "next start",
    "deploy": "now"
  },
...
```

vercelではnow-buildと書いて、``"deploy": "now"``で指定をするとbuildの代わりにnow-buildのコマンドを参照してビルドしてくれます。

これでビルドはyarn buildではなく、yarn now-buildで行われることになりました。

また、今回NEXT_PUBLIC_BASE_URLを環境変数で設定しているので、URLをNEXT_PUBLIC_BASE_URLで設定するのを忘れないようにします。

あとはビルドが通るのを待てば完了です。

ちなみに今回デプロイしたのがこちらです→https://ogp-sample.vercel.app/
必要に応じて、[シェアデバッガー](https://developers.facebook.com/tools/debug)で確認するなり、どこかに貼るなりしてみてください。

こんなふうに動的OGPができているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670446/036b48de-d5a9-3dca-b182-5811abd118ce.png)


## ちょっと詰まったところ

最初、canvasを最新バージョンで動かしていたとき、デプロイコマンドでcanvasをインストールするように書いてもどうやっても動きませんでした。

わけがわからなくて公式のissueを読み漁っていたら、[2.6.1までダウングレードすると動くというコメント](https://github.com/Automattic/node-canvas/issues/1779#issuecomment-819827079)があったので、試したら動きました。

どうやらcanvas2.7.0以降はnode14に対応してないぽい？です。


## 参考

https://zenn.dev/tiwu_dev/articles/68d58d4ab710af
https://nextjs.org/docs/basic-features/data-fetching
https://github.com/vercel/vercel/issues/3460
https://github.com/Automattic/node-canvas/issues/1779