はじめに
Wordpressでは記事詳細ページの下のほうに、コメント一覧を表示し、その下にコメント入力欄を表示する、というのが一般的だと思います。
ただ、クライアントの要望により、コメント一覧・投稿機能を記事詳細ページから分離したい、ということもあると思います。
想定してたよりも、だいぶ大変だったので、筆者が忘れないように、やり方を書き記します。
誰かベストプラクティス的な方法教えてけろ ('ω')
実装の背景
- 課題: 記事詳細ページで全コメントとCAPTCHA認証を毎回読み込むため、パフォーマンスに負荷
- 解決策: コメント一覧・投稿機能を専用ページに分離
- 効果: 記事詳細ページの読み込み速度向上
概要
1. 記事詳細ページの変更
- 記事詳細ページでは、コメントを表示するのみ。書き込みはしない
- 直近2件のコメントのみ表示する
- 「もっと見る」ボタンでコメント一覧ページへ遷移
- 「もっと見る」ボタンに、コメント総件数を括弧内に表示 例: もっと見る(28)
2. コメント一覧ページの新設
-
/comments/?post_id={記事ID}で、コメント一覧ページにアクセス - 全コメントをページング表示(20件/ページ)
- コメント一覧の下のほうに、コメント投稿機能を配置
実装手順
Step 1: 固定ページ「/comments/」の作成
WordPress管理画面で固定ページ「/comments/」を作成して公開します。
記事本文は何も入力しないでOKです。
記事タイトル: コメント一覧 ※このタイトルは使わないので識別できる名前なら何でもOK
URLスラッグ: comments
Step 2: コメント一覧ページテンプレートの作成
page-comments.php を作成し、コメント一覧表示と投稿機能を実装します。
HTMLテンプレートは皆さまの環境に合わせて適宜調整してください。
<?php
/**
* Name: page-comments.php
* Description: コメント一覧ページ
* URL: /comments/?post_id={記事ID}
*/
get_header();
// 記事IDを取得(セキュリティ強化)
$post_id = isset($_GET['post_id']) ? intval($_GET['post_id']) : 0;
// 入力値の妥当性チェック
if ($post_id && (!is_numeric($post_id) || $post_id <= 0)) {
wp_redirect(home_url());
exit;
}
// 記事を取得
$post = get_post($post_id);
// 記事が見つからない場合またはセキュリティチェック失敗時はホームページにリダイレクト
if (!$post || !in_array($post->post_status, ['publish', 'future']) || !$post_id) {
wp_redirect(home_url());
exit;
}
// コメント機能が有効かチェック
if (!comments_open($post_id)) {
wp_redirect(get_permalink($post_id));
exit;
}
// ページング設定
$comments_per_page = 20;
$current_page = get_query_var('cpage') ? max(1, intval(get_query_var('cpage'))) : 1;
// ページ番号の妥当性チェック
if ($current_page > 1000) {
wp_redirect(home_url());
exit;
}
$offset = ($current_page - 1) * $comments_per_page;
$total_comments = get_comment_total_count($post_id);
$max_pages = $total_comments > 0 ? (int) ceil($total_comments / $comments_per_page) : 1;
// コメントを取得
$comments = get_comments(array(
'post_id' => $post_id,
'status' => 'approve',
'number' => $comments_per_page,
'offset' => $offset,
'order' => 'DESC'
));
?>
<?php
// コメント投稿結果メッセージの表示
$comment_posted = isset($_GET['comment_posted']) ? intval($_GET['comment_posted']) : 0;
$comment_error = isset($_GET['comment_error']) ? intval($_GET['comment_error']) : 0;
?>
<?php if ($comment_posted === 1): ?>
<div class="comment-message comment-message-success" style="background-color: #d4edda; color: #155724; padding: 15px; margin: 20px 0; border: 1px solid #c3e6cb; border-radius: 4px;">
<strong>コメントを投稿いただき、ありがとうございます。</strong><br>
コメントは管理者が承認した後、公開されます。今しばらくお待ちくださいませ。
</div>
<?php elseif ($comment_error === 1): ?>
<div class="comment-message comment-message-error" style="background-color: #f8d7da; color: #721c24; padding: 15px; margin: 20px 0; border: 1px solid #f5c6cb; border-radius: 4px;">
<strong>コメント投稿に失敗しました</strong><br>
お手数ですが、再度お試しください。
</div>
<?php endif; ?>
<h2><a href="<?php echo esc_url(get_permalink($post_id)); ?>">
<?php echo esc_html(get_the_title($post_id)); ?>
</a>のコメント一覧</h2>
<h3>コメント一覧</h3>
<?php if ($comments): ?>
<ul class="comments-list">
<?php $args = array(
'avatar_size' => 55,
'reverse_top_level' => false,
'per_page' => -1,
);
wp_list_comments($args, $comments); ?>
</ul>
<!-- コメントページネーション -->
<?php render_comment_pagination($post_id, $current_page, $max_pages); ?>
<?php else: ?>
<p>まだこの記事にコメントはありません。</p>
<?php endif; ?>
<!-- コメント投稿フォーム -->
<h3>コメントを投稿する</h3>
<?php
$args = array(
'title_reply_before' => '<div class="respondform_title">',
'title_reply_after' => '</div>',
'label_submit' => '送信',
);
comment_form($args, $post_id);
?>
<!-- 記事に戻るリンク -->
<a href="<?php echo esc_url(get_permalink($post_id)); ?>">記事に戻る</a>
<?php get_footer(); ?>
Step 3: 記事詳細ページ用コメント表示の修正
comments.php を修正し、直近2件のコメントのみ表示するようにします。
<!-- 記事詳細ページ用のコメント一覧表示(comments.php) -->
<?php
//コメントが有効の場合
if (comments_open()):
$post_id = get_the_ID();
$comments_page_url = add_query_arg('post_id', $post_id, home_url('/comments/'));
$comment_count = get_comments_number(); // 承認済みコメントの総件数を取得
$comments_per_page = 2;
if($comment_count > 0){
$comment_button_text = 'コメントをもっと見る(' . $comment_count . ')';
} else{
$comment_button_text = 'コメントを投稿する';
}
// 直近2件のコメントを取得
$comments = get_comments(array(
'post_id' => $post_id,
'parent' => 0,
'status' => 'approve',
'number' => $comments_per_page,
'offset' => 0,
'order' => 'DESC',
'hierarchical' => false // 子コメント情報を含まない
));
?>
<h3>最新のコメント</h3>
<?php if ($comments()): ?>
<ul class="comments-list">
<?php $args = array(
'avatar_size' => 55,
'reverse_top_level' => false,
'per_page' => -1,
);
wp_list_comments($args, $comments); ?>
</ul>
<?php else: ?>
<p>まだこの記事にコメントはありません。</p>
<?php endif; ?>
<!-- コメントをもっと見るボタン -->
<a href="<?php echo esc_url($comments_page_url); ?>">
<?php echo $comment_button_text; ?>
</a>
<?php endif; ?>
Step 4: functions.php への機能追加
functions.php にコメント機能に関する関数を追加します。
ページネーションは the_comments_pagination() という、おあつらえ向きな関数が用意されていますが、
なーんか思った通りの挙動をしないので、自前でページネーションを実装しました。
/**
* コメント一覧ページのURL生成
*
* @param int $post_id 投稿ID
* @return string コメント一覧ページのURL
*/
function get_comment_page_url($post_id = null)
{
if (!$post_id) {
$post_id = get_the_ID();
}
// 投稿IDの妥当性チェック
if (!$post_id || !is_numeric($post_id) || $post_id <= 0) {
return home_url();
}
// フォールバック: クエリパラメータ形式
return home_url('/comments/?post_id=' . intval($post_id));
}
/**
* コメント総件数を取得する関数
*
* @param int $post_id 投稿ID
* @return int コメント総件数
*/
function get_comment_total_count($post_id = null)
{
// 投稿IDが指定されていない場合は現在の投稿IDを取得
if (!$post_id) {
$post_id = get_the_ID();
}
// コメント機能が有効かチェック
if (!comments_open($post_id)) {
return 0;
}
// 承認済みコメントの総件数を取得
$comment_count = get_comments_number($post_id);
return (int) $comment_count;
}
/**
* コメント一覧ページのページング表示
*
* @param int $post_id 投稿ID
* @param int $current_page 現在のページ
* @param int $max_pages 最大ページ数
* @return void
*/
function render_comment_pagination($post_id, $current_page, $max_pages)
{
try {
// パラメータの妥当性チェック
if (!is_numeric($post_id) || $post_id <= 0) {
return;
}
if (!is_numeric($current_page) || $current_page <= 0) {
$current_page = 1;
}
if (!is_numeric($max_pages) || $max_pages <= 0) {
return;
}
if ($max_pages <= 1) {
return; // ページングが不要
}
// ページネーションHTML構造に合わせて出力
echo '<nav class="pagination">';
echo '<div class="nav-links">';
// ページ番号(前後2件ずつ表示)
$range = 2;
$start = max(1, $current_page - $range);
$end = min($max_pages, $current_page + $range);
for ($i = $start; $i <= $end; $i++) {
if ($i == $current_page) {
echo '<span class="current page-numbers">' . esc_html($i) . '</span>';
}
else {
$page_url = add_query_arg(array(
'post_id' => intval($post_id),
'cpage' => intval($i)
), get_comment_page_url($post_id));
echo '<a class="page-numbers" href="' . esc_url($page_url) . '">' . esc_html($i) . '</a>';
}
}
echo '</div>';
// ナビゲーション矢印の出力
if ($current_page > 1) {
$first_url = add_query_arg(array(
'post_id' => intval($post_id),
'cpage' => 1
), get_comment_page_url($post_id));
echo '<a class="first page-arrows" href="' . esc_url($first_url) . '"></a>';
}
if ($current_page > 1) {
$prev_url = add_query_arg(array(
'post_id' => intval($post_id),
'cpage' => intval($current_page - 1)
), get_comment_page_url($post_id));
echo '<a class="prev page-arrows" href="' . esc_url($prev_url) . '"></a>';
}
if ($current_page < $max_pages) {
$next_url = add_query_arg(array(
'post_id' => intval($post_id),
'cpage' => intval($current_page + 1)
), get_comment_page_url($post_id));
echo '<a class="next page-arrows" href="' . esc_url($next_url) . '"></a>';
}
if ($current_page < $max_pages) {
$last_url = add_query_arg(array(
'post_id' => intval($post_id),
'cpage' => intval($max_pages)
), get_comment_page_url($post_id));
echo '<a class="last page-arrows" href="' . esc_url($last_url) . '"></a>';
}
echo '</nav>';
}
catch (Exception $e) {
error_log('render_comment_pagination error: ' . $e->getMessage());
// エラー時は何も表示しない
}
}
/**
* コメント返信リンクをコメント一覧ページに変更
*
* @param string $comment_reply_link デフォルトの返信リンクHTML
* @param array $args 引数配列
* @param WP_Comment $comment コメントオブジェクト
* @param WP_Post $post 投稿オブジェクト
* @return string 変更された返信リンクHTML
*/
function modify_comment_reply_link($comment_reply_link, $args, $comment, $post)
{
// コメント一覧ページのURLを生成
$comment_page_url = get_comment_page_url($post->ID);
// 返信先コメントIDを取得
$reply_to_comment_id = $comment->comment_ID;
// コメント一覧ページのURLに返信先パラメータを追加
$new_reply_url = add_query_arg('replytocom', $reply_to_comment_id, $comment_page_url);
$new_reply_url .= '#respond';
// 元のリンクテキストを保持しつつ、URLのみを変更
$link_text = $args['reply_text'] ?? '返信';
// コメント情報を取得
$comment_id = $comment->comment_ID;
$comment_author = $comment->comment_author;
$post_id = $post->ID;
// 返信先の表示名を生成
$reply_to_text = '返信';
// 必要な属性値を設定
$new_link = '<div class="reply"><a rel="nofollow" class="comment-reply-link" href="' . esc_url($new_reply_url) . '" data-commentid="' . esc_attr($comment_id) . '" data-postid="' . esc_attr($post_id) . '" data-belowelement="div-comment-' . esc_attr($comment_id) . '" data-respondelement="respond" data-replyto="' . esc_attr($reply_to_text) . '" aria-label="' . esc_attr($reply_to_text) . '">' . esc_html($link_text) . '</a></div>';
return $new_link;
}
add_filter('comment_reply_link', 'modify_comment_reply_link', 10, 4);
/**
* コメント投稿完了後のリダイレクト先を制御
* コメント一覧画面に戻す
*
* @param string $location デフォルトのリダイレクト先URL
* @return string カスタムリダイレクト先URL
*/
function redirect_after_comment($location)
{
// 投稿IDを取得
$post_id = isset($_POST['comment_post_ID']) ? intval($_POST['comment_post_ID']) : 0;
if ($post_id > 0) {
// コメント一覧ページのURLを生成
$comment_page_url = get_comment_page_url($post_id);
// 成功メッセージのクエリパラメータを追加
$comment_page_url = add_query_arg('comment_posted', '1', $comment_page_url);
return $comment_page_url;
}
return $location;
}
add_filter('comment_post_redirect', 'redirect_after_comment');
Step 5: SEO対策の実装
コメント一覧ページのSEO対策として、メタタグを適切に設定します。
「SEO SIMPLE PACK」というプラグインを使っているため、そのフィルターフックを使って設定します。
/**
* コメント一覧ページの対象記事を取得
*
* @return WP_Post|null 対象記事のオブジェクト、取得できない場合はnull
*/
function get_comments_page_target_post()
{
if (!is_page('comments')) {
return null;
}
$post_slug = get_query_var('post_slug');
$post_id = isset($_GET['post_id']) ? intval($_GET['post_id']) : 0;
// 記事を取得
$target_post = null;
if ($post_slug) {
$target_post = get_page_by_path($post_slug, OBJECT, 'post');
}
elseif ($post_id) {
$target_post = get_post($post_id);
}
// 記事が存在し、公開状態か公開予約状態の場合のみ返す
if ($target_post && in_array($target_post->post_status, ['publish', 'future'])) {
return $target_post;
}
return null;
}
/**
* コメント一覧ページのタイトルを設定
*/
function overwrite_ssp_title_comments($ssp_title)
{
$target_post = get_comments_page_target_post();
if ($target_post) {
$post_title = get_the_title($target_post->ID);
$site_name = get_bloginfo('name');
return $post_title . ' のコメント一覧 | ' . $site_name;
}
return $ssp_title;
}
add_filter('ssp_output_title', 'overwrite_ssp_title_comments');
/**
* コメント一覧ページのディスクリプションを設定
*/
function overwrite_ssp_description_comments($ssp_description)
{
$target_post = get_comments_page_target_post();
if ($target_post) {
$post_title = get_the_title($target_post->ID);
$post_excerpt = get_the_excerpt($target_post->ID);
// 記事の抜粋があればそれを使用、なければデフォルトメッセージ
if ($post_excerpt) {
return $post_excerpt;
}
else {
return $post_title . ' のコメント一覧ページです。読者の感想や意見を確認できます。';
}
}
return $ssp_description;
}
add_filter('ssp_output_description', 'overwrite_ssp_description_comments');
Step 6: コメントのエラーハンドリング
/**
* コメント投稿エラー時のリダイレクト処理
*
* @param WP_Error $error エラーオブジェクト
* @param array $comment_data コメントデータ
* @return void
*/
function handle_comment_error($error, $comment_data)
{
// エラーが発生した場合のみ処理
if (is_wp_error($error) && $error->get_error_code()) {
$post_id = isset($comment_data['comment_post_ID']) ? intval($comment_data['comment_post_ID']) : 0;
if ($post_id > 0) {
// コメント一覧ページのURLを生成
$comment_page_url = get_comment_page_url($post_id);
// エラーメッセージのクエリパラメータを追加
$comment_page_url = add_query_arg('comment_error', '1', $comment_page_url);
// リダイレクト
wp_redirect($comment_page_url);
exit;
}
}
}
add_action('comment_duplicate_trigger', 'handle_comment_error', 10, 2);
add_action('comment_flood_trigger', 'handle_comment_error', 10, 2);
/**
* コメント投稿処理全体のエラーハンドリング
*
* @param int|WP_Error $comment_id コメントIDまたはエラーオブジェクト
* @param array $comment_data コメントデータ
* @return void
*/
function handle_comment_submission_error($comment_id, $comment_data)
{
// エラーが発生した場合
if (is_wp_error($comment_id)) {
$post_id = isset($comment_data['comment_post_ID']) ? intval($comment_data['comment_post_ID']) : 0;
if ($post_id > 0) {
// コメント一覧ページのURLを生成
$comment_page_url = get_comment_page_url($post_id);
// エラーメッセージのクエリパラメータを追加
$comment_page_url = add_query_arg('comment_error', '1', $comment_page_url);
// リダイレクト
wp_redirect($comment_page_url);
exit;
}
}
}
add_action('wp_insert_comment', 'handle_comment_submission_error', 10, 2);
Step 7: コメントのタイムスタンプのリンクを削除する
コメント一覧ページを分離した影響か、タイムスタンプのリンクが /entry/{page_slug}/comment-page--2/#comment-2005 みたいな意味不明なリンクになっており、当然ながら正常にリンクしません。
このタイムスタンプのリンクを削除するために、wp_list_comments のコールバックを使用します。
/**
* カスタムコメント表示テンプレート
* タイムスタンプリンクを削除し、それ以外の部分はデフォルトの表示を維持
*/
function my_custom_comments($comment, $args, $depth)
{
$GLOBALS['comment'] = $comment;
?>
<li <?php comment_class(); ?> id="comment-<?php comment_ID(); ?>">
<div id="div-comment-<?php comment_ID(); ?>" class="comment-body">
<div class="comment-author vcard">
<?php
if ($args['avatar_size'] != 0) {
echo get_avatar($comment, $args['avatar_size']);
}
?>
<cite class="fn"><?php comment_author_link(); ?></cite>
<span class="says">より:</span>
</div>
<div class="comment-meta commentmetadata">
<a>
<?php
// リンクなしの日付と時間を表示
printf(
'%s %s',
get_comment_date('', $comment),
get_comment_time()
);
edit_comment_link(__('(Edit)'), ' ', '');
?>
</a>
</div>
<?php if ($comment->comment_approved == '0') : ?>
<em class="comment-awaiting-moderation"><?php _e('Your comment is awaiting moderation.'); ?></em>
<br />
<?php endif; ?>
<div class="comment-content">
<?php comment_text(); ?>
</div>
<div class="reply">
<?php
comment_reply_link(array_merge($args, array(
'reply_text' => "返信",
'depth' => $depth,
'max_depth' => $args['max_depth']
)));
?>
</div>
</div>
<?php
}
wp-content/themes/{theme_name}/comments.php
<?php $args = array(
...
'callback' => 'my_custom_comments', // カスタムコメント表示テンプレートを使用 functions.php に定義
);
wp_list_comments($args, $comments); ?>
wp-content/themes/{theme_name}/page-comments.php
<?php $args = array(
...
'callback' => 'my_custom_comments', // カスタムコメント表示テンプレートを使用 functions.php に定義
);
wp_list_comments($args, $comments); ?>
ここまでやれば、コメント投稿機能、一覧表示の分離ができます。
大変すぎワロタ。