例えばQiitaだとTwitterでツイートするときに,タイトルとユーザー名からなる画像が生成される.

これをWordPressでも自前でやりたい

結果

いい感じにできました(主観)

画像生成サーバーの作成

Go言語でクエリパラメーターにテキストを入れると画像を返してくれるサーバーを作成した.
https://str-to-img.kajindowsxp.com/?text=例えばこんな感じ

全体のコードはここ
https://github.com/kajikentaro/str-to-img

画像を生成するコードは以下(抜粋).
あとはhttpと組み合わせ,デプロイするだけ

package services

import (
	"flag"
	"image"
	"image/color"
	"log"
	"os"

	"github.com/disintegration/imaging"
	"github.com/fogleman/gg"
	"github.com/pkg/errors"
	"golang.org/x/image/font"
	"golang.org/x/image/font/opentype"
)

type GenImageService struct{}

func NewGenImageService() *GenImageService {
	return &GenImageService{}
}

func (s *GenImageService) GenImage(text string) image.Image {
	flag.Parse()

	dc := gg.NewContext(1200, 630)

	dc.SetColor(color.RGBA{255, 255, 255, 255})
	dc.DrawRectangle(0, 0, float64(dc.Width()), float64(dc.Height()))
	dc.Fill()

	frame, err := gg.LoadImage("services/src/frame.png")
	if err != nil {
		panic(errors.Wrap(err, "load background image"))
	}
	frame = imaging.Fill(frame, dc.Width(), dc.Height(), imaging.Center, imaging.Lanczos)
	dc.DrawImage(frame, 0, 0)

	face, err := fetchPostScriptFontFace("services/font/NotoSansJP-Medium.otf")
	if err != nil {
		log.Fatalln(err)
	}
	dc.SetFontFace(face)
	dc.SetColor(color.Black)

	drawStringMultiLine(dc, []rune(text), 100.0)

	return dc.Image()
}

// PostScript アウトラインのフォント読み込み
func fetchPostScriptFontFace(fontfile string) (font.Face, error) {
	ftBinary, err := os.ReadFile(fontfile)
	if err != nil {
		return nil, err
	}

	ft, err := opentype.Parse(ftBinary)
	if err != nil {
		return nil, err
	}

	opt := opentype.FaceOptions{
		Size:    80.0,
		DPI:     72.0,
		Hinting: font.HintingNone,
	}

	face, _ := opentype.NewFace(ft, &opt)

	return face, nil
}

func drawStringMultiLine(dc *gg.Context, text []rune, margin float64) {
	width := float64(dc.Width())

	drewStrCnt := 0
	drewHeight := 40.0
	log.Println(string(text))
	for i := 0; i < len(text); i++ {
		candidateStr := text[drewStrCnt : i+1]
		candidateWidth, h := dc.MeasureString(string(candidateStr))
		if candidateWidth <= width-2*margin {
			// まだ右に余白がある場合
			continue
		}

		dc.DrawStringAnchored(string(text[drewStrCnt:i]), margin, drewHeight, 0, 1)
		drewStrCnt = i
		drewHeight += h
	}
	dc.DrawStringAnchored(string(text[drewStrCnt:]), margin, drewHeight, 0, 1)
}

テーマファイルを編集

管理画面にログインし,「外観」→「テーマファイルエディター」→「テーマのための関数(functions.php)」を順にクリック

以下の内容を追記する

//投稿・固定ページのSNSシェア画像の取得(シェア画像優先)
if ( !function_exists( 'get_singular_sns_share_image_url' ) ):
function get_singular_sns_share_image_url(){
  //NO IMAGE画像で初期化
  $sns_image_url = get_no_image_url();
  //本文を取得
  global $post;
  $content = '';
  if ( isset( $post->post_content ) ){
    $content = $post->post_content;
  }
  //投稿にイメージがあるか調べるための正規表現
  $searchPattern = '/<img.*?src=(["\'])(.+?)\1.*?>/i';
  if ($singular_sns_image_url = get_singular_sns_image_url()) {
    $sns_image_url = $singular_sns_image_url;
  } else if (has_post_thumbnail()){//投稿にサムネイルがある場合の処理
    $image_id = get_post_thumbnail_id();
    $image = wp_get_attachment_image_src( $image_id, 'full');
    $sns_image_url = $image[0];
  } else {
    $sns_image_url = 'https://str-to-img.kajindowsxp.com?text='.urlencode(get_the_title());
  }
  return apply_filters('get_singular_sns_share_image_url', $sns_image_url);
}
endif;

