本記事のゴール
ブログのOGP画像メーカーアプリケーションを作る。
作業
暑い日が続いてますね、クーラーにもやられないよう体調に気をつけて楽しいプログラミングライフを送りたいなと思います...
さて、今日はブログの各記事に貼り付けるOGP画像メーカーを弊社のSpoon Tools!に作る過程をまとめます。参考になればと思います。
要求仕様
複雑なものではないので、箇条書きして整理します。
いま必要
- OGP画像タイトルをテキストで指定
- 背景画像を数パターンから選べる
- 1500x945ピクセルの画像が、作成完了後即時ダウンロードされる
将来的にいるかも
- 背景画像をアップロードできる。
- 複数の画像から選択できる。
将来的に拡張も視野に設計します。
設計
Next.js 13で作成します。サンプルプロジェクトはこちらに用意しました。
npx create-next-app@latest demo-nextjs-ogpmaker
で作成 あるいは、上記サンプルプロジェクトをgit clone
してください。
エントリーポイントとなるpage.tsx
にゴリゴリ実装します。
流れとしては以下。
- フロントエンドでフォームのあるページを作り
- バックエンドで画像加工処理を行い、
- ブラウザからダウンロードさせるだけ。
フィジビリティスタディ (技術検討)
以前に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
に簡単なフォームがあるページを組んでいきます。
- react-hook-form (フォーム管理)
をyarn
で入れておきます。(tailwindcssも使うのですが、Next.js13からデフォルトで入るようになったので簡単ですね。)
"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で思いのままに改変できるので使いやすいですね!
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も自前で実装する必要があります。その場合、ポストを迅速に書き続けるためにこのようなソリューションが有効だと思いました。
この記事が何かのお役に立てれば幸いです。最後までお読みいただきありがとうございました!