WordPress標準コードブロックにコピーボタンを追加する方法【functions.php】

「WordPress標準コードブロックにコピーボタンを追加する方法【functions.php】」の内容を表す技術イラスト

技術記事にプログラムやコマンドを掲載するとき、コードブロックに「コピー」ボタンがあると、読者は範囲選択をせずにコード全体を取得できます。

WordPressのGutenbergには標準の「コード」ブロックがありますが、テーマやプラグインの構成によってはコピーボタンが表示されません。

この記事では、子テーマのfunctions.phpへコードを追加し、WordPress標準コードブロックにコピーボタンを表示する方法を解説します。

完成コードには、次の機能を含めています。

  • Gutenberg標準の「コード」ブロックだけを対象にする
  • 記事に保存されているブロック本文を変更しない
  • コピー前は白いボタンで表示する
  • コピー成功時は「コピー済み」と表示する
  • コピー失敗時は「コピー失敗」と表示する
  • Clipboard APIが使えない環境では代替処理を試みる
  • マウスだけでなくキーボード操作にも配慮する
  • コード本文にボタンのクラス名が含まれていても誤検出しない
目次

完成後の動作

標準コードブロックの右上に、白い「コピー」ボタンが表示されます。

ボタンを押すと、コードブロック内のテキストがクリップボードへコピーされ、ボタンは一時的に「コピー済み」へ変化します。コピーに失敗した場合は「コピー失敗」と表示されます。

Hello World

導入前の注意点

このコードは、WordPress標準の「コード」ブロック(core/code)を対象としています。「整形済みテキスト」ブロック、カスタムHTMLブロック、テーマやプラグイン独自のコードブロックには適用されません。

編集するファイルは、親テーマではなく子テーマのfunctions.phpを推奨します。親テーマへ直接追加すると、テーマの更新時に変更内容が失われる可能性があります。

作業前には、現在のfunctions.phpを必ずバックアップしてください。PHPの構文エラーが発生すると、WordPressの画面が正常に表示されなくなる場合があります。

以下のコードは、既存のfunctions.php末尾へ追記する形式です。コード全体を先頭から末尾までコピーし、既存コードの後ろへそのまま追加してください。

CSSとJavaScriptもPHPコード内に含まれているため、style.cssや別のJavaScriptファイルへ追加する必要はありません。コードを分割せず、ひとまとまりのまま追加します。

コピーボタンを追加する全コード

以下が完成コードです。

/**
 * Add a copy button to WordPress Gutenberg core/code blocks.
 */

/**
 * Wrap each core Code block with a copy button on the front end.
 *
 * The saved post content is not changed. The wrapper is added only when WordPress
 * renders the block.
 *
 * @param string $block_content Rendered Code block HTML.
 * @param array  $block         Parsed block data.
 * @return string
 */
function ekplabz_add_copy_button_to_code_block( $block_content, $block ) {
	// Check for the actual unescaped wrapper, not just the class-name text.
	// A code example may legitimately contain "ekplabz-code-copy".
	if ( 0 === strpos( ltrim( $block_content ), '<div class="ekplabz-code-wrap">' ) ) {
		return $block_content;
	}

	$button = '<button type="button" class="ekplabz-code-copy" aria-label="コードをクリップボードへコピー">'
		. '<span aria-live="polite">コピー</span>'
		. '</button>';

	return '<div class="ekplabz-code-wrap">' . $button . $block_content . '</div>';
}
add_filter( 'render_block_core/code', 'ekplabz_add_copy_button_to_code_block', 10, 2 );

/**
 * Load the small amount of CSS and JavaScript required by the copy button.
 */
function ekplabz_enqueue_code_copy_assets() {
	$css = <<<'CSS'
.ekplabz-code-wrap {
  position: relative;
  margin-block: 1.5em;
}

.ekplabz-code-wrap > .wp-block-code {
  margin: 0;
  padding-top: 3.25rem !important;
  overflow: auto;
}

.ekplabz-code-copy {
  position: absolute;
  top: 0.65rem;
  right: 0.65rem;
  z-index: 2;
  min-width: 4.5em;
  padding: 0.4em 0.75em;
  border: 1px solid #d1d5db;
  border-radius: 0.4rem;
  color: #111827;
  background: #ffffff;
  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.16);
  font: inherit;
  font-size: 0.78rem;
  line-height: 1.2;
  cursor: pointer;
  transition: background-color 0.15s ease, transform 0.15s ease;
}

