テーマをCocoonに変更しました!
wordpress

【WordPress】ピンバックを記事下にカード表示する方法|Cocoon

FavoriteLoadingお気に入り登録
wordpress

ブログのサイドバーにある「最近のコメント」を見ていたら、コメントに混ざって他のブログからのピンバックがずらっと並んでいました。しかも、その中の1件はタイトルがプログラムのコードそのもの。どう見ても怪しいです。

そこで今回は、ピンバックの仕組みを確認するところから始めて、最終的にこんな形に整理しました。

場所表示するもの
記事下その記事に届いたピンバックだけを、相手のアイキャッチ付きカードで表示
記事下のフォームコメント欄は残して、承認制である案内文を表示
サイドバー「最近のコメント」に普通のコメントだけをカードで表示

途中でかなりハマったので、その原因もあわせて紹介します。

ピンバックとは?

ピンバックは、「あなたの記事にリンクしたよ」と相手のブログに自動で知らせる仕組みです。

流れはこんな感じです。

  1. Aさんが、自分のブログ記事にBさんの記事へのリンクを貼って公開する
  2. AさんのWordPressが、Bさんのサイトへ自動で通知を送る
  3. Bさん側で「本当にリンクされているか」を確認する
  4. OKなら、Bさんの記事のコメント欄に「ピンバック」として表示される

よく似たものにトラックバックがあります。

ピンバックとトラックバックは別物?

よく似たものに「トラックバック」があります。どちらも「あなたの記事にリンクしたよ」と知らせるものですが、目的は同じでも仕組みが違う別物です。

項目ピンバックトラックバック
送り方リンクを貼るだけで自動で送信相手の「トラックバックURL」を調べて手動で送信
通知の窓口(WordPress)xmlrpc.phpwp-trackback.php
リンクの確認本当にリンクがあるか確認する確認しない
送られる内容リンク元のタイトルとURLタイトル・URL・記事の抜粋
スパムの多さ少なめかなり多い

ざっくり言うと、トラックバックは昔からある仕組みで、手動で送れるうえにリンクの確認がないため、リンクしていなくても送り放題でした。そのせいでスパムの温床になってしまいました。

ピンバックは、その弱点を改良した後発の仕組みです。リンクを貼るだけで自動で送られ、受け取る側で本当にリンクがあるかを確認してくれます。

WordPressの「他のブログからの通知(ピンバック・トラックバック)を受け付ける」の設定は、両方まとめてオン・オフになります。この記事のコードも両方をまとめて扱っているので、トラックバックが届いた場合も、承認すれば記事下のカードに表示されます。

とはいえ、今はトラックバックを使う人はほとんどいないので、実際に届くのはほぼピンバックです。

変なタイトルのピンバックは、この「RSSを自動で記事化しているサイト」から来ていることが多いです。承認するときは、リンク元がちゃんと人が書いた記事かどうかを見て判断しましょう。

まずはWordPressの設定を確認

管理画面の「設定」から「ディスカッション」を開いて、次のようにしておきます。

項目設定理由
新しい投稿に対し他のブログからの通知(ピンバック・トラックバック)を受け付けるオンピンバックを受け取るため
コメントの手動承認を必須にするオン承認したものだけ表示するため
すでに承認されたコメントの投稿者のコメントを許可し、それ以外のコメントを承認待ちにするオフ一度承認した相手からのものも、毎回自分で確認するため

私の場合、3つ目にチェックが入っていたので、過去に承認した相手からのピンバックが確認なしで表示されていた可能性がありました。怪しかったピンバックは削除しておきました。

次に怪しいものが来たときは、「削除」ではなく「スパム」にしておくのがおすすめです。Akismetを使っていれば、同じ相手からのものを次から自動で弾きやすくなります。

SiteGuardの「XML-RPC防御」は無効のままでOK

SiteGuard WP Pluginには「XML-RPC防御」という設定があります。ここを有効にすると、「ピンバック無効化」「XML-RPC無効化」のどちらを選んでもピンバックが届かなくなります

