【Next.js】satoriを使ってJSXからOGP画像を動的に生成する
やりたいこと
SNSなどで共有された時にアイキャッチとして表示されることから、一種のSEO対策とも言えるOGP(Open Graph Protcol)画像。
QiitaやZennのようなサービスだとそのOGP画像が動的に生成されるが、当サイト(Next.js製のSPA)でもそれを実現したい。
実装方法
Metadata Files APIでの実装方法
Next.js 13.3から追加されたMetadata Files APIを利用すれば、任意のルート下にjsxファイルを作成しそこでImageResponse
を返すだけで、わざわざ画像へのパスを指定することもなく実現できる。
1. ファイルの作成
app ├── blog │ ├── [slug].tsx ├── page.tsx ├── opengraph-image.tsx
まずopengraph-image.tsx
を作成し、OGP画像を設定したいルート下に配置する。
2. 画像の生成
import { ImageResponse } from "next/server";
export const runtime = "edge";
export const size = {
width: 1200,
height: 630,
};
export const contentType = "image/png";
type Params = {
params: { slug: string };
};
export default function Image({ params: { slug } }: Params) {
const article = getArticleBySlug(slug);
return new ImageResponse(
(
<div
style={{
height: "100%",
width: "100%",
display: "flex",
backgroundImage: "linear-gradient(135deg, #7dc7f8 10%, #027cd9 100%)",
color: "#f3f3f3",
justifyContent: "center",
alignItems: "center",
padding: "0 2rem",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
padding: "3rem 4rem 2.5rem",
backgroundColor: "#181b29",
justifyContent: "space-between",
borderRadius: "10px",
width: "100%",
height: "90%",
}}
>
<p style={{ fontSize: 60, fontWeight: 700 }}>{article.title}</p>
<p style={{ fontSize: 40, fontWeight: 500 }}>Oteto Blog</p>
</div>
</div>
),
{ ...size }
);
}
あとは公式サイトの方法に沿って任意の画像をjsx記法で実装し、ImageResponse
を返すだけ。
ImageResponse
での画像生成は内部でsatoriを使用しているため、指定できるスタイルはsatoriのドキュメントから確認できる。
3. metaタグが出力される
<meta property="og:image:type" content="image/png" />
<meta
property="og:image"
content="http://localhost:3001/opengraph-image?623e08608f24ec4c"
/>
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
すると自動的にOGPのためのmeta
タグが出力される。
実際にhttp://localhost:3001/opengraph-image?xxx
にアクセスしてみると、記事タイトルが埋め込まれた画像が返されることを確認。
これで/blog/[slug]
に全て動的なOGP画像が設定された。
build前に生成する方法
OGP画像用のルートを用意するのではなく、画像として事前に生成したい場合。この方法だとGitで管理できるというメリットがある。
今回はbuild(static export)時に全記事のOGP画像を生成し/public/img/<特定の記事ディレクトリ>
下に配置する、という場合を想定して実装してみる。
1. satoriをインストール
npm i satori
npm i sharp
HTML・CSSからSVGを生成できるvercel製のライブラリsatoriとそれをPNGに変換するためのsharpをインストールする。
2. 画像を生成し書き出す
import satori from "satori";
import sharp from "sharp";
import fs from "fs";
export const writeOgpImage = async (articleTitle: string, path: string) => {
// ディレクトリが存在しなければ作成する
if (!fs.existsSync(path)) fs.mkdirSync(path);
const image = await generateOgpImage(articleTitle);
fs.writeFileSync(`${path}/ogp.png`, image);
};
const generateOgpImage = async (title: string) => {
const fontMedium = fs.readFileSync("public/font/NotoSansJP-Regular.ttf");
const fontBold = fs.readFileSync("public/font/NotoSansJP-Bold.ttf");
const svg = await satori(
<div
style={{
height: "100%",
width: "100%",
display: "flex",
backgroundImage: "linear-gradient(135deg, #7dc7f8 10%, #027cd9 100%)",
color: "#f3f3f3",
justifyContent: "center",
alignItems: "center",
padding: "0 2rem",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
padding: "3rem 4rem 2.5rem",
backgroundColor: "#181b29",
justifyContent: "space-between",
borderRadius: "10px",
width: "100%",
height: "90%",
}}
>
<p style={{ fontSize: 60, fontWeight: 700 }}>{title}</p>
<div style={{ display: "flex", alignItems: "center" }}>
<img
src="https://pote-chil.com/img/ot-icon.svg"
alt=""
width={110}
height={110}
style={{
padding: "1rem",
border: "1px solid #333545",
borderRadius: "100%",
}}
/>
<p style={{ marginLeft: "16px", fontSize: 40, fontWeight: 500 }}>
Oteto Blog
</p>
</div>
</div>
</div>,
{
width: 1200,
height: 630,
fonts: [
{
name: "Noto Sans JP",
data: fontMedium,
style: "normal",
weight: 500,
},
{
name: "Noto Sans JP",
data: fontBold,
style: "normal",
weight: 700,
},
],
}
);
return await sharp(Buffer.from(svg)).png().toBuffer();
};
今回は任意のフォントや画像を使用してみる。
- jsx記法で画像の要素を指定し、SVG画像を生成
readFileSync
で任意のフォントを読み込み適用する
sharp
でPNGに変換- 指定したパスに画像を書き出す
3. 全記事の画像を生成
先ほど実装したOGP画像生成を記事の数だけ行う必要がある。
export const generateStaticParams = async (): Promise<Params[]> => {
const allArticles = getArticles();
+ // OGP画像生成
+ for (const article of allArticles) {
+ await writeOgpImage(article.title, article.path);
+ }
return allArticles.map((article) => ({
category: article.category,
slug: article.slug,
}));
};
そこで当サイトの場合はgenerateStaticParams
の中で全記事のルートを生成するついでに画像を生成するようにした。
すると/public/img/<特定の記事ディレクトリ>
下に無事画像が生成された 🎉