.ekplabz-code-copy:hover {
  background: #f3f4f6;
}

.ekplabz-code-copy:active {
  transform: translateY(1px);
}

.ekplabz-code-copy:focus-visible {
  outline: 2px solid #38bdf8;
  outline-offset: 2px;
}

.ekplabz-code-copy.is-copied {
  color: #ffffff;
  background: #166534;
}

.ekplabz-code-copy.is-error {
  color: #ffffff;
  background: #991b1b;
}
CSS;

	$js = <<<'JS'
(function () {
  'use strict';

  function fallbackCopy(text) {
    var textarea = document.createElement('textarea');
    var copied = false;

    textarea.value = text;
    textarea.setAttribute('readonly', '');
    textarea.style.position = 'fixed';
    textarea.style.opacity = '0';
    textarea.style.pointerEvents = 'none';
    document.body.appendChild(textarea);
    textarea.select();

    try {
      copied = document.execCommand('copy');
    } catch (error) {
      copied = false;
    }

    textarea.remove();
    return copied;
  }

  function showResult(button, succeeded) {
    var label = button.querySelector('span');
    var oldTimer = Number(button.dataset.copyTimer || 0);

    window.clearTimeout(oldTimer);
    button.classList.remove('is-copied', 'is-error');
    button.classList.add(succeeded ? 'is-copied' : 'is-error');
    label.textContent = succeeded ? 'コピー済み' : 'コピー失敗';

    button.dataset.copyTimer = String(window.setTimeout(function () {
      button.classList.remove('is-copied', 'is-error');
      label.textContent = 'コピー';
    }, 1600));
  }

  function copyCode(button) {
    var wrapper = button.closest('.ekplabz-code-wrap');
    var code = wrapper ? wrapper.querySelector('.wp-block-code code') : null;
    var text;

    if (!code) {
      showResult(button, false);
      return;
    }

    text = code.textContent || '';

    if (navigator.clipboard && window.isSecureContext) {
      navigator.clipboard.writeText(text).then(function () {
        showResult(button, true);
      }).catch(function () {
        showResult(button, fallbackCopy(text));
      });
      return;
    }

    showResult(button, fallbackCopy(text));
  }

  document.addEventListener('click', function (event) {
    var target = event.target;
    var button;

    if (!(target instanceof Element)) {
      return;
    }

    button = target.closest('.ekplabz-code-copy');

    if (button) {
      copyCode(button);
    }
  });
}());
JS;

	wp_register_style( 'ekplabz-code-copy', false, array(), null );
	wp_enqueue_style( 'ekplabz-code-copy' );
	wp_add_inline_style( 'ekplabz-code-copy', $css );

	wp_register_script( 'ekplabz-code-copy', false, array(), null, true );
	wp_enqueue_script( 'ekplabz-code-copy' );
	wp_add_inline_script( 'ekplabz-code-copy', $js );
}
add_action( 'wp_enqueue_scripts', 'ekplabz_enqueue_code_copy_assets' );

コード全体の構成

このコードは、大きく分けて3つの役割を持っています。

部分役割
PHP標準コードブロックを検出し、コピーボタンを追加する
CSSボタンの位置、色、余白、状態表示を整える
JavaScriptコードをクリップボードへコピーし、結果を表示する

コピー操作は閲覧者のブラウザ上で発生するため、PHPだけでは完結しません。PHPでボタンとJavaScriptをページへ組み込み、実際のコピー処理をJavaScriptに担当させています。

PHP:標準コードブロックだけにボタンを追加する

最初のPHP部分では、WordPressがコードブロックを表示するときのHTMLへボタンを追加しています。

add_filter( 'render_block_core/code', 'ekplabz_add_copy_button_to_code_block', 10, 2 );

render_block_core/codeは、core/codeブロックの表示内容だけを加工するフィルターです。

WordPressには、特定のブロックだけを対象にできるrender_block_{$this->name}という仕組みがあります。ブロック名がcore/codeなので、実際のフック名はrender_block_core/codeになります。

これにより、段落、見出し、引用などの別ブロックへ影響を与えず、標準の「コード」ブロックだけを処理できます。

保存済みの記事本文は変更しない

フィルターへ渡される$block_contentは、WordPressが表示用に生成したコードブロックのHTMLです。

return '<div class="ekplabz-code-wrap">' . $button . $block_content . '</div>';

