【WordPress】投稿ごとに好きなCSSやJSを追加する方法

【WordPress】投稿ごとに好きなCSSやJSを追加する方法 WordPress

記事ごとに見た目を少し変えたい、特定の記事だけに小さなスクリプトを動かしたい、という場面は意外と多くあります。そのたびにテーマのCSSやJSへ書き足すと、ファイルが膨らんで管理しづらくなります。この記事では、投稿編集画面にメタボックスを追加し、入力したCSSとJSをその投稿ページだけに出力する方法を解説します。メタキーは_custom_cssと_custom_jsを使います。

これは任意のコードを保存して実行する仕組みです
保存したJavaScriptは、そのページを開いた全員のブラウザで実行されます。編集権限があるだけの著者ロールなどに開放すると、保存型XSSの入口になります。この記事のコードは、保存と画面表示の両方でunfiltered_html権限を確認します。この権限は通常、管理者と編集者にあります。マルチサイトでは特権管理者のみで、wp-config.phpでDISALLOW_UNFILTERED_HTMLを有効にしている環境では誰にもありません。
スポンサーリンク

実装の全体像

手順は四つです。メタキーを登録し、編集画面に入力欄を置き、保存時に権限を確認し、フロントでその投稿ページだけに出力します。CSSは<head>にインラインで、JSはフッターにインラインで出します。コードはすべて子テーマのfunctions.phpに追加します。子テーマの使い方は子テーマでfunctions.phpを正しく拡張する方法をご覧ください。

対象の投稿タイプとメタキーを登録する

対象の投稿タイプを返す関数を一つ作り、以降のコードはすべてそれを参照します。固定ページにも使いたいときは、配列にpageを足すだけで済みます。メタキーは_で始まる保護メタにしているため、標準のカスタムフィールド欄には表示されず、REST APIからも勝手には更新できません。

functions.php:対象投稿タイプとメタキーの登録
<?php
// 対象の投稿タイプ(固定ページも使うなら 'page' を追加)
function my_custom_code_types() {
  return ['post'];
}

// メタキーの登録(編集権限を unfiltered_html に限定)
add_action('init', function () {
  foreach (['_custom_css', '_custom_js'] as $key) {
    register_meta('post', $key, [
      'type'          => 'string',
      'single'        => true,
      'auth_callback' => function () {
        return current_user_can('unfiltered_html');
      },
    ]);
  }
});

編集画面にメタボックスを追加する

権限のないユーザーには、入力欄そのものを表示しません。nonceの発行もここで行います。

functions.php:メタボックスの追加
<?php
// メタボックス(権限のあるユーザーにだけ表示する)
add_action('add_meta_boxes', function () {
  if (!current_user_can('unfiltered_html')) return;

  add_meta_box('custom_code_box', '投稿専用CSS・JS', function ($post) {
    wp_nonce_field('save_custom_code_' . $post->ID, 'custom_code_nonce');
    $css = get_post_meta($post->ID, '_custom_css', true);
    $js  = get_post_meta($post->ID, '_custom_js', true);

    echo '<p>この投稿だけに適用するCSS(styleタグは不要)</p>';
    echo '<textarea name="_custom_css" style="width:100%;height:200px;font-family:monospace;">'
       . esc_textarea($css) . '</textarea>';
    echo '<p>この投稿だけで実行するJavaScript(scriptタグは不要)</p>';
    echo '<textarea name="_custom_js" style="width:100%;height:200px;font-family:monospace;">'
       . esc_textarea($js) . '</textarea>';
  }, my_custom_code_types(), 'normal', 'default');
});

保存処理で権限を確認する

保存処理はsave_postフックにまとめます。nonce、編集権限、unfiltered_htmlの順に確認し、一つでも満たさなければ何も保存しません。リビジョンや自動保存でもsave_postは実行されるため、先に除外しています。

ここでscriptタグやstyleタグを正規表現で取り除く実装がよく見られますが、防御にはなりません。たとえば</st</style>yle>のように入れ子にすると、除去後に</style>が復元されます。安全性を担保するのは権限チェックだけです。制御文字の除去は、文字化けや不可視文字の混入を防ぐ目的で、Ccカテゴリに限定しています。ZWJなどの書式文字まで消すと絵文字の結合が壊れるためです。

functions.php:保存処理
<?php
add_action('save_post', function ($post_id, $post) {
  if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
  if (wp_is_post_revision($post_id)) return;
  if (!in_array($post->post_type, my_custom_code_types(), true)) return;

  // nonce → 編集権限 → コードを保存できる権限、の順に確認する
  if (!isset($_POST['custom_code_nonce'])
      || !wp_verify_nonce($_POST['custom_code_nonce'], 'save_custom_code_' . $post_id)) return;
  if (!current_user_can('edit_post', $post_id)) return;
  if (!current_user_can('unfiltered_html')) return;

  foreach (['_custom_css', '_custom_js'] as $key) {
    $raw = isset($_POST[$key]) ? wp_unslash($_POST[$key]) : '';
    // 制御文字(Cc)だけを除去。改行とタブは残す
    $raw = preg_replace('/[^\P{Cc}\t\n\r]+/u', '', $raw);

    if ($raw === '') {
      delete_post_meta($post_id, $key);
    } else {
      update_post_meta($post_id, $key, $raw);
    }
  }
}, 10, 2);

