coded Kazuya Imoto - Front-End Developer site.

Astro × microCMSで記事投稿と同時にサムネイル画像を自動生成する

公開日:2025年1月3日

はじめに

Astro × microCMSを使って、ブログ記事のサムネイル画像を自動生成する方法についてまとめました。ほぼ自分用の備忘録です。

実現したいこと

microCMSの管理画面から記事を投稿した際に、記事一覧でサムネイル画像が自動生成され表示されるようにする。

使用したバージョン

"astro": "^4.15.11"
"@astrojs/react": "^3.6.2"

必要なパッケージをインストール

以下のコマンドで必要なパッケージをインストールします。

npm i sharp fs-extra canvas

ベース画像の準備

次に、サムネイルの背景となる画像を作成します。この画像は、publicフォルダ内に配置してください。
例:public/img/baseArticleImage.png

記事画像を保存するディレクトリを作成

生成されたサムネイル画像をまとめて格納するディレクトリを作成します。
例:public/img/article

サムネイル画像生成コード

記事のタイトルを元に、サムネイル画像を生成するコードを作成します。コードは、以下のようにsrc/library/microcms/ディレクトリ内に記述します。

// 既存のコード
const client = createClient({
  ...
})

// ここから記事画像を生成するコード
// サムネイル生成関数の型定義
type GenerateThumbnailParams = {
  title: string;
  outputFilePath: string;
};

// 改行位置等が気になったりしたので、英単語と日本語を考慮して分割する関数
const splitTextByWidth = (
  text: string,
  maxWidth: number,
  font: string
): string[] => {
  const canvas = createCanvas(1, 1); // 仮のキャンバス作成
  const context = canvas.getContext("2d");
  context.font = font; // フォントの設定

  const lines: string[] = [];
  let currentLine = "";

  // 正規表現で単語と日本語文字を分割
  const tokens = text.match(/[\u3040-\u30FF\u4E00-\u9FAF]|[a-zA-Z]+|./g) || [];

  // 分割された各トークンを処理
  for (const token of tokens) {
    // 現在の行にトークンを追加した場合の仮の行を作成
    const testLine = currentLine ? `${currentLine}${token}` : token;
    // 仮の行の幅を計測
    const testLineWidth = context.measureText(testLine).width;

    // 仮の行が最大幅を超える場合
    if (testLineWidth > maxWidth) {
      // 現在の行が空なら、トークンをそのまま行として追加
      if (currentLine === "") {
        lines.push(token);
        currentLine = ""; // 次の行の処理に備えて初期化
      } else {
        // 現在の行を追加し、新しい行を開始
        lines.push(currentLine);
        currentLine = token;
      }
    } else {
      // 仮の行の幅が収まる場合、現在の行にトークンを追加
      currentLine = testLine;
    }
  }

  // 最後の行が空でなければ追加
  if (currentLine) {
    lines.push(currentLine);
  }

  return lines;
};

// サムネイルを生成する関数
async function generateThumbnail({
  title,
  outputFilePath,
}: GenerateThumbnailParams): Promise<void> {
  const templatePath = "./public/img/baseArticleImage.png"; // テンプレートの背景画像のパス

  try {
    // 出力先のディレクトリがなかった場合は作成する
    const outputDir = path.dirname(outputFilePath);
    await fsExtra.ensureDir(outputDir);

    // テキストが640pxに収まるように分割
    const font = "43px Noto Sans JP"; // 使用するフォントとサイズ(幅計算のため)
    const lines = splitTextByWidth(title, 640, font); // 分割された行

    // SVGでテキストを作成
    const textSvg = `
    <svg width="800" height="495" xmlns="http://www.w3.org/2000/svg">
      <style>
        .title {
          font-size: 43px;
          font-family: Noto Sans JP;
          font-weight: bold;
          fill: black;
        }
      </style>
      <rect width="800" height="495" fill="transparent" />
      ${(() => {
        const lineHeight = 60; // 行間
        const totalTextHeight = lines.length * lineHeight; // 全テキストの高さ
        const startY = (460 - totalTextHeight) / 2 + lineHeight; // SVG全体の中央に揃える
        return lines
          .map(
            (line, index) =>
              `<text x="60" y="${startY + index * lineHeight}" class="title">${line}</text>`
          )
          .join("\n");
      })()}
    </svg>
    `;
    const textBuffer = Buffer.from(textSvg); // SVGをバッファとして保持

    // 背景画像にSVGを合成
    await sharp(templatePath)
      .composite([{ input: textBuffer, blend: "over" }])
      .toFile(outputFilePath);

    console.log(`サムネイル生成成功: ${outputFilePath}`);
  } catch (error) {
    console.error("サムネイル生成中にエラー発生:", error);
  }
}

// サムネイルを自動で生成する関数
async function generateBlogThumbnails() {
  try {
    // microCMSクライアントの作成
    const client = createClient({
      serviceDomain: import.meta.env.MICROCMS_SERVICE_DOMAIN,
      apiKey: import.meta.env.MICROCMS_API_KEY,
    });

    // ブログ記事を取得
    const blogs = await client.get({
      endpoint: "blogs", // エンドポイント(例なので変更してください)
      queries: {
        limit: 100, // 取得する記事数(必要に応じて調整)
      },
    });

    // サムネイル保存先ディレクトリ
    const thumbnailDir = "./public/img/article";
    await fsExtra.ensureDir(thumbnailDir);

    // 各記事のサムネイルを生成
    for (const blog of blogs.contents) {
      // サムネイルのファイル名を記事IDから生成
      const thumbnailPath = path.join(thumbnailDir, `${blog.id}.webp`);

      // すでにサムネイルが存在する場合はスキップ(必要に応じてコメントアウト)
      if (await fsExtra.pathExists(thumbnailPath)) {
        console.log(`サムネイル already exists: ${blog.id}`);
        continue;
      }

      // サムネイル生成
      await generateThumbnail({
        title: blog.title,
        outputFilePath: thumbnailPath,
      });

      console.log(`サムネイル自動生成完了: ${blog.id}`);
    }
  } catch (error) {
    console.error("サムネイル自動生成中にエラー発生:", error);
  }
}

// サムネイル生成の実行
(async () => {
  try {
    await generateBlogThumbnails();
  } catch (error) {
    console.error("エラー:", error);
  }
})();

// ビルド時や定期的に実行する
export async function generateAllThumbnails() {
  await generateBlogThumbnails();
}

実装内容の全体の流れ

  1. microCMSからブログ記事のタイトルを取得する。
  2. タイトルを特定幅に収まるように分割する。
  3. 背景画像に分割したタイトルをSVG形式で配置する。
  4. microCMS上の全記事についてサムネイル画像を自動生成する。

結果

開発環境で記事一覧にアクセス or ビルド時に以下のように表示されるはずです。

参考

【microCMS × Astro】記事のサムネイルの自動生成