この処理では、表示時のHTMLをラッパー要素で囲んでボタンを追加しています。WordPressのデータベースに保存されている記事本文そのものを書き換える処理ではありません。

機能を停止したい場合は、追加したPHPコードを外せば、元のコードブロック表示へ戻せます。

コード本文と実際のラッパーを区別する

重複防止判定では、クラス名だけを検索せず、実際に追加するラッパーHTMLがブロックの先頭にあるかを確認します。

if ( 0 === strpos( ltrim( $block_content ), '<div class="ekplabz-code-wrap">' ) ) {
    return $block_content;
}

コードブロックは、HTMLやCSS、PHPそのものを解説するためにも使用されます。そのため、次のようなコードを記事内へ掲載する可能性があります。

wp_register_style( 'ekplabz-code-copy', false, array(), null );

単にekplabz-code-copyという文字列があるかを調べると、このコード本文まで「すでにボタンがある」と誤認してしまいます。

一方、コードブロック内に書かれた<div>は表示用にエスケープされ、実際のHTML要素としては扱われません。そこで、PHPが追加した未エスケープのラッパーがブロック先頭に存在する場合だけ、処理済みと判定します。

これにより、同じブロックへ処理が重複して適用されることを防ぎながら、コード本文にクラス名が含まれていてもコピーボタンを追加できます。

修正前に一部のコードブロックへ適用されなかった理由

修正前の判定は、次のようになっていました。

if ( false !== strpos( $block_content, 'ekplabz-code-copy' ) ) {
    return $block_content;
}

この判定は、コードブロックのHTML全体からekplabz-code-copyを探します。実際のボタンだけでなく、読者に見せるコード本文も検索対象です。

そのため、適用結果は次のように分かれていました。

掲載したコードクラス名を含むか修正前の結果
add_action( 'wp_enqueue_scripts', ... )含まないボタンが付く
wp_register_style( 'ekplabz-code-copy', ... )含む処理が途中終了する

見た目上は同じWordPress標準コードブロックでも、本文に特定の文字列が含まれるかどうかで結果が変わっていました。これはブロックの種類やキャッシュによるランダムな現象ではなく、重複防止条件による再現性のある誤検出です。

修正版では、検索対象をクラス名だけの文字列から、実際のラッパー開始タグへ変更しています。

アクセシビリティへの配慮

ボタンにはaria-labelを付けています。

aria-label="コードをクリップボードへコピー"

また、表示文字を囲むspanにはaria-live="polite"を設定しています。

<span aria-live="polite">コピー</span>

これにより、「コピー」から「コピー済み」へ表示が変わったことを、支援技術が認識しやすくなります。

CSS:白いコピーボタンを右上へ配置する

コードブロック全体を囲む要素には、position: relativeを設定しています。

.ekplabz-code-wrap {
  position: relative;
  margin-block: 1.5em;
}

コピーボタン側でposition: absoluteを使用し、ラッパーの右上を基準に配置するためです。

.ekplabz-code-copy {
  position: absolute;
  top: 0.65rem;
  right: 0.65rem;
  z-index: 2;
}

コードとボタンが重ならないようにする

ボタンをコードブロック内の右上へ重ねるだけでは、コードの1行目とボタンが重なる可能性があります。

そこで、コードブロック上部に余白を確保しています。

.ekplabz-code-wrap > .wp-block-code {
  padding-top: 3.25rem !important;
}

!importantは多用すべきではありませんが、テーマ側がコードブロックの余白を強く指定している場合でも、ボタン用の上部余白を確保する目的で使用しています。

コピー前のボタンを白くする

通常状態は、白い背景、濃い文字、薄いグレーの枠にしています。

border: 1px solid #d1d5db;
color: #111827;
background: #ffffff;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.16);

ホバー時は背景を薄いグレーへ変化させます。

.ekplabz-code-copy:hover {
  background: #f3f4f6;
}

成功と失敗を色で区別する

コピー成功時にはJavaScriptからis-copiedクラスが追加されます。

.ekplabz-code-copy.is-copied {
  color: #ffffff;
  background: #166534;
}

失敗時にはis-errorクラスが追加されます。

.ekplabz-code-copy.is-error {
  color: #ffffff;
  background: #991b1b;
}

通常は白、成功は緑、失敗は赤となるため、状態を視覚的に判断できます。表示文字も同時に変化するため、色だけに依存した通知にはなっていません。

JavaScript:コードをクリップボードへコピーする

コピー対象は、押されたボタンと同じラッパー内にあるコード要素です。