フロントでCSSをheadに出力する

対象の投稿ページだけ、wp_headで<style>を出力します。優先度を50にしているのは、テーマやプラグインのスタイルより後に出して、上書きしやすくするためです。

functions.php:CSSの出力
<?php
add_action('wp_head', function () {
  if (!is_singular(my_custom_code_types())) return;

  $css = (string) get_post_meta(get_queried_object_id(), '_custom_css', true);
  if ($css === '') return;

  echo "<style id=\"post-custom-css\">\n" . $css . "\n</style>\n";
}, 50);

フロントでJSをフッターに出力する

JSは、中身が空のダミーハンドルを登録し、wp_add_inline_script()で差し込みます。第5引数をtrueにしているので、フッターで出力されます。

functions.php:JSの出力
<?php
add_action('wp_enqueue_scripts', function () {
  if (!is_singular(my_custom_code_types())) return;

  $js = (string) get_post_meta(get_queried_object_id(), '_custom_js', true);
  if ($js === '') return;

  // 空のダミーハンドルをフッターに登録し、そこへインラインで差し込む
  wp_register_script('post-custom-inline', '', [], null, true);
  wp_enqueue_script('post-custom-inline');
  wp_add_inline_script('post-custom-inline', $js);
}, 20);

使い方

投稿編集画面の「投稿専用CSS・JS」に、それぞれコードだけを書きます。styleタグやscriptタグは不要です。CSSはheadに、JSはフッターに出力され、その投稿ページでだけ有効になります。ブロックエディタでも、クラシックのメタボックスとして編集画面の下部に表示されます。

固定ページやカスタム投稿タイプにも使う

上のコードは、最初に定義したmy_custom_code_types()を使っています。固定ページにも使うなら、次のように配列へ追加するだけで、メタボックス、保存、出力のすべてが対応します。出力フックを複製する必要はありません。複製するとid="post-custom-css"が重複するため避けてください。

functions.php:対象を広げる
<?php
function my_custom_code_types() {
  return ['post', 'page', 'article']; // article はカスタム投稿タイプの例
}

トラブルシューティング

  • 入力欄が表示されない:ログイン中のユーザーがunfiltered_html権限を持っているか確認してください。マルチサイトでは特権管理者のみです。
  • 保存しても反映されない:ブロックエディタでは、メタボックスの内容が投稿本体とは別のリクエストで保存されます。更新ボタンを押したあと、保存完了を待ってからページを確認してください。
  • JSが動かない:ブラウザのコンソールでエラーを確認します。jQueryを使うコードなら、ダミーハンドルの依存にjqueryを指定します(下記)。フッターで実行されるため、DOMは構築済みです。
  • CSSが効かない:テーマ側の指定が優先されている可能性があります。セレクタを具体的にするか、優先度の値を大きくして出力を遅らせます。
functions.php:jQuery依存にする場合
<?php
// jQueryを使う投稿JSの場合は、依存に 'jquery' を指定する
wp_register_script('post-custom-inline', '', ['jquery'], null, true);

全ページ共通ではなく、条件でアセットを出し分けたいときはテーマに条件付きでCSSやJSを読み込む方法、外部ファイルとして読み込むならwp_enqueue_scriptの使い方とwp_enqueue_styleの使い方も参考になります。

よくある質問(FAQ)

Q著者ロールのユーザーにも使わせてよいですか?
Aおすすめしません。入力したJavaScriptは全訪問者のブラウザで実行されるため、信頼できるユーザーに限定してください。この記事のコードはunfiltered_html権限を持つユーザーだけが保存できます。
Qstyleタグやscriptタグを除去する処理は入れなくてよいですか?
A入れても防御にはなりません。入れ子にした文字列で、除去後にタグが復元されるためです。コードを保存できる人を権限で絞ることが対策になります。出力時にタグを自動で付けるので、入力欄にはコードだけを書けば足ります。
Q標準の[カスタムフィールド]欄で編集できますか?
Aできません。_で始まるメタキーは保護メタとして扱われ、カスタムフィールド欄には表示されません。編集は、この記事のメタボックスから行います。
Q外部ファイルとして読み込みたい場合は?
Aファイルを用意し、wp_enqueue_scriptsフックの中で条件分岐して読み込みます。方法はテーマに条件付きでCSSやJSを読み込む方法で解説しています。

まとめ

投稿ごとのCSS・JSは、メタボックスで入力して保存し、対象の投稿ページだけに出力すれば実現できます。任意のコードを実行させる仕組みなので、保存と画面表示の両方をunfiltered_html権限で絞ることが最も重要です。タグの除去ではなく、権限で守るようにしてください。