ピンバックを使いたい場合は、無効のまま触らないのが正解です。画面の下で「ピンバック無効化」に丸が付いていても、機能自体が無効なら何もブロックされません。

やりたいことを実現するコード

ここからは、子テーマの functions.php と style.css に追加していきます。作業前に、必ず両方のファイルをバックアップしてください。

手順1:functions.phpに追加

子テーマの functions.php の一番最後に、次のコードをまるごと貼り付けます。先頭に「<?php」は付けません。

このコードでやっていることは次のとおりです。

番号内容
画像取得相手ページのアイキャッチ(og:image)を取得して保存。承認時か初回表示時に1回だけ取りに行き、失敗したら1日後に取り直す
1記事下のコメント一覧を非表示にする
2コメントフォームの直前に、その記事宛てのピンバックだけをカード表示
3フォームの上に案内文と、送信直後の「受け付けました」メッセージを表示
4コメント送信後、フォームの位置に戻す
5「最近のコメント」ウィジェットを普通のコメントだけにする
/* ========================================
   ピンバック・コメント表示切り分け
======================================== */

// 相手ページからアイキャッチ(og:image)を取得
if ( ! function_exists( 'hiro_fetch_og_image' ) ) {
    function hiro_fetch_og_image( $url ) {
        $scheme = strtolower( (string) wp_parse_url( $url, PHP_URL_SCHEME ) );
        if ( empty( $url ) || ! in_array( $scheme, array( 'http', 'https' ), true ) ) {
            return '';
        }
        // https を先に試し、ダメなら元のURLで再挑戦
        $urls = array_unique( array( set_url_scheme( $url, 'https' ), $url ) );
        $html = '';
        foreach ( $urls as $try ) {
            $res = wp_remote_get( $try, array(
                'timeout'             => 8,
                'redirection'         => 5,
                'limit_response_size' => 300000,
                'user-agent'          => 'Mozilla/5.0 (compatible; HiroPingImage)',
            ) );
            if ( ! is_wp_error( $res ) && 200 === (int) wp_remote_retrieve_response_code( $res ) ) {
                $html = wp_remote_retrieve_body( $res );
                if ( '' !== $html ) {
                    break;
                }
            }
        }
        if ( '' === $html ) {
            return '';
        }
        $patterns = array(
            '/<meta[^>]+(?:property|name)=["\']og:image["\'][^>]*content=["\']([^"\']+)["\']/i',
            '/<meta[^>]+content=["\']([^"\']+)["\'][^>]*(?:property|name)=["\']og:image["\']/i',
            '/<meta[^>]+name=["\']twitter:image["\'][^>]*content=["\']([^"\']+)["\']/i',
        );
        $img = '';
        foreach ( $patterns as $pattern ) {
            if ( preg_match( $pattern, $html, $m ) ) {
                $img = $m[1];
                break;
            }
        }
        if ( '' === $img ) {
            return '';
        }
        $img = html_entity_decode( $img, ENT_QUOTES );
        if ( 0 === strpos( $img, '//' ) ) {
            $img = 'https:' . $img;
        } elseif ( 0 === strpos( $img, '/' ) ) {
            $host = wp_parse_url( $url, PHP_URL_HOST );
            $img  = 'https://' . $host . $img;
        }
        return esc_url_raw( set_url_scheme( $img, 'https' ) );
    }
}

// 保存済みの画像URLを返す(失敗時は1日たったら取り直す)
if ( ! function_exists( 'hiro_get_ping_image' ) ) {
    function hiro_get_ping_image( $comment ) {
        $cid  = (int) $comment->comment_ID;
        $key  = '_hiro_ping_img';
        $meta = (string) get_comment_meta( $cid, $key, true );
        if ( '' !== $meta ) {
            if ( 0 !== strpos( $meta, 'none:' ) ) {
                return $meta;
            }
            $failed_at = (int) substr( $meta, 5 );
            if ( time() - $failed_at < DAY_IN_SECONDS ) {
                return '';
            }
        }
        $img = hiro_fetch_og_image( $comment->comment_author_url );
        update_comment_meta( $cid, $key, $img ? $img : 'none:' . time() );
        return $img;
    }
}