解説

そもそもCocoonには「親テーマ(cocoon-master)」と「子テーマ(cocoon-child)」が存在し,インストール時には親ファイルをインストールした上で,子テーマを有効化していた.

ソースを見ると wp-content/themes/cocoon-master/tmp/header-ogp.phpget_singular_sns_share_image_url()を読んでいる

if (is_singular()){//単一記事ページの場合
  if ($ogp_image = get_singular_sns_share_image_url()) {
    echo '<meta property="og:image" content="'.esc_url($ogp_image).'">';echo "\n";
  }
} else {//単一記事ページページ以外の場合(アーカイブページやホームなど)
  if (is_category() && !is_paged() && $eye_catch = get_the_category_eye_catch_url(get_query_var('cat'))) {
    $ogp_image = $eye_catch;
  } elseif (is_tag() && !is_paged() && $eye_catch = get_the_tag_eye_catch_url(get_queried_object_id())) {
    $ogp_image = $eye_catch;
  } elseif ( get_ogp_home_image_url() ) {
    $ogp_image = get_ogp_home_image_url();
  } else {
    if ( get_the_site_logo_url() ){//ヘッダーロゴがある場合はロゴを使用
      $ogp_image = get_the_site_logo_url();
    }
  }
  if ( !empty($ogp_image) ) {//使えそうな$ogp_imageがある場合
    echo '<meta property="og:image" content="'.esc_url($ogp_image).'">';echo "\n";
  }

wp-content/themes/cocoon-master/lib/utils.phpget_singular_sns_share_image_url()が定義されている.

//投稿・固定ページのSNSシェア画像の取得(シェア画像優先)
if ( !function_exists( 'get_singular_sns_share_image_url' ) ):
function get_singular_sns_share_image_url(){
  //NO IMAGE画像で初期化
  $sns_image_url = get_no_image_url();
  //本文を取得
  global $post;
  $content = '';
  if ( isset( $post->post_content ) ){
    $content = $post->post_content;
  }
  //投稿にイメージがあるか調べるための正規表現
  $searchPattern = '/<img.*?src=(["\'])(.+?)\1.*?>/i';
  if ($singular_sns_image_url = get_singular_sns_image_url()) {
    $sns_image_url = $singular_sns_image_url;
  } else if (has_post_thumbnail()){//投稿にサムネイルがある場合の処理
    $image_id = get_post_thumbnail_id();
    $image = wp_get_attachment_image_src( $image_id, 'full');
    $sns_image_url = $image[0];
  } else if ( preg_match( $searchPattern, $content, $image ) && !is_archive() && is_auto_post_thumbnail_enable()) {//投稿にアイキャッチは無いが画像がある場合の処理
    $sns_image_url = $image[2];
  } else if ( $no_image_url = get_no_image_url() ){//NO IMAGEが設定されている場合
    $sns_image_url = $no_image_url;
  } else if ( $ogp_home_image_url = get_ogp_home_image_url() ){//ホームイメージが設定されている場合
    $sns_image_url = $ogp_home_image_url;
  } else {
    $sns_image_url = NO_IMAGE_LARGE;
  }
  return apply_filters('get_singular_sns_share_image_url', $sns_image_url);
}
endif;

今回は「投稿にサムネイルがある場合」を除き,画像生成サーバーで作成した画像をOGPに設定するので先程のコードに修正した.

このutils.phpよりもfunctions.phpの方が先に読み込まれるので,functions.phpの関数が優先される形になる.