本記事のゴール

ブログのOGP画像メーカーアプリケーションを作る。

作業

暑い日が続いてますね、クーラーにもやられないよう体調に気をつけて楽しいプログラミングライフを送りたいなと思います...

さて、今日はブログの各記事に貼り付けるOGP画像メーカーを弊社のSpoon Tools!に作る過程をまとめます。参考になればと思います。

要求仕様

複雑なものではないので、箇条書きして整理します。

いま必要
将来的にいるかも

将来的に拡張も視野に設計します。

設計

Next.js 13で作成します。サンプルプロジェクトはこちらに用意しました。

npx create-next-app@latest demo-nextjs-ogpmakerで作成 あるいは、上記サンプルプロジェクトをgit cloneしてください。

エントリーポイントとなるpage.tsxにゴリゴリ実装します。

流れとしては以下。

  1. フロントエンドでフォームのあるページを作り
  2. バックエンドで画像加工処理を行い、
  3. ブラウザからダウンロードさせるだけ。

フィジビリティスタディ (技術検討)

以前にPHPプロジェクトでImageMagickを使って画像作成を行ったことを思い出したので 「ImageMagick Next.js」とかで検索すると以下の記事がヒットしました。

Cloud Functions で ImageMagick 使って OGP 画像を生成・表示する

なるほど、Cloudに任せちゃうのはアリだなと。他に、以下もヒット。

ImageMagickをやめて画像の処理はSaaSに頼ることにした こちらは有名なzenn.devの開発者catnoseさんのzenn内でのポストですね。やっぱり画像合成処理は自前でバックエンドを用意してImageMagickで頑張るより、外注するのがアイデアとしては筋が良さそう。

ただ、ポスト内で勧められているさくらインターネットさんのImage Fluxは利用料金がちょっと高い(と言っても月額550円ですが。。。)そのため個人に近い弊社では利用が難しいですね...。そもそもクラウドに投げるのは、利用用途が違うかも(ある程度スケールしたサービスでのトラフィックを考慮した外注処理かも)と思い始めてきました。

改めて先述のCloud Functionsで〜の記事を見返していると、Next.jsの公式ドキュメントへのリンクが。ImageResponse これ、いい。使おう。となりました。

実装作業

具体的に実装に入っていきます。

先に完成形をこちらにレポジトリとして用意します。

まずフロントエンドのページから。先述の通り、page.tsxに簡単なフォームがあるページを組んでいきます。

yarnで入れておきます。(tailwindcssも使うのですが、Next.js13からデフォルトで入るようになったので簡単ですね。)

src/app/page.tsx
"use client";

import { FieldValues, useForm } from 'react-hook-form';

type Result = {
  count: number
  calcedTotal: number
  calcedPlus: number
}

const Home = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
  });
  const baseImageUrl = "http://localhost:3000/api/og" // 開発環境以外で使用する場合はドメインを変更してください。

  const onSubmit = (param: FieldValues) => {

    const link = document.createElement('a')
    link.href = baseImageUrl + "?title=" + param.title + "&siteTitle=" + param.siteTitle
    link.download = 'ogp_image.png'
    link.target = '_blank'
    link.rel = 'noopener noreferrer'
    link.click()

  }

  return (

    <main className="m-10 p-5 bg-white">
      <form onSubmit={handleSubmit(onSubmit)}>
        <div className="py-6">
          <h1 className="leading-loose font-bold text-2xl">OGPメーカーのツール</h1>
        </div>

        <h2>ページタイトル</h2>
        <div className="py-2 w-full flex">
          <input {...register('title')} className="w-full px-6 py-4 border rounded-md text-xl" placeholder="ページタイトル" />
        </div>

        <h2>サイトタイトル</h2>

        <div className="py-2 w-full flex">
          <input {...register('siteTitle')} className="w-full px-6 py-4 border rounded-md text-xl" placeholder="サイトタイトル" />
        </div>

        <div className="w-full pt-12 pb-6">
          <button type="submit" className="w-full bg-blue-300 hover:bg-blue-200 text-gray-700 font-semibold px-4 py-4 rounded-md">
            作成
          </button>
        </div>

      </form>
    </main >


  );
};

export default Home

こちらの画面から呼ぶバックエンドのエンドポイントをsrc/app/api/og.tsxに用意します。

画像加工/合成に際しては、ImageMagickとかだと、ゴリゴリオプションで指定しなければいけないので、直感的ではなく、欲しい結果を得られるようになるまで試行錯誤が必要だと思います。その面、ImageResponseはhtml + cssで思いのままに改変できるので使いやすいですね!

src/app/api/og.tsx
import { ImageResponse } from 'next/server';

export const config = {
  runtime: "edge",
};


const font = fetch(
  "http://localhost:3000/fonts/NotoSansJP-Bold-Subset.ttf"
).then((res) => res.arrayBuffer());

export async function GET(req: Request) {

  try {
    const { searchParams } = new URL(req.url);
    const fontData = await font;

    const options = {
      title: searchParams.get("title")?.slice(0, 100) || "(タイトル未入力)",
      siteTitle: searchParams.get("siteTitle")?.slice(0, 100) || "(サイトタイトル未入力)",
    };

    return new ImageResponse(
      (
        <div
          style={{
            backgroundColor: '#FFF',
            width: '100%',
            height: '100%',
            backgroundSize: '100% 100%',
            display: 'flex',
            textAlign: 'center',
            alignItems: 'center',
            justifyContent: 'center',
            flexDirection: 'column',
            flexWrap: 'nowrap'
          }}
        >
          <div
            style={{
              color: '#000',
              padding: '0 40px',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              justifyItems: 'center',
              wordWrap: "break-word",
              fontSize: '4rem',
              fontWeight: 'bold',
              lineHeight: '5rem'
            }}
          >
            {options.title}
          </div>

          <div
            style={{
              width: '100%',
              display: 'flex',
              alignItems: 'flex-end',
              justifyContent: 'flex-end',
              position: 'absolute',
              fontSize: '2rem',
              fontWeight: 'bold',
              bottom: '40px',
              right: '40px',
              color: 'black',
            }}
          >
            {options.siteTitle}
          </div>
        </div>
      ),
      {
        width: 1200, // OGPの標準なサイズを指定
        height: 630,
        fonts: [
          {
            name: "NotoSansJP",
            data: fontData,
            style: "normal",
          },
        ],
      }
    );
  } catch (e: any) {
    console.log(`${e.message}`);
    return new Response(`Failed to generate the image`, {
      status: 500,
    });
  }
}

まとめ

Wordpressなどでは、設定したアイキャッチ画像がそのままOGPになるケースも多いですが、Next.jsで作る自前のブログなどでは OGPも自前で実装する必要があります。その場合、ポストを迅速に書き続けるためにこのようなソリューションが有効だと思いました。

この記事が何かのお役に立てれば幸いです。最後までお読みいただきありがとうございました!