var wrapper = button.closest('.ekplabz-code-wrap');
var code = wrapper ? wrapper.querySelector('.wp-block-code code') : null;

ページ内にコードブロックが複数あっても、押したボタンに対応するコードだけを取得できます。

コードの内容はtextContentで取得しています。

text = code.textContent || '';

innerHTMLではなくtextContentを使うことで、コード表示用のHTMLタグではなく、読者がコピーしたい文字列そのものを取得します。

Clipboard APIを優先する

安全な接続環境でClipboard APIが利用できる場合は、navigator.clipboard.writeText()を使います。

if (navigator.clipboard && window.isSecureContext) {
  navigator.clipboard.writeText(text)
}

一般的な公開サイトでは、HTTPSで配信することが重要です。

代替コピー処理

Clipboard APIを利用できない場合や、コピーに失敗した場合は、非表示のtextareaを一時的に作成してコピーを試みます。

textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
copied = document.execCommand('copy');
textarea.remove();

document.execCommand('copy')は古い方式なので、主処理ではなく互換性のための代替手段として使用しています。

成功・失敗表示を1.6秒後に戻す

コピー結果に応じて、クラスと表示文字を変更します。

button.classList.add(succeeded ? 'is-copied' : 'is-error');
label.textContent = succeeded ? 'コピー済み' : 'コピー失敗';

その後、setTimeout()で1.6秒後に通常状態へ戻します。

window.setTimeout(function () {
  button.classList.remove('is-copied', 'is-error');
  label.textContent = 'コピー';
}, 1600);

ページを再読み込みしなくても、同じボタンを繰り返し使用できます。

イベントデリゲーションを使う理由

各ボタンへ個別にイベントを登録せず、ページ全体でクリックを1回監視しています。

