2026年8月6日

2026年8月6日

WordPressのcomments_template()をカスタマイズして独自コメントシステムを作る方法

WordPressのコメント欄は、comments_template() が読み込むテンプレート、wp_list_comments() の表示コールバック、comment_form() の入力欄という3つの部品を差し替えることで、標準の保存・承認・スパム対策の仕組みを残したまま独自のコメントシステムに作り替えられます。この記事では、クラシックテーマを前提に、テンプレートの差し替え、表示のカスタマイズ、「評価(1〜5)」のような独自項目の追加と保存までを、安全に実装する手順を解説します。

コメント欄の見た目を少し変えたいだけの場合や、ブロックテーマを使っている場合は、後半の「ブロックテーマの場合」を先に読んでください。

comments_template()の役割を押さえる

comments_template( $file, $separate_comments ) は、表示中の投稿のコメントを取得し、テーマの comments.php(既定値)を読み込む関数です。通常は single.php などのループ内で呼ばれます。中で行っていることは大きく3つです。

  • WP_Comment_Query でコメントを取得する(取得条件は comments_template_query_args フィルターで変更可能)
  • 取得結果をグローバルの $wp_query->comments にセットし、have_comments() や wp_list_comments() から使えるようにする
  • 読み込むテンプレートのパスを comments_template フィルターに通してから require する

つまり「どのファイルを読むか」「どのコメントを取るか」「どう表示するか」を、それぞれ別のフックで変えられます。コア本体のファイルやテーマ本体の comments.php を直接書き換える必要はありません。

手順1:作業場所を決めてバックアップする

作業前にサイトのファイルとデータベースをバックアップし、ステージング環境で試してください。

  • 見た目の変更だけなら、子テーマに comments.php をコピーして編集する
  • 独自項目の保存など、テーマを変えても残したい機能は、プラグインか mu-plugin に書く

表示(テーマの責任)と、データの保存(機能の責任)を分けておくと、テーマ変更時に評価データが表示されなくなる、といった事故を防げます。

手順2:読み込むテンプレートを差し替える

子テーマに comments.php を置けば自動的にそちらが優先されます。プラグイン側から専用テンプレートを使わせたいときは、comments_template フィルターでパスを返します。

<?php
// 投稿タイプ review のときだけ、プラグイン同梱のテンプレートを使う
add_filter( 'comments_template', function ( $theme_template ) {
    if ( is_singular( 'review' ) ) {
        $file = plugin_dir_path( __FILE__ ) . 'templates/comments-review.php';
        if ( file_exists( $file ) ) {
            return $file;
        }
    }
    return $theme_template;
} );

条件を付けずに全投稿で差し替えると、テーマ側の調整がすべて無効になります。対象を絞り、ファイルが無い場合はテーマのテンプレートに戻すのが安全です。

手順3:テンプレートの骨組みを書く

テンプレート内では、パスワード保護の確認、コメント一覧、ページ送り、フォームの順に並べます。

<?php
if ( post_password_required() ) {
    return; // 保護中の投稿ではコメントを出さない
}
?>
<section id="comments" class="my-comments">
    <?php if ( have_comments() ) : ?>
        <h2 class="my-comments__title">
            <?php
            printf(
                /* translators: %s: コメント数 */
                esc_html__( 'コメント(%s件)', 'my-theme' ),
                esc_html( number_format_i18n( get_comments_number() ) )
            );
            ?>
        </h2>
        <ol class="my-comments__list">
            <?php
            wp_list_comments( array(
                'style'       => 'ol',
                'short_ping'  => true,
                'avatar_size' => 48,
                'callback'    => 'my_comment_item',
            ) );
            ?>
        </ol>
        <?php the_comments_pagination(); ?>
    <?php endif; ?>

    <?php if ( ! comments_open() && get_comments_number() ) : ?>
        <p class="my-comments__closed"><?php esc_html_e( 'コメントは受け付けていません。', 'my-theme' ); ?></p>
    <?php endif; ?>

    <?php comment_form(); ?>
</section>

返信機能(スレッド表示)を使う場合は、返信フォームを移動させるスクリプトを読み込みます。テンプレートに直接 <script> を書かず、wp_enqueue_scripts で登録します。

<?php
add_action( 'wp_enqueue_scripts', function () {
    if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
        wp_enqueue_script( 'comment-reply' );
    }
} );

手順4:1件ずつの表示をコールバックで作る

wp_list_comments() の callback に関数名を渡すと、各コメントの開始部分を自由に出力できます。閉じタグ(</li>)はWordPressが自動で出力するため、コールバック内では閉じません。ここを閉じてしまうと入れ子の返信でHTMLが崩れます。

<?php
function my_comment_item( $comment, $args, $depth ) {
    $rating = (int) get_comment_meta( $comment->comment_ID, 'my_rating', true );
    ?>
    <li id="comment-<?php comment_ID(); ?>" <?php comment_class( 'my-comment' ); ?>>
        <article class="my-comment__body">
            <header class="my-comment__meta">
                <?php echo get_avatar( $comment, $args['avatar_size'] ); ?>
                <span class="my-comment__author"><?php comment_author_link( $comment ); ?></span>
                <time datetime="<?php comment_time( 'c' ); ?>"><?php comment_date( '', $comment ); ?></time>
            </header>
            <?php if ( '0' === $comment->comment_approved ) : ?>
                <p class="my-comment__pending"><?php esc_html_e( 'コメントは承認待ちです。', 'my-theme' ); ?></p>
            <?php endif; ?>
            <?php if ( $rating >= 1 && $rating <= 5 ) : ?>
                <p class="my-comment__rating">
                    <?php echo esc_html( sprintf( '評価:%d / 5', $rating ) ); ?>
                </p>
            <?php endif; ?>
            <div class="my-comment__text"><?php comment_text(); ?></div>
            <?php
            comment_reply_link( array_merge( $args, array(
                'depth'     => $depth,
                'max_depth' => $args['max_depth'],
            ) ) );
            ?>
        </article>
    <?php
}

