2026年8月16日

2026年8月16日

WordPressでCanvas APIを使って記事ごとのOGP画像を生成する実装方法

Canvas API で OGP 画像を作るときは、訪問者のブラウザで描くのではなく、投稿の編集画面で描いて PNG としてメディアライブラリに保存し、その URL を og:image に出力する構成にします。SNS のクローラーは JavaScript を実行しないため、表示のたびにブラウザで描く方法では SNS に画像が届かないからです。この記事では、その前提を確認したうえで、編集画面に生成ボタンを置く小さなプラグインの実装(PHP と JavaScript)、og:image の出力、フォントや権限まわりのつまずきやすい点を説明します。

前提: SNS のクローラーは JavaScript を実行しない

Facebook、X、Slack、LINE などは、URL が共有されると専用のクローラーでページの HTML を取得し、og:image などのメタタグに書かれた画像を取りに行きます。このとき、多くのクローラーはページの JavaScript を実行しません。訪問者のブラウザで Canvas に描いても、その画像は訪問者の画面にあるだけで、クローラーからは見えません。

そこで、Canvas で描く作業は投稿を書く人の画面(管理画面)で行い、できた画像をサーバーに保存します。こうすると、OGP 画像は普通の画像ファイルとして公開され、どのクローラーからも取得できます。

全体の流れは次のとおりです。

  1. 投稿の編集画面に、Canvas と「タイトルから作成」ボタンを置いたメタボックスを表示する
  2. ボタンを押すと、JavaScript が Canvas にタイトルを描き、PNG に変換する
  3. PNG を REST API でメディアライブラリにアップロードする
  4. 添付ファイルの ID を投稿のカスタムフィールドに保存する
  5. 公開ページの <head> に、その画像の URL を og:image として出力する

画像の仕様を決める

OGP 画像は 1200×630px(横長、およそ 1.91:1)がよく使われ、Facebook のドキュメントでもこの程度の大きさが推奨されています。X の大きな画像のカードや他のサービスでは表示時に縦横が少し切り取られることがあるため、文字は中央寄りに配置し、端に重要な情報を置かないようにします。

形式は、文字がくっきり残る PNG が向いています。各サービスのファイルサイズの上限や推奨値は変わることがあるため、公開前に各サービスの開発者向けドキュメントで確認してください(2026年10月現在)。

PHP 側: カスタムフィールド・メタボックス・スクリプトの読み込み

以下は、ブロックエディター(投稿タイプ「投稿」)を対象にした最小構成のプラグインです。wp-content/plugins/wpdr-canvas-ogp/ フォルダーに PHP ファイルと後述の ogp.js を置きます。テーマの functions.php に書くとテーマを変えたときに機能が消えるので、プラグインにしておくのが扱いやすい方法です。まずステージングで試してください。

<?php
/**
 * Plugin Name: WPDR Canvas OGP
 * Description: 投稿の編集画面で Canvas を使って OGP 画像を作成する。
 * Version: 1.0.0
 */

defined( 'ABSPATH' ) || exit;

const WPDR_OGP_META = '_wpdr_ogp_image_id';

// 添付ファイル ID を保存するカスタムフィールドを REST API から編集できるように登録する。
add_action( 'init', 'wpdr_ogp_register_meta' );
function wpdr_ogp_register_meta() {
	register_post_meta(
		'post',
		WPDR_OGP_META,
		array(
			'type'              => 'integer',
			'single'            => true,
			'default'           => 0,
			'show_in_rest'      => true,
			'sanitize_callback' => 'absint',
			'auth_callback'     => function ( $allowed, $meta_key, $post_id ) {
				return current_user_can( 'edit_post', $post_id );
			},
		)
	);
}

// メディアをアップロードできるユーザーにだけメタボックスを表示する。
add_action( 'add_meta_boxes_post', 'wpdr_ogp_add_meta_box' );
function wpdr_ogp_add_meta_box() {
	if ( current_user_can( 'upload_files' ) ) {
		add_meta_box( 'wpdr-ogp', 'OGP画像の作成', 'wpdr_ogp_render_meta_box', 'post', 'side' );
	}
}

function wpdr_ogp_render_meta_box( $post ) {
	?>
	<canvas id="wpdr-ogp-canvas" width="1200" height="630" style="width:100%;height:auto;border:1px solid #dcdcde;"></canvas>
	<p><button type="button" class="button" id="wpdr-ogp-generate">タイトルから作成</button></p>
	<p id="wpdr-ogp-status" aria-live="polite"></p>
	<?php
}

// ブロックエディターでだけスクリプトを読み込む。
add_action( 'enqueue_block_editor_assets', 'wpdr_ogp_enqueue' );
function wpdr_ogp_enqueue() {
	$screen = get_current_screen();
	if ( ! $screen || 'post' !== $screen->post_type || ! current_user_can( 'upload_files' ) ) {
		return;
	}
	wp_enqueue_script(
		'wpdr-ogp',
		plugins_url( 'ogp.js', __FILE__ ),
		array( 'wp-api-fetch', 'wp-data', 'wp-editor' ),
		'1.0.0',
		true
	);
	$config = array(
		'siteName' => wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ),
		'metaKey'  => WPDR_OGP_META,
	);
	wp_add_inline_script( 'wpdr-ogp', 'var wpdrOgp = ' . wp_json_encode( $config ) . ';', 'before' );
}