// ピンバックを承認したタイミングで画像を取得しておく
add_action( 'transition_comment_status', function( $new_status, $old_status, $comment ) {
    if ( 'approved' === $new_status && in_array( $comment->comment_type, array( 'pingback', 'trackback' ), true ) ) {
        hiro_get_ping_image( $comment );
    }
}, 10, 3 );

// 1. 記事下のコメント一覧を非表示にする(コメント・ピンバックとも)
add_filter( 'comments_array', function( $comments ) {
    if ( is_admin() ) {
        return $comments;
    }
    return array();
} );

// 2. コメントフォームの直前に、その記事の承認済みピンバックをカード表示
add_action( 'comment_form_before', function() {
    if ( ! is_singular() ) {
        return;
    }
    $post_id = (int) get_queried_object_id();
    if ( $post_id <= 0 ) {
        return;
    }
    $pings = get_comments( array(
        'post_id' => $post_id,
        'status'  => 'approve',
        'type'    => 'pings',
        'order'   => 'ASC',
    ) );
    if ( empty( $pings ) ) {
        return;
    }
    echo '<div class="hiro-pings">';
    echo '<h3 class="hiro-pings-title">この記事へのピンバック</h3>';
    echo '<ul class="hiro-pings-list">';
    foreach ( $pings as $p ) {
        $url   = $p->comment_author_url;
        $name  = $p->comment_author;
        $title = $name;
        $site  = '';
        $pos   = strrpos( $name, ' | ' );
        if ( false !== $pos ) {
            $title = substr( $name, 0, $pos );
            $site  = substr( $name, $pos + 3 );
        }
        if ( '' === $site ) {
            $site = (string) wp_parse_url( $url, PHP_URL_HOST );
        }
        $img = hiro_get_ping_image( $p );

        echo '<li class="hiro-ping">';
        echo '<a class="hiro-ping-link" href="' . esc_url( $url ) . '" target="_blank" rel="nofollow noopener">';
        echo '<div class="hiro-ping-body">';
        echo '<span class="hiro-ping-site">' . esc_html( $site ) . '</span>';
        echo '<span class="hiro-ping-title">' . esc_html( $title ) . '</span>';
        echo '<span class="hiro-ping-date">' . esc_html( get_comment_date( 'Y.m.d', $p ) ) . '</span>';
        echo '</div>';
        if ( $img ) {
            echo '<div class="hiro-ping-thumb"><img src="' . esc_url( $img ) . '" alt="" width="160" height="90" loading="lazy" decoding="async"></div>';
        }
        echo '</a></li>';
    }
    echo '</ul></div>';
} );

// 3. フォーム内の先頭に「送信完了メッセージ」と「常時の案内文」を表示
add_action( 'comment_form_top', function() {
    if ( isset( $_GET['comment_sent'] ) ) {
        echo '<p class="hiro-comment-sent" style="padding:10px;border:1px solid #4caf50;background:#e8f5e9;font-weight:bold;">コメントを受け付けました。承認後に表示されます。</p>';
    }
    echo '<p class="hiro-comment-note" style="font-size:0.9em;">コメントは承認後、サイドバーの「最近のコメント」に表示されます。</p>';
} );

// 4. コメント送信後、フォーム位置に戻して完了メッセージを出す
add_filter( 'comment_post_redirect', function( $location, $comment ) {
    $location = preg_replace( '/#.*$/', '', $location );
    return add_query_arg( 'comment_sent', '1', $location ) . '#respond';
}, 10, 2 );

// 5. 「最近のコメント」ウィジェットは通常コメントだけにする
add_filter( 'widget_comments_args', function( $args ) {
    $args['type'] = 'comment';
    return $args;
} );

手順2:style.cssに追加

子テーマの style.css の一番最後に、次のCSSを追加します。前半が記事下のピンバックカード、後半がサイドバーの「最近のコメント」です。