コメント本文は comment_text() で出力します。保存時にWordPressがHTMLを制限し、表示時にも標準のフィルターがかかるため、$comment->comment_content をそのまま echo するより安全です。独自項目は数値にキャストし、範囲を確認してから esc_html() で出しています。

さらに細かく制御したい場合は Walker_Comment を継承したクラスを walker 引数に渡す方法もありますが、多くのケースはコールバックで足ります。

手順5:フォームに独自項目を追加する

comment_form() の入力欄は comment_form_default_fields(未ログイン時の名前・メール・サイト欄)や comment_form_defaults(見出しや送信ボタンの文言など)で変更できます。ログインユーザーにも表示したい項目は comment_form_logged_in_after と comment_form_after_fields の両方に出すか、comment_form_field_comment で本文欄の前に足す方法が分かりやすいです。ここでは本文欄の前に評価を追加し、改ざん対策のnonceも入れます。

<?php
add_filter( 'comment_form_field_comment', function ( $field ) {
    if ( ! is_singular( 'review' ) ) {
        return $field;
    }
    $select  = '<p class="comment-form-rating"><label for="my_rating">評価</label>';
    $select .= '<select id="my_rating" name="my_rating" required>';
    $select .= '<option value="">選択してください</option>';
    for ( $i = 5; $i >= 1; $i-- ) {
        $select .= sprintf( '<option value="%1$d">%1$d</option>', $i );
    }
    $select .= '</select></p>';
    $select .= wp_nonce_field( 'my_rating_submit', 'my_rating_nonce', true, false );
    return $select . $field;
} );

手順6:送信時に検証し、コメントメタとして保存する

検証は、フォーム送信(wp-comments-post.php)のときだけ発火する pre_comment_on_post アクションで行います。preprocess_comment フィルターは管理画面からの返信やREST API経由の投稿でも通るため、そこでnonceを必須にすると管理者の返信まで止まってしまいます。

<?php
add_action( 'pre_comment_on_post', function ( $post_id ) {
    if ( 'review' !== get_post_type( $post_id ) ) {
        return;
    }
    $nonce = isset( $_POST['my_rating_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['my_rating_nonce'] ) ) : '';
    if ( ! wp_verify_nonce( $nonce, 'my_rating_submit' ) ) {
        wp_die( esc_html__( 'ページを再読み込みしてから送信してください。', 'my-theme' ), '', array( 'response' => 403, 'back_link' => true ) );
    }
    $rating = isset( $_POST['my_rating'] ) ? absint( $_POST['my_rating'] ) : 0;
    if ( $rating < 1 || $rating > 5 ) {
        wp_die( esc_html__( '評価を1〜5で選んでください。', 'my-theme' ), '', array( 'response' => 400, 'back_link' => true ) );
    }
} );

add_action( 'comment_post', function ( $comment_id ) {
    if ( ! isset( $_POST['my_rating'] ) ) {
        return; // 管理画面からの返信などフォーム以外の経路
    }
    $comment = get_comment( $comment_id );
    if ( ! $comment || 'review' !== get_post_type( $comment->comment_post_ID ) ) {
        return;
    }
    $rating = absint( $_POST['my_rating'] );
    if ( $rating >= 1 && $rating <= 5 ) {
        add_comment_meta( $comment_id, 'my_rating', $rating, true );
    }
} );

保存側でも値を再確認しているのは、検証フックを通らない経路があっても不正な値を残さないためです。

注意点として、ページキャッシュを使っているサイトでは、キャッシュされたページのnonceが有効期限を過ぎて送信エラーになることがあります。キャッシュプラグインでコメント付きページの保存期間を短くするか、nonceの代わりに別の対策を検討してください。

スパム対策と個人情報の扱い

独自システムにしても、スパム対策はWordPress標準の仕組み(「設定」→「ディスカッション」の承認設定や禁止語)と、Akismet などのスパム対策プラグインをそのまま使えます。標準の保存処理を残している利点です。

評価や追加項目にメールアドレス・電話番号などの個人情報を含める場合は、プライバシーポリシーへの記載が必要です。また、WordPressの個人データのエクスポート・消去機能(wp_privacy_personal_data_exporters などのフィルター)に対応させると、利用者からの開示・削除依頼に応えやすくなります。

ブロックテーマの場合

ブロックテーマでは comments.php は使われず、テンプレート内の「コメント」ブロックと、その中の子ブロック(コメントテンプレート、投稿コメントフォームなど)で表示が組み立てられます。見た目はサイトエディターで調整し、独自項目の保存(手順6)はそのまま流用できます。フォームへの項目追加は comment_form() 系のフックが効く場合が多いものの、ブロックの構成やバージョンによって挙動が異なることがあるため(2026年10月現在)、ステージングで表示を確認してください。

まとめ

  • comments_template() は「取得・テンプレート読み込み」を担い、comments_template フィルターで読み込むファイルを差し替えられる
  • 1件ずつの表示は wp_list_comments() の callback で作り、閉じタグは書かない
  • 独自項目は comment_form_field_comment などで追加し、pre_comment_on_post で検証、comment_post で add_comment_meta() に保存する
  • 入力はサニタイズ、出力はエスケープし、nonceを使うときはページキャッシュとの相性を確認する
  • 標準のスパム対策とプライバシー機能を活かせる構成にする

お気軽にご相談ください

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