ポイントを補足します。

  • カスタムフィールド名を _ で始めると、管理画面の「カスタムフィールド」欄には表示されない保護されたフィールドになります。REST API から書き込めるように、auth_callback で「その投稿を編集できるユーザー」に限って許可しています。
  • show_in_rest で REST API に公開するカスタムフィールドは、投稿タイプが custom-fields に対応している必要があります。標準の「投稿」は対応済みですが、カスタム投稿タイプで使う場合は supports に含めてください。
  • アップロードには upload_files 権限が必要です。標準の権限グループでは投稿者以上が持っており、寄稿者は持っていません。

JavaScript 側: 描画とアップロード

プラグインのフォルダーに ogp.js を置きます。wp.apiFetch は、管理画面では WordPress が nonce を自動で付けるため、ログイン中のユーザーの権限で REST API を呼べます。

( function () {
	const W = 1200;
	const H = 630;
	const FONT = '"Hiragino Sans", "Yu Gothic", "Noto Sans JP", sans-serif';

	// 日本語は単語の区切りに空白がないため、1文字ずつ幅を測って折り返す。
	function wrapText( ctx, text, maxWidth ) {
		const lines = [];
		let line = '';
		for ( const ch of text ) {
			if ( line && ctx.measureText( line + ch ).width > maxWidth ) {
				lines.push( line );
				line = ch;
			} else {
				line += ch;
			}
		}
		if ( line ) {
			lines.push( line );
		}
		return lines;
	}

	async function draw( canvas, title ) {
		const ctx = canvas.getContext( '2d' );
		try {
			await document.fonts.load( 'bold 60px ' + FONT );
		} catch ( e ) {
			// フォントを読み込めなくても代替フォントで描画を続ける。
		}

		ctx.fillStyle = '#1e3a5f';
		ctx.fillRect( 0, 0, W, H );

		ctx.fillStyle = '#ffffff';
		ctx.font = 'bold 60px ' + FONT;
		ctx.textBaseline = 'top';
		let lines = wrapText( ctx, title, W - 200 );
		if ( lines.length > 3 ) {
			lines = lines.slice( 0, 3 );
			lines[ 2 ] = lines[ 2 ].slice( 0, -1 ) + '…';
		}
		const lineHeight = 84;
		const top = ( H - lines.length * lineHeight ) / 2 - 20;
		lines.forEach( ( text, i ) => ctx.fillText( text, 100, top + i * lineHeight ) );

		ctx.fillStyle = '#cbd5e1';
		ctx.font = '28px ' + FONT;
		ctx.fillText( wpdrOgp.siteName, 100, H - 90 );
	}

	document.addEventListener( 'click', async ( event ) => {
		const button = event.target.closest( '#wpdr-ogp-generate' );
		if ( ! button ) {
			return;
		}
		const canvas = document.getElementById( 'wpdr-ogp-canvas' );
		const status = document.getElementById( 'wpdr-ogp-status' );
		const editor = wp.data.select( 'core/editor' );
		const title = ( editor.getEditedPostAttribute( 'title' ) || '' ).trim();
		const postId = editor.getCurrentPostId();

		if ( ! title ) {
			status.textContent = '先にタイトルを入力してください。';
			return;
		}

		button.disabled = true;
		status.textContent = '作成しています…';
		try {
			await draw( canvas, title );
			const blob = await new Promise( ( resolve ) => canvas.toBlob( resolve, 'image/png' ) );
			if ( ! blob ) {
				throw new Error( '画像に変換できませんでした。' );
			}

			const body = new FormData();
			body.append( 'file', blob, 'ogp-' + postId + '.png' );
			body.append( 'post', postId );
			body.append( 'alt_text', title );
			const media = await wp.apiFetch( { path: '/wp/v2/media', method: 'POST', body } );

			// 投稿の編集内容としてカスタムフィールドを更新する(保存時に一緒に送られる)。
			wp.data.dispatch( 'core/editor' ).editPost( { meta: { [ wpdrOgp.metaKey ]: media.id } } );
			status.textContent = '作成しました。投稿を保存すると反映されます。';
		} catch ( error ) {
			status.textContent = '失敗しました: ' + ( error.message || '不明なエラー' );
		} finally {
			button.disabled = false;
		}
	} );
} )();

画像のアップロードは、ボタンを押した時点で行われます。一方、添付ファイルの ID を投稿に結びつける処理はエディターの編集内容として扱われるため、投稿の「保存」や「更新」を押すまで公開ページには反映されません。

公開ページに og:image を出力する

SEO プラグインを使っていない場合は、wp_head で og:image を出力します。