/* ピンバック欄 */
.hiro-pings {
  margin: 1.5em 0 2em;
}
.hiro-pings-title {
  font-size: 1.1em;
  margin: 0 0 0.8em;
  padding: 0.4em 0.8em;
  border-left: 5px solid #1e88e5;
  background: #f5f8fc;
}
.hiro-pings-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.hiro-ping {
  margin: 0 0 0.8em;
  padding: 0;
}
.hiro-ping-link {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  border: 1px solid #e3e8ef;
  border-radius: 8px;
  background: #fff;
  color: #333;
  text-decoration: none;
  transition: box-shadow 0.2s, transform 0.2s;
}
.hiro-ping-link:hover {
  color: #333;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
  transform: translateY(-2px);
}
.hiro-ping-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.hiro-ping-site {
  font-size: 0.75em;
  font-weight: bold;
  color: #1e88e5;
}
.hiro-ping-title {
  font-size: 0.95em;
  font-weight: bold;
  line-height: 1.5;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.hiro-ping-date {
  font-size: 0.75em;
  color: #888;
}
.hiro-ping-thumb {
  flex: 0 0 160px;
}
.hiro-ping-thumb img {
  display: block;
  width: 160px;
  height: 90px;
  object-fit: cover;
  border-radius: 6px;
}
@media (max-width: 480px) {
  .hiro-ping-thumb {
    flex-basis: 100px;
  }
  .hiro-ping-thumb img {
    width: 100px;
    height: 56px;
  }
}

/* サイドバー:最近のコメント */
.widget_recent_comments ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
.widget_recent_comments ul li.recentcomments {
  position: relative;
  margin: 0 0 10px;
  padding: 10px 12px 10px 42px;
  border: 1px solid #e3e8ef;
  border-left: 4px solid #ec6f95;
  border-radius: 6px;
  background: #fff;
  font-size: 0.8em;
  line-height: 1.6;
  color: #888;
  transition: box-shadow 0.2s;
}
.widget_recent_comments ul li.recentcomments::before {
  content: "";
  position: absolute;
  top: 12px;
  left: 12px;
  width: 20px;
  height: 20px;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ec6f95' d='M4 3h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H10l-5 4v-4H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z'/%3E%3Ccircle cx='8' cy='10' r='1.3' fill='%23fff'/%3E%3Ccircle cx='12' cy='10' r='1.3' fill='%23fff'/%3E%3Ccircle cx='16' cy='10' r='1.3' fill='%23fff'/%3E%3C/svg%3E") no-repeat center / contain;
}
.widget_recent_comments ul li.recentcomments:hover {
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
}
.widget_recent_comments ul li.recentcomments > a {
  display: block;
  margin: 0 0 4px;
  padding: 0;
  border: none;
  background: none;
  font-size: 1.1em;
  font-weight: bold;
  line-height: 1.5;
  color: #333;
  text-decoration: none;
}
.widget_recent_comments ul li.recentcomments > a:hover {
  color: #ec6f95;
}
.widget_recent_comments ul li.recentcomments .comment-author-link,
.widget_recent_comments ul li.recentcomments .comment-author-link a {
  display: inline;
  margin: 0;
  padding: 0;
  font-size: 1em;
  font-weight: bold;
  color: #ec6f95;
  text-decoration: none;
}

/* 最近のコメントの見出しだけ縦線を消す */
.widget_recent_comments .widget-title,
.widget_recent_comments .widget-sidebar-title {
  border-left: none;
}
.widget_recent_comments .widget-title::before,
.widget_recent_comments .widget-sidebar-title::before {
  content: none;
}

サイドバーのアイコンは画像ファイルを使わず、CSSの中に吹き出しの形を直接描いています。軽いうえに、記号の文字化けの心配もありません。

手順3:「最近のコメント」ウィジェットをサイドバーに置く

「外観」から「ウィジェット」を開いて、「最近のコメント」がサイドバーに入っているかを確認します。ここが今回いちばんハマったところなので、次の章で詳しく説明します。

最後に、キャッシュを削除してから Ctrl + F5 で表示を確認すれば完成です。

実際にハマったところ

ピンバックが全部の記事に表示される

コードを入れたあと、なぜかどの記事にも同じピンバックが表示されていました。記事IDの取り方を変えても直らず、古いテンプレートファイルが残っていないかも調べましたが、single.php も comments.php もありません。

原因は、「最近のコメント」ウィジェットがサイドバーではなく、記事下のウィジェットエリアに置かれていたことでした。このウィジェットはサイト全体の新着を表示するので、どの記事でも同じ内容になります。それが「全記事にピンバックが出ている」ように見えていたわけです。

記事下のウィジェットエリアから削除して、サイドバーに移したら一発で解決しました。

Jetpackのコメント機能とぶつかる

記事下のフォームが、WordPress標準ではなくJetpackのコメントフォームになっていました。Jetpackはフォーム部分をJetpackのサーバーから読み込む枠に丸ごと差し替えるため、今回のコードと相性が悪いです。

今回の機能Jetpackのコメントがオンだと
記事下のピンバック表示表示されない、または位置がズレる
フォーム上の案内文Jetpackの枠に置き換わって消える
送信後のメッセージ送信がJetpack経由になるので出ない

「Jetpack」の「設定」にある「ディスカッション」から、コメントの項目をオフにしました。オフにしても過去のコメントやピンバックは消えません。フォーム下の「新しい投稿をメールで受け取る」などのチェックはJetpackの購読機能なので、そのまま残ります。

相手のアイキャッチが取れない

最初のコードでは、画像を取りに行く前にWordPressの関数でURLの安全チェックをしていました。ところが、このチェックは同じサーバー内にあるサイトへのアクセスを弾くことがあります。私の場合、ピンバック元が同じVPSの別サイトだったので、画像が取れていませんでした。

しかも最初のコードは「取れなかった」と一度保存すると二度と取りに行かない作りだったので、次の2点を直しました。

  • URLのチェックを「http か https か」だけにする
  • 失敗しても、1日たったら自動で取り直す

サイドバーのCSSが効かない

最初は「最近のコメント」のリストに付くidを使ってCSSを書いていましたが、反映されませんでした。ウィジェットの設置状況によってidが変わることがあるので、idに頼らずクラス名だけで指定する形に書き直したら反映されました。

見出しの縦線を消したら下線も消えた

Cocoonのスキンで、サイドバーの見出しに付いていた青い縦線を「最近のコメント」だけ消しました。ところが、文字の下にあったグレーの短い線まで一緒に消えました。スキンによって飾りの作り方が違うためです。私はスッキリしたのでそのままにしましたが、気になる場合はブラウザの「検証」で見出しのCSSを確認して調整してください。

まとめ

やったことポイント
ディスカッション設定の見直し手動承認をオン、承認済み投稿者の自動許可はオフ
SiteGuardのXML-RPC防御ピンバックを使うなら無効のまま
記事下にピンバックをカード表示相手のアイキャッチは1回だけ取得して保存するので軽い
コメントはサイドバーだけフォームには承認制の案内文を表示
Jetpackのコメント機能今回のカスタマイズとぶつかるのでオフ
ウィジェットの置き場所記事下に置くと、全記事に同じ内容が出る

コードの問題だと思い込んでいたら、原因はウィジェットの置き場所でした。表示がおかしいときは、まず「それがどこから出ているのか」を確かめるのが近道だと実感しました。

ピンバックは承認制にしておけば、怪しいサイトからのリンクを表に出さずに済みます。同じように「最近のコメント」がピンバックだらけになっている方の参考になればうれしいです。

気になる中古パソコン

パソコン

Supported by Rakuten Web Service

コメント

📖目次
タイトルとURLをコピーしました
PR

当サイトは、Amazon、楽天アソシエイト・他プログラムの参加者です。リンクを通じて商品を購入すると、紹介料を得る場合があります。

⚠ 免責事項

本記事は、筆者が実際に行った作業を記録したものです。サーバーの環境や設定によっては、同じ手順でも結果が異なる場合があります。

作業を行う際は、事前にバックアップを取ったうえで、ご自身の責任で実施してください。本記事の内容を参考にしたことによって生じたトラブルや損害について、当サイトは一切の責任を負いかねますので、あらかじめご了承ください。