_                 _    
                     | |               | |   
  ___  ___ _ __ _ __ | |__   ___   ___ | | __
 / __|/ __| '__| '_ \| '_ \ / _ \ / _ \| |/ /
 \__ \ (__| |  | |_) | |_) | (_) | (_) |   < 
 |___/\___|_|  | .__/|_.__/ \___/ \___/|_|\_\
               | |                           
               |_|                           
 

honoのブログにogp画像を自動生成して表示する

Published on: 2024/05/25

honoのブログにOGP画像を追加する方法を検討します。

検討案

  • mdxのフロントマターで対応する
  • satoriを使ってogp画像を自動生成する

mdxのフロントマターで対応する

MDXファイルの先頭の部分を「フロントマター(front matter)」と呼ばれています。

---
title: "タイトル"
description: "ブログの説明文"
date: "2024/05/23"
published: true
---

フロントマターは三つのダッシュ---で囲まれており、YAML形式で記述されます。 ここにogpのimage_urlを記録するフィールドを追加し、build時にogp画像の設定を行えば良さそうです。

satoriを使ってogp画像を自動生成する

satoriはNext.jsなどで有名なVercel社が開発したHTML/CSSをsvgに変換するライブラリです。 このライブラリとresvgなどのSVGtoPNG変換系ライブラリを組み合わせることで自動でOGP画像を生成することができます。

先行例としてHonoXでsatoriを使ってOGイメージもSSGする や、kvnang/workers-ogなどがあります。

今回は、すでにhonoxでblogを作成しているため、Pages Functionsを使ってOGP画像の自動生成を実装します。 Pages FunctionsでのOGP自動生成はkvnang/workers-ogを使うと簡単ですが、このライブラリは日本語には対応してません。 そのため、このライブラリを改造して日本語にも対応したOGP自動生成を実現したいと思います。

ディレクトリ構成

完成したディレクトリ構成は以下のようになります。 lib/workers-ogは、kvnang/workers-ogのpackage/workers-og/srcのファイルをコピーしています。

functions
├── api
└── lib
    ├── vendors
    └── workers-og

日本語フォント対応

workers-og/og.tsファイル内でGoogleフォントを指定して読み込んでいる箇所があります。 そこをNoto Sans JPを読み込むように変更します。

  const svg = await satori(reactElement, {
    ...widthHeight,
    fonts: !!options?.fonts?.length
      ? options.fonts
      : [
          {
            name: "Noto Sans JP",
            data: await loadGoogleFont({ family: "Noto Sans JP", weight: 600 }),
            weight: 500,
            style: "normal",
          },
        ],
  });

エンドポイントの対応

OGPの画像はGETリクエストでクエリパラメータで受け取った文字列を描画し、PNGファイルを返却するAPIとして実装します。 APIのエンドポイントを記述するファイルとして、functions/api/[[route]].tsファイルを作成します。

import { Hono } from "hono";
import { handle } from "hono/cloudflare-pages";
import { createOGPImage } from "./createOGPImage";

export const runtime = "edge";
const app = new Hono().basePath("/api");

app.get("/ogimg", createOGPImage);

export const GET = app.fetch;
export const POST = app.fetch;
export const onRequest = handle(app);

APIの作成は、Honoを利用しています。 Honoは、シンプルで早いWebフレームワークです。 書き方はExpressによく似ており、Expressを利用したことがあるなら、すぐにキャッチアップできると思われます。

OGP自動生成処理の作成

functions/api/createOGPImage.tsファイルを作成します。

import { ImageResponse } from "../lib/workers-og";

export const createOGPImage = async (c) => {
  const title = c.req.query("title") || "Undefined";

  const html = `
      <div style="display: flex; justify-content: center; align-items: center; width: 1200px; height: 630px; background-color: #fff; font-family: "Noto Sans JP"">
        <h1 style="font-size: 48px; color: #333; padding: 0 28px 0; display: flex;">${decodeURIComponent(
          title
        )}</h1>
        <div style="display: flex; justify-content: center; align-items: center; font-size: 36px; color: #333; position: absolute; bottom: 16px;">
          My Blog
        </div>
      </div>
   `;

  return new ImageResponse(html, {
    width: 1200,
    height: 630,
  });
};

ImageResponseは前述したkvnang/workers-ogを流用しNoto Sans JPを利用するようにしたライブラリです。 このライブラリは、HTML文字列、width、heightを受け取ると、PNG画像をresponseで返却します。 responseの処理はライブラリ内で行いますので、c.renderなどの処理を呼び出す必要はありません。

API呼び出し側のコード

API呼び出し側では、metaタグでog:imageのURLを動的に生成します。

export default jsxRenderer(({ children, title, ogImage }) => {
  const hostUrl = import.meta.env.PROD
    ? "https://example.com/"
    : "http://localhost:5173/";
  const ogImgTitle = title ? encodeURIComponent(title) : "";
  const ogTitle = title
    ? title + " | My Blog"
    : "My Blog";
  ogImage = ogImage ? ogImage : hostUrl + "api/ogimg?title=" + ogImgTitle;
  return (
    <html lang="jp">
      <head>
        {import.meta.env.PROD ? <GoogleAnalytics /> : null}
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta property="og:title" content={ogTitle} />
        <meta property="og:image" content={ogImage} />
	// 以下、省略
  )});

titleはencodeURIComponent関数でエスケープしないと、日本語をクエリパラメータで指定したときに、エラーが発生してOGP画像が生成されません。

resvg-wasmのバージョンについて

2024/05/26時点でresvg-wasmの最新バージョンは2.6.2です。 こちらのバージョンを利用すると、Cloudflare Workersの1MB制限(無料プラン)にwasmファイルのサイズが触れてしまいます。

そのため、resvg-wasmのバージョンを2.4.0固定にしないといけませんでした。

まとめ

このブログの内容で、OGPの自動生成を実現することができました。 冒頭のフロントマターも合わせて対応するとOGP画像の設定の自由度が高まるため、両方対応してしまうのが良いかと思われます。