add_action( 'wp_head', 'wpdr_ogp_output', 5 );
function wpdr_ogp_output() {
	if ( ! is_singular( 'post' ) ) {
		return;
	}
	// SEO プラグインが OGP を出力している場合は重複させない(Yoast SEO の例)。
	$enabled = apply_filters( 'wpdr_ogp_output_enabled', ! defined( 'WPSEO_VERSION' ) );
	if ( ! $enabled ) {
		return;
	}
	$id  = (int) get_post_meta( get_queried_object_id(), WPDR_OGP_META, true );
	$src = $id ? wp_get_attachment_image_src( $id, 'full' ) : false;
	if ( ! $src ) {
		return;
	}
	printf( "<meta property=\"og:image\" content=\"%s\">\n", esc_url( $src[0] ) );
	printf( "<meta property=\"og:image:width\" content=\"%d\">\n", (int) $src[1] );
	printf( "<meta property=\"og:image:height\" content=\"%d\">\n", (int) $src[2] );
	echo "<meta name=\"twitter:card\" content=\"summary_large_image\">\n";
}

og:image だけでなく、og:title・og:url・og:type などもページに必要です。テーマやほかのプラグインがすでに出力しているかを、公開ページのソースで確認してください。

Yoast SEO や Rank Math などの SEO プラグインを使っている場合は、OGP タグを二重に出さないことが大切です。いちばん簡単なのは、作成した画像を各プラグインの投稿ごとの SNS 画像の設定欄で指定する方法です。自動で連携させたい場合は、プラグインが用意している OGP 画像のフィルター(Yoast SEO の wpseo_opengraph_image など)を使う方法がありますが、フィルターの名前や挙動はプラグインのバージョンによって変わることがあるため、各プラグインの開発者向けドキュメントで確認してから実装してください。上のコードの wpdr_ogp_output_enabled フィルターは、ほかの SEO プラグインを使う場合に出力を止めるための独自フィルターです。

つまずきやすい点

フォントが思ったとおりにならない

Canvas は、ブラウザがその時点で使えるフォントでしか描けません。管理画面では公開側のテーマが読み込む Web フォントは読み込まれないため、上の例では OS に入っている日本語フォントを優先しています。特定の Web フォントで描きたい場合は、編集画面でそのフォントを @font-face などで読み込み、document.fonts.load() で読み込みを待ってから描画します。日本語フォントはファイルが大きいので、自分のサーバーに置く場合はライセンスと読み込み時間を確認してください。

「Tainted canvas」のエラーで画像に変換できない

ロゴや背景画像を Canvas に描く場合、別のドメイン(CDN など)の画像をそのまま描くと Canvas が「汚染」された状態になり、toBlob() がセキュリティエラーになります。画像を同じドメインから読み込むか、配信元で CORS(Access-Control-Allow-Origin)を許可したうえで img.crossOrigin = 'anonymous' を指定して読み込みます。

作り直すたびに画像が増える

ボタンを押すたびに新しい添付ファイルができ、WordPress はそれぞれにサムネイルなどの中間サイズも作ります。何度も作り直した投稿では、使わなくなった画像をメディアライブラリで整理してください。画像の URL は作り直すたびに変わるため、SNS 側の古いキャッシュが残りにくいという利点もあります。

SNS に古い画像が表示される

SNS はページの情報を一定期間キャッシュします。Facebook はシェアデバッガーで再取得を依頼できます。ほかのサービスでは再取得の方法が用意されていないこともあり、時間をおくと更新されるのを待つことになります。

サーバー側で生成する方法との比較

OGP 画像は、PHP の GD や Imagick、Node.js の Canvas 互換ライブラリなどを使ってサーバー側で生成することもできます。サーバー側なら、既存の投稿すべてに一括で作る、テンプレートを変えたら全部作り直す、といった処理を自動化しやすくなります。一方で、日本語フォントの用意や、リクエストのたびに生成しないためのキャッシュの仕組みが必要です。

Canvas を使う方法は、追加のサーバー環境が要らず、書き手が仕上がりを見てから保存できるのが長所です。投稿数が少なく、記事ごとに確認して作りたいサイトに向いています。

まとめ

  • SNS のクローラーは JavaScript を実行しないため、Canvas で描いた画像はサーバーに保存し、その URL を og:image に出力する
  • 編集画面のメタボックスで描画し、toBlob() で PNG にして /wp/v2/media へアップロード、添付ファイル ID をカスタムフィールドに保存する
  • カスタムフィールドは register_post_meta() で show_in_rest と auth_callback を指定し、アップロードは upload_files 権限を持つユーザーに限る
  • SEO プラグインを使う場合は OGP タグを二重に出さず、プラグインの設定欄か公式のフィルターで連携する
  • フォントの読み込み待ち、別ドメイン画像による Canvas の汚染、画像の増加、SNS のキャッシュに注意する

お気軽にご相談ください

お見積りへ お問い合わせへ
▲