microCMS + Astro|CMSとローカルの画像を使ったOGPイメージの自動生成方法
この記事では、microCMS + Astroで構成されているサイトで、OGPイメージをビルド時に自動生成する方法について説明します。
microCMSで持っている画像と、ソースコードと一緒に管理しているローカル画像をイメージに使用する方法も合わせて紹介します。
概要
Astroエンドポイント
Astroでは、どんな種類のデータでも提供できるカスタムエンドポイントを作成できます。これを利用して、今回はOGPイメージとしてPNGファイルを生成するエンドポイントを作成します。
下の様にsrc/pages/
フォルダ配下に、エンドポイント用のtsファイルを作成する形となります。ビルド時には.ts
が外れ、image.png
が生成されます。
src/pages/image.png.ts
Satoriとsharpを使った画像データ生成
Satoriは、JSX構文で記載された要素をインプットとしてSVGデータを生成するパッケージです。これを活用して、サイトをデザインするように、OGPイメージのデザインを行います。
生成したSVGデータは、sharpを使ってPNGデータに変換します。
CMSとローカルの画像の利用
microCMSで持っている画像と、ソースコードと一緒に管理しているローカル画像をイメージに使用することも可能です。
今回は例として、下の様に背景はローカル画像、作者アイコンはmicroCMSで持っている画像を使うパターンで説明します。
使用パッケージ
カッコ内は、記事作成で使用したバージョンです。
- astro (4.3.5)
- satori (0.10.13)
- sharp (0.33.2)
- @astrojs/react (3.0.10)
上のパッケージが未インストールであれば、リンク先に従ってインストールしてください。
実装方法
エンドポイントの作成
下のmicroCMS公式ブログの通りにAstroブログが作成されているとして、実装するポイントを説明します。
ブログ記事詳細ページ (src/pages/[blogId].astro
) に対応するOGPイメージを自動生成するようにします。
まずは、src/pages/og/
フォルダを作成し、エンドポイントとして[blogId].png.ts
を新規作成してください。
コードは下の通りです。ブログ記事詳細ページと同様、getStaticPaths
で動的なパスをAstroに渡します。
また、GET
では、パラメータとして取得したblogId
から記事の詳細情報を取得し、それを元に生成したOGPイメージデータを返却しています。
OGPイメージデータを作成するgetOgImage
は後で説明します。
import { getBlogs, getBlogDetail } from "../library/microcms";
import { getOgImage } from "../components/OgImage";
// 詳細記事ページの全パスを取得
export async function getStaticPaths() {
const response = await getBlogs({ fields: ["id"] });
return response.contents.map((content: any) => ({
params: {
blogId: content.id,
},
}));
}
export async function GET({ params }: APIContext) {
if (params.blogId === undefined) {
// blogIdがない場合は404を返す
return new Response(null, { status: 404 });
}
// 記事の詳細情報を取得
const blog = await getBlogDetail(blogId as string);
// OGPイメージを生成
return new Response(await getOgImage(blog));
}
APIスキーマの設定
作者アイコンと作者名をmicroCMSから取得するため、下の様にAPIスキーマの設定を行います。authorImg
の画像フィールドとauthorName
のテキストフィールドを追加してください。
次に、src/library/microcms.ts
の型定義Blog
に、authorImg
とauthorName
を追加してください。
//型定義
export type Blog = {
id: string;
createdAt: string;
updatedAt: string;
publishedAt: string;
revisedAt: string;
title: string;
content: string;
// 追加
authorImg: {
url: string;
height: number;
width: number;
};
// 追加
authorName: string;
};
コンポーネントの作成
OGPイメージデータを生成するコンポーネントを作成します。src/components/
フォルダを作り、OgImage.tsx
を新規作成してください。
コードの全体像は下の通りです。個々の処理を追って説明していきます。
import fs from "fs";
import satori from "satori";
import sharp from "sharp";
export async function getOgImage(blog: any) {
// フォントを読み込む
const fontRegularData = fs.readFileSync(
"src/styles/fonts/NotoSansJP-Regular.ttf",
);
const fontBoldData = fs.readFileSync(
"src/styles/fonts/NotoSansJP-Bold.ttf",
);
// ベースのローカル画像を読み込む
const baseImage = Uint8Array.from(
fs.readFileSync("src/assets/ogpImageBase.png"),
).buffer;
// authorImgの画像を読み込む
const authorImage = await fetch(blog.authorImg.url);
const authorImageBuffer = await authorImage.arrayBuffer();
const svg = await satori(
<div
style={{
width: "1200px",
height: "630px",
display: "flex",
position: "relative",
}}
>
<img
/* @ts-ignore リンターの警告がでる場合はignoreする */
src={baseImage}
width={1600}
height={630}
style={{
height: "100%",
width: "100%",
}}
/>
<div
style={{
width: "100%",
height: "100%",
position: "absolute",
padding: "30px",
fontFamily: "Noto Sans JP",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
}}
>
<div
style={{
flexGrow: 1,
display: "flex",
flexDirection: "column",
justifyContent: "center",
}}
>
<div
style={{
display: "flex",
margin: "18px 30px",
flexDirection: "column",
}}
>
<div
style={{
fontSize: "58px",
fontWeight: 700,
lineHeight: "1.1",
}}
>
{blog.title}
</div>
</div>
</div>
<div
style={{
marginLeft: "30px",
marginBottom: "34px",
display: "flex",
alignItems: "center",
}}
>
<img
/* @ts-ignore リンターの警告がでる場合はignoreする */
src={authorImageBuffer}
width={blog.authorImg.width}
height={blog.authorImg.height}
alt="author image"
style={{
height: "100px",
width: "100px",
objectFit: "cover",
borderRadius: "50%",
}}
/>
<div
style={{
fontSize: "40px",
marginLeft: "12px",
}}
>
{blog.authorName}
</div>
</div>
</div>
</div>,
{
width: 1200,
height: 630,
fonts: [
{
name: "Noto Sans JP",
data: fontRegularData,
weight: 400,
style: "normal",
},
{
name: "Noto Sans JP",
data: fontBoldData,
weight: 700,
style: "normal",
},
],
},
);
// SVGをPNGに変換
return await sharp(Buffer.from(svg)).png().toBuffer();
}
コンポーネント詳細|フォントファイルの読み込み
Satoriでテキストをレンダリングする場合は、フォントデータを渡す必要があります。下のコードでは例として、Google FontsのNoto Sans Japaneseをダウンロードして、src/styles/fonts/
フォルダに格納したファイルを読み込んでいます。
// フォントを読み込む
const fontRegularData = fs.readFileSync(
"src/styles/fonts/NotoSansJP-Regular.ttf",
);
const fontBoldData = fs.readFileSync(
"src/styles/fonts/NotoSansJP-Bold.ttf",
);
コンポーネント詳細|画像の読み込み
Satoriで画像を使うために、ローカルの画像とmicroCMSで登録するauthorの画像のデータを読み込んでいます。両方とも、Satoriで受け取れるArrayBufferオブジェクトになるようにしています。
対応している画像ファイル形式について、Satoriのドキュメントに記載はありませんでしたが、PNGは使用可能でした。こちらのissueにもある通り、現状WEBPは使用不可となっていました。
// ベースのローカル画像を読み込む
const baseImage = Uint8Array.from(
fs.readFileSync("src/assets/ogpImageBase.png"),
).buffer;
// authorの画像を読み込む
const authorImage = await fetch(blog.authorImg.url);
const authorImageBuffer = await authorImage.arrayBuffer();
コンポーネント詳細|SVGデータの生成
satori
の第一引数にOGPイメージのJSX要素、第二引数にサイズ、フォントデータを渡してSVGデータを生成しています。backgroundImage
にArrayBufferオブジェクトを渡すことができないため、imgタグを重ねるようになっています。
JSX要素に設定できるstyleには制限がありますので、詳細はこちらの公式ドキュメントを参照してください。
注意点として、複数の子ノードを持つdivタグにはdisplay: "flex"
またはdisplay: "none"
を明示する必要があります。
const svg = await satori(
<div
style={{
width: "1200px",
height: "630px",
display: "flex",
position: "relative",
}}
>
/*** デザイン部分は省略 ***/
</div>,
{
width: 1200,
height: 630,
fonts: [
{
name: "Noto Sans JP",
data: fontRegularData,
weight: 400,
style: "normal",
},
{
name: "Noto Sans JP",
data: fontBoldData,
weight: 700,
style: "normal",
},
],
},
);
コンポーネント詳細|SVGからPNGデータへの変換
最後に、sharpを使ってSVGデータをPNGデータに変換しています。
// SVGをPNGに変換
return await sharp(Buffer.from(svg)).png().toBuffer();
headerタグへのイメージパス指定
OGPイメージを生成するだけでなく、headerタグにイメージのパスを設定することも必要です。下のコードで今回作成したOGPイメージのパスが取得できます。
new URL(`/og/${blogId}.png`, Astro.url).toString()
イメージパスをheaderタグに設定する方法は、この記事の本題から外れますので割愛します。
以上で実装は完了です。
動作確認
下のコマンドでビルドを実行し、dist/og/
フォルダにOGPイメージが作成されることを確認してください。
npm run build