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

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

Cocoon 2.8.4でサポート

はじめに

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にプラグインの機能一覧を示します。

機能一覧
項目説明
背景色背景の色を設定します。
文字色文字の色を設定します。
枠色枠の色を設定します。
著者名

  • オフのとき、サイトアイコンとサイトのタイトルが表示します。

  • オンのとき、プロフィールアイコンと投稿の著者名を表示します。

画像を作成クリックされると、画像生成しプレビュー表示します。
画像を保存クリックすると、メディアに保存され、アイキャッチ画像に設定します。

実装手順

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

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

  • my-html-canvas
    • html-canvas.php
    • assets
      • css
        • admin.css
      • js
        • admin.js
  • ラベル
    PHPを追加

    以下のコードをhtml-canvas.phpに追加します。

    <?php
    /**
     * Plugin Name: My HTML Canvas
     * Description: 投稿タイトルからアイキャッチ画像を生成します。
     * Version: 1.0.0
     * Author: CHU-YA
     */
    
    if (!defined('ABSPATH')) {
      exit;
    }
    
    class HTML_Canvas {
      public function __construct() {
        add_action('add_meta_boxes', [$this, 'add_meta_box']);
        add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
        add_action('wp_ajax_html_canvas_save_image', [$this, 'ajax_save_image']);
      }
    
    
      /**
       * メタボックスを追加
       *
       * @return void
       */
      public function add_meta_box() {
        add_meta_box(
          'html_canvas',
          'アイキャッチ画像自動生成',
          [$this, 'render_html_canvas'],
          'post',
          'side'
        );
      }
    
    
      /**
       * メタボックスの入力フォームを出力
       *
       * @param WP_Post $post 現在編集中の投稿オブジェクト
       * @return void
       */
      public function render_html_canvas($post) {
        ?>
        <div class="html-canvas">
          <p><label>背景色<input type="color" id="html_canvas_bgcolor" value="#ffffff"></label></p>
          <p><label>文字色<input type="color" id="html_canvas_fontcolor" value="#333333"></label></p>
          <p><label>枠色<input type="color" id="html_canvas_framecolor" value="#a2d7dd"></label></p>
          <p><label><input type="checkbox" id="html_canvas_use_author">著者名</label></p>
          <p><button type="button" class="button html-canvas__generate">画像を作成</button></p>
          <p><img class="html-canvas__preview" src="" alt="" style="max-width:100%; display:none;"></p>
          <p><button type="button" class="button button-primary html-canvas__save">画像を保存</button></p>
        </div>
        <?php
      }
    
    
      /**
       * エディター用JS・CSSを追加
       *
       * @param string $hook 現在の管理画面フック名
       * @return void
       */
      public function enqueue_assets($hook) {
        // 投稿編集画面以外では読み込まない
        if (!in_array($hook, ['post.php', 'post-new.php'], true)) {
          return;
        }
    
        global $post;
    
        // 外部ライブラリhtml2canvasを追加
        wp_enqueue_script('html2canvas', 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js', [], null, true);
    
        // フロント用JS・CSSを追加
        wp_enqueue_script('html-canvas', plugin_dir_url(__FILE__) . 'assets/js/admin.js', ['jquery'], null, true);
        wp_enqueue_style('html-canvas', plugin_dir_url(__FILE__) . 'assets/css/admin.css');
    
        // Ajaxで使うパラメータなどをJS側に渡す
        $author_id   = get_post_field('post_author', $post->ID);
        $author_name = get_the_author_meta('display_name', $author_id);
        $avatar_url  = get_the_author_uploaded_avatar_url($author_id);
    
        if (function_exists('get_thumbnail_aspect_ratio')) {
          $ratio = get_thumbnail_aspect_ratio();
        } else {
          $ratio = 3 / 4;
        }
    
        wp_localize_script('html-canvas', 'htmlCanvasData', [
          'ajaxUrl'      => admin_url('admin-ajax.php'),
          'postId'       => $post->ID,
          'nonce'        => wp_create_nonce('html_canvas_action'),
          '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
       */
      public function ajax_save_image() {
        // nonceチェック
        check_ajax_referer('html_canvas_action', 'html_canvas_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("html-canvas-{$post_id}.png", null, $binary);
    
        if (!empty($upload['error'])) {
          wp_send_json_error(['message' => $upload['error']]);
        }
    
        // 添付ファイル情報を作成
        $attachment = [
          'post_mime_type' => 'image/png',
          'post_title'     => "HTML Canvas 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);
    
        // 更新情報をJSON形式で返却
        wp_send_json_success([
          'url'       => $upload['url'],
          'attach_id' => $attach_id,
        ]);
      }
    }
    
    
    new HTML_Canvas();
  • ラベル
    JavaScriptを追加

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

    jQuery(function ($) {
      const width  = 800;
      const height = width * htmlCanvasData.cardRatio;
    
      let currentImageData = '';
    
    
      /**
       * 現在の投稿タイトルを取得
       *
       * @return {string} 投稿タイトル(未入力の場合は空文字)
       */
      function getCurrentTitle() {
        // Gutenbergエディターのチェック
        if (typeof wp !== 'undefined' && wp.data && wp.data.select('core/editor')) {
          return wp.data.select('core/editor').getEditedPostAttribute('title') || '';
        }
    
        // クラシックエディターのチェック
        const $titleInput = jQuery('#title');
        if ($titleInput.length) {
          return $titleInput.val() || '';
        }
    
        return '';
      }
    
    
      /**
       * プレビュー用のHTMLテンプレート文字列を生成
       *
       * @param {string} fontColor 文字色
       * @param {string} frameColor 枠色
       * @param {string} titleText タイトル文字列
       * @return {string} 生成されたHTML文字列
       */
      function generateHTML(fontColor, frameColor, titleText) {
        const displayMode = $('#html_canvas_use_author').is(':checked');
    
        let siteIconUrl = htmlCanvasData.siteIconUrl;
        let siteName = htmlCanvasData.siteName;
    
        // 著者名表示モードの場合
        if (displayMode) {
          siteIconUrl = htmlCanvasData.authorAvatar;
          siteName = htmlCanvasData.authorName;
        }
    
        return `
          <div class="html-canvas__preview-container" style="border-color:${frameColor}; height: ${height}px; width:${width}px;">
            <div class="html-canvas__title" style="color:${fontColor};">${titleText}</div>
            <div class="html-canvas__site">
              <img src="${siteIconUrl}" alt="サイトアイコン" class="html-canvas__icon">
              <span class="html-canvas__site-name" style="color:${fontColor};">${siteName}</span>
            </div>
          </div>
        `;
      }
    
    
      /**
       * HTML要素をPNG画像に変換してプレビューを表示
       *
       * @param {string} htmlContent 描画対象のHTML文字列
       * @param {string} bgColor 背景色
       * @return {void}
       */
      function htmlToPng(htmlContent, bgColor) {
        const $container = $('<div>').html(htmlContent).appendTo('body');
        const element = $container.find('.html-canvas__preview-container')[0];
    
        html2canvas(element, {
          useCORS: true,
          backgroundColor: bgColor,
        }).then(function (canvas) {
          const img = canvas.toDataURL('image/png');
          currentImageData = img;
          $('.html-canvas__preview').attr('src', img).show();
        }).catch(function (error) {
          console.error('html2canvas Error: ', error);
          alert('画像の生成に失敗しました。');
        }).finally(function () {
          $container.remove();
        });
      }
    
    
      // 「画像を作成」ボタンクリックの場合
      $('.html-canvas__generate').on('click', function () {
        const title      = getCurrentTitle();
        const bgColor    = $('#html_canvas_bgcolor').val();
        const fontColor  = $('#html_canvas_fontcolor').val();
        const frameColor = $('#html_canvas_framecolor').val();
    
        const htmlContent = generateHTML(fontColor, frameColor, title);
        htmlToPng(htmlContent, bgColor);
      });
    
    
      // 「画像を保存」ボタンクリックの場合
      $('.html-canvas__save').on('click', function () {
        if (!currentImageData) {
          alert('画像を作成してください。');
          return;
        }
    
        $.post(htmlCanvasData.ajaxUrl, {
          action: 'html_canvas_save_image',
          html_canvas_nonce: htmlCanvasData.nonce,
          postId: htmlCanvasData.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を追加

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

    /* アイキャッチ画像自動生成メタボックス */
    .html-canvas__generate,
    .html-canvas__save {
      display: block;
      width: 100%;
    }
    
    .html-canvas__preview {
      display: none;
      width: 100%;
    }
    
    .html-canvas input[type="color"] {
      width: 100%;
    }
    
    /* アイキャッチ */
    .html-canvas__preview-container {
      border-style: solid;
      border-width: 30px;
      box-sizing: border-box;
      display: flex;
      flex-direction: column;
      padding: 40px;
    }
    
    
    /* タイトル */
    .html-canvas__title {
      align-content: center;
      flex: 1;
      font-size: 42px;
      font-weight: bold;
      line-height: 1.2;
      margin: 0;
      text-align: center;
    }
    
    /* サイト情報 */
    .html-canvas__site {
      align-items: center;
      display: flex;
      gap: 10px;
    }
    
    .html-canvas__icon {
      height: 50px;
      width: 50px;
    }
    
    .html-canvas__site-name {
      font-size: 30px;
    }

補足

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

さいごに

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

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