投稿タイトルからアイキャッチ画像を自動生成する方法

Cocoonカスタマイズ集
この記事は約18分で読めます。
記事内に広告が含まれています。

Cocoon 2.8.4でサポート

プラグインの形式で実装します。

add_meta_boxsave_postアクションを使い投稿編集画面に独自パネル(メタボックス)を追加し設定値を保存する方法を学べます。

まあた、ライブラリ「html2canvas」でHTML+CSSを画像化し、アイキャッチ画像として保存・設定する方法を学べます。

はじめに

Cocoon 2.8.4から投稿・固定ページの投稿タイトルからアイキャッチ画像を作成する機能が追加されました。
それに伴いJIN:Rなどの他テーマでも同様の機能がサポートされるようになっています。

Cocoonの機能の課題

PHPで画像処理を行う際には、主に以下の2つのライブラリが使用されます。

  • GD(標準で利用可能)
  • ImageMagick(拡張モジュールで、サーバー設定によっては使用不可)

Cocoonは、PHPに標準で備わっているGDライブラリがそのまま利用できるため、特別な設定なしで扱えるという利点から採用されています。
しかしGDではテキストのサイズや位置の調整、レイアウトの確認が非常に手間であり、以下のような課題があります。

  • 要素の座標指定が必要で手間がかかる
  • レイアウト確認には画像を都度出力する必要がある

画像生成方法の比較

表1に画像生成方法ごとの特徴をまとめます。

画像生成方法
観点GD(PHP)SVG + canvas(JS)HTML+CSS + html2canvas(JS)
依存ライブラリ標準PHP拡張canvas API(JS)html2canvas(外部JS)
レイアウト確認のしやすさ度出力が必要ラウザ上で即時確認可DOMベースで即表示
レイアウト調整のしやすさ標指定で煩雑性である程度調整可能CSSで直感的に調整可能
テキスト・フォント制御ォント制限あり軟な表現が可能Webフォント・CSSスタイル対応
要素の追加変更ードが煩雑になりやすいSVG構造を保てば比較的容易HTML要素を追加するだけで済む
再利用性・保守性スタム関数が複雑化しやすいンプレート再利用可能HTMLテンプレート + CSS活用容易

目的

表1に示した利点を踏まえ、ライブラリ「html2canvas」を用いて、投稿タイトルからアイキャッチ画像を自動生成するプラグインを追加します。
使用テーマがCocoonである場合は、表1で指定した比率に従い画像を生成します。それ以外のテーマでは、縦横比3:4で画像が作成されます。
この記事では、この方法について説明します。

Cocoon設定
設定大項目項目設定値
画像サムネイル画像

完成イメージ

機能一覧

表2にプラグインの機能一覧を示します。

機能一覧
項目説明
背景色背景の色を設定します。
文字色文字の色を設定します。
枠色枠の色を設定します。
著者名オフのとき、サイトアイコンとサイトのタイトルが表示します。
オンのとき、プロフィールアイジョンと投稿の著者名を表示します。
画像を作成クリックされると、画像生成しプレビュー表示します。
画像を保存クリックすると、メディアに保存され、アイキャッチ画像に設定します。

実装手順