document.addEventListener('click', function (event) {

クリックされた場所が.ekplabz-code-copyの内部かを確認し、該当する場合だけコピー処理を実行します。

button = target.closest('.ekplabz-code-copy');

この方式なら、ページ内のコードブロック数が増えても、ボタンごとに同じイベント登録処理を繰り返す必要がありません。

CSSとJavaScriptをWordPressへ読み込む

フロントエンドで使用するCSSとJavaScriptは、wp_enqueue_scriptsアクションから登録しています。

add_action( 'wp_enqueue_scripts', 'ekplabz_enqueue_code_copy_assets' );

名前にscriptsとありますが、このフックはフロントエンド用のJavaScriptだけでなくCSSの読み込みにも使用されます。

CSSの登録

wp_register_style( 'ekplabz-code-copy', false, array(), null );
wp_enqueue_style( 'ekplabz-code-copy' );
wp_add_inline_style( 'ekplabz-code-copy', $css );

今回は小さなCSSなので、別ファイルを作らず、登録したスタイルハンドルへwp_add_inline_style()で追加しています。

JavaScriptの登録

wp_register_script( 'ekplabz-code-copy', false, array(), null, true );
wp_enqueue_script( 'ekplabz-code-copy' );
wp_add_inline_script( 'ekplabz-code-copy', $js );

第5引数のtrueは、スクリプトをフッター側で出力する指定です。HTML本文を読み込んだ後にコピー処理が利用できる構成になります。

wp_add_inline_script()へ渡す文字列には<script>タグを含めません。WordPress側が必要なスクリプトタグを生成します。

WordPressへの導入手順

1. 子テーマを使用していることを確認する

WordPress管理画面の「外観」から、子テーマが有効になっていることを確認します。

2. functions.phpをバックアップする

編集対象は、通常は次のファイルです。

wp-content/themes/子テーマ名/functions.php

編集前のファイルをローカルへ保存しておくと、構文エラーが発生した場合に戻せます。

3. 完成コードを追加する

この記事の完成コードを先頭から末尾までコピーし、子テーマのfunctions.php末尾にある既存コードの後へ追加します。

完成コードにはPHP、CSS、JavaScriptがすべて含まれています。途中のCSS;やJS;を含め、コード全体を分割せずに追加してください。

4. 標準の「コード」ブロックで確認する

投稿または固定ページへ、Gutenberg標準の「コード」ブロックを追加します。

「整形済みテキスト」ブロックや「カスタムHTML」ブロックは別のブロックなので、このコードの対象にはなりません。

5. キャッシュを削除する

変更後も以前の表示が残る場合は、次のキャッシュを削除します。

  • WordPressのキャッシュプラグイン
  • テーマ側のキャッシュ
  • CDNのキャッシュ
  • ブラウザのキャッシュ

ボタンのデザインを変更する

主な調整箇所は次のとおりです。

変更したい項目CSSプロパティ現在値
背景色background#ffffff
文字色color#111827
枠線border1px solid #d1d5db
角の丸みborder-radius0.4rem
上からの位置top0.65rem
右からの位置right0.65rem
文字サイズfont-size0.78rem
成功時の背景background#166534
失敗時の背景background#991b1b

たとえば、角をもう少し丸くする場合は次のように変更します。

border-radius: 0.75rem;

ボタンが表示されない場合

標準のコードブロックか確認する

このコードが対象にするのはcore/codeです。「整形済みテキスト」や、テーマ独自のコードブロックには表示されません。

キャッシュを削除する

PHPを更新しても、ページキャッシュに古いHTMLが残っているとボタンが表示されません。WordPress、テーマ、CDN、ブラウザの各キャッシュを確認します。

functions.phpの保存に失敗していないか確認する

WordPress管理画面が構文エラーを検出した場合、変更自体が適用されていないことがあります。エラーメッセージが表示されていないか確認してください。

コピーできない場合

HTTPSで公開されているか確認する

Clipboard APIは安全なコンテキストでの利用が基本です。公開サイトがHTTPSになっているか確認します。

JavaScript最適化を一時停止する

キャッシュプラグインや高速化機能がJavaScriptを結合・遅延・圧縮した結果、処理が変わる場合があります。問題の切り分けでは、JavaScript最適化を一時的に停止して確認します。

ブラウザの開発者ツールを確認する

ブラウザの開発者ツールを開き、ConsoleにJavaScriptエラーが出ていないか確認します。別のプラグインやテーマが発生させたエラーが、後続処理へ影響する場合もあります。

functions.phpで構文エラーが出る場合

貼り付ける場所を確認する

完成コードは、子テーマのfunctions.php末尾にある既存のPHPコードの続きとして追加します。style.cssや、WordPress管理画面の「追加CSS」へ貼り付けるコードではありません。

CSSをfunctions.phpへ直接貼っていないか確認する

@charset "UTF-8";や通常のCSSをPHPコードの途中へそのまま貼ると、PHPとして解釈されて構文エラーになります。

この記事の完成コードでは、CSSをNowdoc構文の$css文字列内へ収めています。CSS;の位置や引用符を崩さず、まとまりごと貼り付けてください。

コードの一部だけが欠けていないか確認する

次の記号が欠けると、PHPの構文エラーになりやすいため注意が必要です。

  • 行末のセミコロン ;
  • 関数を閉じる波括弧 }
  • 文字列を閉じる引用符 'または"
  • Nowdocを閉じるCSS;とJS;

小規模実装からプラグイン化できる

まずは子テーマのfunctions.phpへ追加する方法が手軽です。

一方、複数サイトで再利用する場合や、テーマを変更しても機能を残したい場合は、この処理を小さなWordPressプラグインへ分離できます。

機能をプラグイン化すると、次のような拡張もしやすくなります。

  • 管理画面からボタン色を変更する
  • 表示文字を変更する
  • 特定の投稿タイプだけで有効にする
  • 言語名やファイル名をコードブロック上部へ表示する
  • コードブロックがあるページだけCSSとJavaScriptを読み込む
  • ショートコードや独自ブロックにも対応する

まとめ

WordPress標準コードブロックへのコピーボタンは、PHP、CSS、JavaScriptを組み合わせれば、外部プラグインを追加せずに実装できます。

  • render_block_core/codeで標準コードブロックだけを加工する
  • 記事本文は変更せず、表示時のHTMLへボタンを追加する
  • コード本文にクラス名が含まれても処理済みと誤判定しない
  • CSSで白いボタンを右上へ配置する
  • Clipboard APIを使ってコードをコピーする
  • 成功・失敗を文字と色で通知する
  • wp_enqueue_scriptsからCSSとJavaScriptを読み込む

最初は子テーマのfunctions.phpで小さく導入し、再利用範囲が広がった段階でプラグイン化すると、保守しやすい構成へ発展させられます。

参考資料

参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

機械設計・油圧・CAD・Python・AIなど、ものづくりに関わる技術を扱っています。工学知識を整理・構造化し、設計や自動化に再利用できる形へ変えていくことを目指しています。

目次