以下の手順で実装します。

  • html-canvas
    • featured-image-form.php
    • generator.js
    • style.css
  • ラベル
    PHPを追加

    以下のコードをfeatured-image-form.phpに追加します。

    <?php
    /*
    Plugin Name: アイキャッチ画像自動生成
    Description: 投稿タイトルからアイキャッチ画像を生成します。
    Version: 1.0
    Author: CHU-YA
    */
    
    if (!defined('ABSPATH')) exit;
    
    
    class AFI_Auto_Featured_Image_Generator {
    
      /**
       * コンストラクタ
       * メタボックス追加、スクリプト・スタイルの読み込み、Ajax処理のフックを設定する
       */
      public function __construct() {
        add_action('add_meta_boxes', [$this, 'add_meta_box']);
        add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
        add_action('wp_ajax_afi_save_image', [$this, 'ajax_save_image']);
      }
    
      /**
       * 投稿編集画面にメタボックスを追加するコールバック
       *
       * @return void
       */
      public function add_meta_box() {
        add_meta_box(
          'afi_meta_box',
          'アイキャッチ画像自動生成',
          [$this, 'render_meta_box'],
          'post',
          'side'
        );
      }
    
    
      /**
       * メタボックスのHTMLを出力するコールバック
       *
       * @param WP_Post $post 現在編集中の投稿オブジェクト
       * @return void
       */
      public function render_meta_box($post) {
        ?>
        <div id="afi_meta_box">
          <p><label>背景色<input type="color" id="afi_bgcolor" value="#ffffff"></label></p>
          <p><label>文字色<input type="color" id="afi_fontcolor" value="#333333"></label></p>
          <p><label>枠色<input type="color" id="afi_framecolor" value="#a2d7dd"></label></p>
          <p><label><input type="checkbox" id="afi_use_author">著者名</label></p>
          <p><button type="button" class="button" id="afi_generate">画像を作成</button></p>
          <p><img id="afi_preview" src="" style="max-width:100%; display:none;"></p>
          <p><button type="button" class="button button-primary" id="afi_save">画像を保存</button></p>
        </div>
        <?php
      }
    
    
      /**
       * 投稿編集画面で必要なスクリプト・スタイルを読み込む
       *
       * @param string $hook 現在の管理画面フック名
       * @return void
       */
      public function enqueue_assets($hook) {
        // 投稿編集画面以外では読み込まない
        if (!in_array($hook, ['post.php', 'post-new.php'])) return;
    
        global $post;
    
        // 外部ライブラリhtml2canvasの読み込み
        wp_enqueue_script(
          'html2canvas',
          'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js',
          [],
          null,
          true
        );
    
        // プラグイン独自のJS
        wp_enqueue_script(
          'afi_js',
          plugin_dir_url(__FILE__) . 'generator.js',
          ['jquery'],
          null,
          true
        );
    
        // プラグイン独自のCSS
        wp_enqueue_style(
          'afi_css',
          plugin_dir_url(__FILE__) . 'style.css'
        );
    
        // Ajaxで使うパラメータなどをJS側に渡す
        $author_id = get_post_field('post_author', $post->ID);
    
        $user = get_userdata($author_id);
    
        $author_name = get_the_author_meta('display_name', $author_id);
        $avatar_url = get_the_author_upladed_avatar_url($author_id);
        if (function_exists('get_thumbnail_aspect_ratio')) {
          $ratio = get_thumbnail_aspect_ratio();
        } else {
          $ratio = 3 / 4;
        }
    
        // スクリプト向けの変数を追加
        wp_localize_script('afi_js', 'afiData', [
          'ajaxUrl'      => admin_url('admin-ajax.php'),
          'postId'       => $post->ID,
          'nonce'        => wp_create_nonce('afi_nonce'),
          'cardRatio'    => $ratio,
          'siteIconUrl'  => get_site_icon_url(64),
          'siteName'     => get_bloginfo('name'),
          'authorName'   => esc_js($author_name),
          'authorAvatar' => esc_url($avatar_url),
        ]);
      }
    
    
      /**
       * Ajaxリクエストを受けて生成画像を保存し、投稿のアイキャッチ画像に設定する。
       *
       * @return void JSON形式で成功/失敗を返す。
       */
      public function ajax_save_image() {
        // nonceチェック
        check_ajax_referer('afi_nonce', 'nonce');
    
        $post_id = intval($_POST['postId']);
        $data = $_POST['imageData'] ?? '';
    
        // 画像データ形式チェック(data URL)
        if (!preg_match('/^data:image\/png;base64,/', $data)) {
          wp_send_json_error(['message' => '無効な画像データ']);
        }
    
        // base64デコード
        $binary = base64_decode(str_replace('data:image/png;base64,', '', $data));
    
        // ファイルをアップロードディレクトリに保存
        $upload = wp_upload_bits("afi-{$post_id}.png", null, $binary);
    
        if (!empty($upload['error'])) {
          wp_send_json_error(['message' => $upload['error']]);
        }
    
        // 添付ファイル情報を作成
        $attachment = [
          'post_mime_type' => 'image/png',
          'post_title'     => "AFI Image {$post_id}",
          'post_status'    => 'inherit',
        ];
    
        // 添付ファイルとして登録
        $attach_id = wp_insert_attachment($attachment, $upload['file'], $post_id);
    
        if (is_wp_error($attach_id)) {
          wp_send_json_error([
            'message' => '画像の保存に失敗しました',
            'error_details' => $attach_id->get_error_message()
          ]);
        }
    
        // 画像メタデータ生成と更新
        require_once ABSPATH . 'wp-admin/includes/image.php';
        $meta = wp_generate_attachment_metadata($attach_id, $upload['file']);
        wp_update_attachment_metadata($attach_id, $meta);
    
        // 投稿のアイキャッチ画像に設定
        set_post_thumbnail($post_id, $attach_id);
    
        // 成功レスポンス送信
        wp_send_json_success([
          'url' => $upload['url'],
          'attach_id' => $attach_id
        ]);
      }
    }
    
    new AFI_Auto_Featured_Image_Generator();
  • ラベル
    JavaScriptを追加

    以下のコードをgenerator.jsに追加します。

    jQuery(function($) {
      const width  = 800;
      const height = width * afiData.cardRatio;
    
      let currentImageData = '';
    
    
      // タイトルを取得
      function getCurrentTitle() {
        if (typeof wp !== 'undefined' && wp.data) {
          return wp.data.select('core/editor').getEditedPostAttribute('title') || '';
        }
        return $('#title').val() || '';
      }
    
      // プレビュー用のHTMLテンプレート生成
      function generateHTML(fontColor, frameColor, titleText) {
        const displayMode = $('#afi_use_author').is(':checked');
    
        let siteIconUrl = afiData.siteIconUrl;
        let siteName = afiData.siteName;
    
        // 著者名の場合
        if (displayMode) {
          siteIconUrl = afiData.authorAvatar;
          siteName = afiData.authorName;
        }
    
        return `
          <div id="afi_preview_container" style="border-color:${frameColor}; height: ${height}px; width:${width}px;">
            <div id="afi_title" style="color:${fontColor};">${titleText}</div>
            <div id="afi_site">
              <img src="${siteIconUrl}" alt="サイトアイコン" id="afi_icon">
              <span id="afi_site_name" style="color:${fontColor};">${siteName}</span>
            </div>
          </div>
        `;
      }
    
      // HTMLをPNG画像に変換しimgに表示
      function htmlToPng(htmlContent, bgColor) {
        const $container = $('<div>').html(htmlContent).appendTo('body');
        const element = $container.find('#afi_preview_container')[0];
    
        html2canvas(element, {
          useCORS: true,
          backgroundColor: bgColor
        }).then(function(canvas) {
          const img = canvas.toDataURL("image/png");
          currentImageData = img;
          $('#afi_preview').attr('src', img).show();
        }).catch(function(error) {
          console.error('html2canvas Error: ', error);
          alert('画像の生成に失敗しました。');
        }).finally(function() {
          $container.remove();
        });
      }
    
    
      // 画像を作成ボタン
      $('#afi_generate').on('click', function () {
        const title = getCurrentTitle();
        const bgColor   = $('#afi_bgcolor').val();
        const fontColor = $('#afi_fontcolor').val();
        const frameColor= $('#afi_framecolor').val();
    
        const htmlContent = generateHTML(fontColor, frameColor, title);
        htmlToPng(htmlContent, bgColor);
      });
    
      // 画像を保存ボタン
      $('#afi_save').on('click', function () {
        if (!currentImageData) {
          alert('画像を作成してください。');
          return;
        }
    
        $.post(afiData.ajaxUrl, {
          action: 'afi_save_image',
          nonce: afiData.nonce,
          postId: afiData.postId,
          imageData: currentImageData
        }, function(res) {
          if (res.success) {
            // Gutenbergエディターの場合
            if (typeof wp !== 'undefined' && wp.data && wp.data.dispatch) {
              wp.data.dispatch('core/editor').editPost({
                featured_media: res.data.attach_id
              });
            } else {
              $('#_thumbnail_id').val(res.data.attach_id);
            }
          } else {
            alert('保存に失敗しました: ' + (res.data?.message || 'エラー'));
          }
        });
      });
    });

    クラシックエディターでは、画像を保存してもアイキャッチ画像はすぐに表示されません。
    投稿を更新すると反映されます。

  • ラベル
    CSSを追加

    以下のコードをstyle.cssに追加します。

    /* アイキャッチ画像自動生成メタボックス */
    #afi_generate,
    #afi_save {
      display: block;
      width: 100%;
    }
    
    #afi_preview {
      display: none;
      width: 100%;
    }
    
    input[type="color"] {
      width: 100%;
    }
    
    
    /* アイキャッチ */
    #afi_preview_container {
      border-style: solid;
      border-width: 30px;
      box-sizing: border-box;
      display: flex;
      flex-direction: column;
      padding: 40px;
    }
    
    /* タイトル */
    #afi_title {
      align-content: center;
      flex: 1;
      font-size: 42px;
      font-weight: bold;
      line-height: 1.2;
      margin: 0;
      text-align: center;
    }
    
    /* サイト情報 */
    #afi_site {
      align-items: center;
      display: flex;
      gap: 10px;
    }
    
    #afi_icon {
      height: 50px;
      width: 50px;
    }
    
    #afi_site_name {
      font-size: 30px;
    }

補足

CSSでbackground-color: transparent;を指定しても、html2canvasでは背景が白く描画される場合があります。
そのため、背景を透明にしたい場合は、html2canvasのオプションbackgroundColornullまたはrgba(0, 0, 0, 0)のようにアルファ値が0の色を明示的に指定します。

参考

さいごに

今回紹介した方法を使えば、投稿タイトルからアイキャッチ画像を生成・保存できます。
Cocoon標準のGD方式と違い、HTMLとCSSで自由にデザインでき、即時プレビューでき、より柔軟なレイアウトを求める方におすすめです。

タイトルとURLをコピーしました