Cocoon 内部ブログカード表示でのクエリリセット対策

Cocoon

フロントページで新着投稿を全文表示テンプレートを使用した際に、新着投稿内で内部ブログカード表示を行うと、カスタムテンプレート内で上書きしたクエリがリセットされる現象があったのでその対策のご紹介です。

スポンサーリンク

現象

下図が不具合時の再現イメージです。

フロントページで新着投稿を全文表示テンプレートを使用した際に、新着投稿内で内部ブログカード表示を行うと、2件目以降の投稿がリスト化されず、固定ページがリストに表示されてしまいます。

本来あるはずのページネーションも表示されていません。

対策

子テーマのスキンフォルダ下の function.php にて、lib/blogcard-in.php 内の2つの関数をオーバーロードします。

  • url_to_internal_blogcard_tag()
  • url_to_internal_blogcard()

ファイル場所

  • <子テーマフォルダ>
    • skins
      • <使用スキンフォルダ>
        • functions.php

ファイル編集

上記の functions.php の末尾に以下のコードを追加してください。

//内部URLからブログをカードタグの取得
if ( !function_exists( 'url_to_internal_blogcard_tag' ) ):
function url_to_internal_blogcard_tag($url){
  if ( !$url ) return;
  $url = strip_tags($url);//URL
  $id = url_to_postid( $url );//IDを取得(URLから投稿ID変換)
  //内部ブログカード作成可能なURLかどうか
  if ( !is_internal_blogcard_url($url) ) return;
  //_v($url);

  $no_image = get_no_image_160x90_url();
  if (!$no_image) {
    $no_image = get_site_screenshot_url($url);
  }

  $thumbnail = null;
  $date_tag = null;
  //投稿・固定ページの場合
  if ($id) {
    //global $post;
    $post_data = get_post($id);
    setup_postdata($post_data);
    // wp_reset_query();	/* del */

    $title = $post_data->post_title;//タイトルの取得

    //メタディスクリプションの取得
    $snippet = get_the_page_meta_description($id);

    //投稿管理画面の抜粋を取得
    if (!$snippet) {
      $snippet = $post_data->post_excerpt;
    }
    //All in One SEO Packのメタディスクリプションを取得
    if (!$snippet) {
      $snippet = get_the_all_in_one_seo_pack_meta_description($id);
    }
    //記事本文の抜粋文を取得
    if (!$snippet) {
      $snippet = get_content_excerpt($post_data->post_content, get_entry_card_excerpt_max_length());
    }
    $snippet = preg_replace('/\n/', '', $snippet);

    //日付表示
    $date = null;
    $post_date = mysql2date(get_site_date_format(), $post_data->post_date);
    switch (get_internal_blogcard_date_type()) {
      case 'post_date':
        $date = $post_date;
        break;
      case 'up_date':
        $date = mysql2date(get_site_date_format(), $post_data->post_modified);
        if (!$date) {
          $date = $post_date;
        }
        break;
    }
    if (is_internal_blogcard_date_visible()) {
      $date = '<div class="blogcard-post-date internal-blogcard-post-date">'.$date.'</div>';//日付の取得
      $date_tag = '<div class="blogcard-date internal-blogcard-date">'.$date.'</div>';
    }


    //サムネイルの取得(要160×90のサムネイル設定)
    $thumbnail = get_the_post_thumbnail($id, get_internal_blogcard_thumbnail_size(), array('class' => 'blogcard-thumb-image internal-blogcard-thumb-image', 'alt' => ''));

  } elseif (is_home_url($url)){
    //トップページの場合
    $title = get_front_page_title_caption();
    $snippet = get_front_page_meta_description();
    $image = get_ogp_home_image_url();
    if (!empty($image)) {
      $thumbnail = get_blogcard_thumbnail_image_tag($image);
    }
  } elseif ($cat = get_category_by_path($url, false)){
    //カテゴリページの場合
    $cat_id = $cat->cat_ID;

    $title = get_the_category_title($cat_id);
    $snippet = get_the_category_snippet($cat_id);
    $image = get_the_category_eye_catch_url($cat_id);

    if ($image) {
      $thumbnail = get_blogcard_thumbnail_image_tag($image);
    }
  } elseif ($tag = url_to_tag_object($url)) {
    $tag_id = $tag->term_id;
    $title = get_the_tag_title($tag_id);
    $snippet = get_the_tag_snippet($tag_id);
    $image = get_the_tag_eye_catch_url($tag_id);

    if ($image) {
      $thumbnail = get_blogcard_thumbnail_image_tag($image);
    }
  }
  //タイトルのフック
  $title = apply_filters('cocoon_blogcard_title',$title);
  $title = apply_filters('cocoon_internal_blogcard_title',$title);
  //スニペットのフック
  $snippet = apply_filters( 'cocoon_blogcard_snippet', $snippet );
  $snippet = apply_filters( 'cocoon_internal_blogcard_snippet', $snippet );

  //サムネイルが存在しない場合
  if ( !$thumbnail ) {
    $thumbnail = get_blogcard_thumbnail_image_tag($no_image);
  }

  //ブログカードのサムネイルを右側に
  $additional_class = get_additional_internal_blogcard_classes();

  //新しいタブで開く場合
  $target = is_internal_blogcard_target_blank() ? ' target="_blank"' : '';

  //ファビコン
  $favicon_tag =
  '<div class="blogcard-favicon internal-blogcard-favicon">'.
    '<img src="//www.google.com/s2/favicons?domain='.get_the_site_domain().'" class="blogcard-favicon-image internal-blogcard-favicon-image" alt="" width="16" height="16" />'.
  '</div>';

  //サイトロゴ
  $domain = get_domain_name(punycode_decode($url));
  $site_logo_tag = '<div class="blogcard-domain internal-blogcard-domain">'.$domain.'</div>';
  $site_logo_tag = '<div class="blogcard-site internal-blogcard-site">'.$favicon_tag.$site_logo_tag.'</div>';

  //取得した情報からブログカードのHTMLタグを作成
  //_v($url);
  $tag =
  '<a href="'.$url.'" title="'.esc_attr($title).'" class="blogcard-wrap internal-blogcard-wrap a-wrap cf"'.$target.'>'.
    '<div class="blogcard internal-blogcard'.$additional_class.' cf">'.
      '<div class="blogcard-label internal-blogcard-label">'.
        '<span class="fa"></span>'.
      '</div>'.
      '<figure class="blogcard-thumbnail internal-blogcard-thumbnail">'.$thumbnail.'</figure>'.
      '<div class="blogcard-content internal-blogcard-content">'.
        '<div class="blogcard-title internal-blogcard-title">'.$title.'</div>'.
        '<div class="blogcard-snippet internal-blogcard-snippet">'.$snippet.'</div>'.

      '</div>'.
      '<div class="blogcard-footer internal-blogcard-footer cf">'.
        $site_logo_tag.$date_tag.
      '</div>'.
    '</div>'.
  '</a>';

  return $tag;
}
endif;

//本文中のURLをブログカードタグに変更する
if ( !function_exists( 'url_to_internal_blogcard' ) ):
function url_to_internal_blogcard($the_content) {
  // //ブロックエディターのブログカード用の本文整形
  // $the_content = fix_blogcard_content($the_content);
  //_v($the_content);
  $res = preg_match_all('/^(<p>)?(<a[^>]+?>)?https?:\/\/'.preg_quote(get_the_site_domain()).'(\/)?([-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+)?(<\/a>)?(<\/p>)?/im', $the_content,$m);
  //_v($m);
  foreach ($m[0] as $match) {

    //マッチしたpタグが適正でないときはブログカード化しない
    if ( !is_p_tag_appropriate($match) ) {
      continue;
    }

    $url = strip_tags($match);//URL

    //wpForoのブログカードは外部ブログカードに任せる
    if (includes_wpforo_url($url)) {
      continue;
    }

    $tag = url_to_internal_blogcard_tag($url);


    if ( !$tag ) continue;//IDを取得できない場合はループを飛ばす
    //本文中のURLをブログカードタグで置換
    $the_content = preg_replace('{^'.preg_quote($match, '{}').'}im', $tag , $the_content, 1);
    // wp_reset_postdata();	/* del */
    
  }

  return $the_content;//置換後のコンテンツを返す
}
endif;

オーバーロード元からの変更箇所は以下の2箇所です。
関数コールをコメントアウトしています。

    // wp_reset_query();	/* del */
    // wp_reset_postdata();	/* del */

完成!

以下は、原因解析過程や対策検討の備忘録です。

原因

原因仮定

2件目以降の投稿の代わりに固定ページが表示され、ページネーションも表示できていないことから、新着投稿を表示する最中にクエリが通常投稿から固定ページに戻ってしまっているのが原因だと考えられます。

原因箇所特定

全ての投稿で現象が発生するわけではないので、発生している投稿を細分化して問題箇所を探していきます。

投稿内に「続きを読む」ブロックを挿入し、
挿入位置を移動させ投稿を更新 → 問題が発生するか確認、
を繰り返すのが編集も少なくて手っ取り早いかと思います。

「続きを読む」ブロックを後ろに移動させていくと、内部ブログカード前までは問題ないのに、後ろにすると現象が発生することがわかりました。
ちなみに、外部ブログカードは表示しても問題ありませんでした。

ブログカード表示は Cocoon が提供してくれている機能で、Cocoon 設定から On/Off が簡単に切り替えられます。
もし、問題が内部ブログカード表示なら Off にすれば現象は改善されるはずなので、Cocoon 設定 > ブログカード > 内部ブログカード設定 の「ブログカード表示を有効にする」のチェックを外してみたところ、現象が起きなくなりました。

Cocoon の内部ブログカード表示が、原因箇所のようです。

コードの確認

内部ブログカード表示コードがどのファイルにあるかを探すため、設定画面から辿っていくことにしました。

Cocoon 設定 > ブログカードの html ソースを確認します。
内部カード表示付近に

<label for="internal_blogcard_enable">ブログカード表示</label>

という記述があるので、cocoon-master フォルダ配下を「internal_blogcard_enable」でgrep します。

結果、lib\blogcard-in.php と lib\page-settings\blogcard-in-forms.php で見つかり、lib\page-settings\blogcard-in-forms.php の方は Cocoon 設定画面っぽいので無視し、lib\blogcard-in.php の方を確認します。

lib\blogcard-in.php
… 省略 …
if ( is_internal_blogcard_enable() ) {
  add_filter('the_content', 'url_to_internal_blogcard', 11);
  add_filter('widget_text', 'url_to_internal_blogcard', 11);
  add_filter('widget_text_pc_text', 'url_to_internal_blogcard', 11);
  //add_filter('widget_classic_text', 'url_to_internal_blogcard', 11);
  add_filter('widget_text_mobile_text', 'url_to_internal_blogcard', 11);
  add_filter('the_category_tag_content', 'url_to_internal_blogcard', 11);
  //コメント内ブログカード
  if (is_comment_internal_blogcard_enable()) {
    add_filter('comment_text', 'url_to_internal_blogcard' ,11);
  }
}
… 省略 …

全文表示テンプレートで使っている「the_content」に「url_to_internal_blogcard」が追加されていることから、url_to_internal_blogcard() が件の関数のようです。

lib\blogcard-in.php
… 省略 …
function url_to_internal_blogcard($the_content) {
  // //ブロックエディターのブログカード用の本文整形
  // $the_content = fix_blogcard_content($the_content);
  //_v($the_content);
  $res = preg_match_all('/^(<p>)?(<a[^>]+?>)?https?:\/\/'.preg_quote(get_the_site_domain()).'(\/)?([-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+)?(<\/a>)?(<\/p>)?/im', $the_content,$m);
  //_v($m);
  foreach ($m[0] as $match) {

    //マッチしたpタグが適正でないときはブログカード化しない
    if ( !is_p_tag_appropriate($match) ) {
      continue;
    }

    $url = strip_tags($match);//URL

    //wpForoのブログカードは外部ブログカードに任せる
    if (includes_wpforo_url($url)) {
      continue;
    }

    $tag = url_to_internal_blogcard_tag($url);


    if ( !$tag ) continue;//IDを取得できない場合はループを飛ばす
    //本文中のURLをブログカードタグで置換
    $the_content = preg_replace('{^'.preg_quote($match, '{}').'}im', $tag , $the_content, 1);
    wp_reset_postdata();

  }

  return $the_content;//置換後のコンテンツを返す
}
… 省略 …

さーっと流し見したところ、ありました!
なんかリセットしてるっぽい関数!

    wp_reset_postdata();

この関数は、new WP_Query を使って二番目のクエリを実行した後に、メインクエリの $post グローバル変数を復元するために使用します。つまり $post がメインクエリの現在の投稿になります。

https://wpdocs.osdn.jp/%E9%96%A2%E6%95%B0%E3%83%AA%E3%83%95%E3%82%A1%E3%83%AC%E3%83%B3%E3%82%B9/wp_reset_postdata

んんー、なんだかそれっぽい?

さらに、url_to_internal_blogcard() がコールしている関数 url_to_internal_blogcard_tag() も見てみます。

lib\blogcard-in.php
… 省略 …
function url_to_internal_blogcard_tag($url){
  if ( !$url ) return;
  $url = strip_tags($url);//URL
  $id = url_to_postid( $url );//IDを取得(URLから投稿ID変換)
  //内部ブログカード作成可能なURLかどうか
  if ( !is_internal_blogcard_url($url) ) return;
  //_v($url);

  $no_image = get_no_image_160x90_url();
  if (!$no_image) {
    $no_image = get_site_screenshot_url($url);
  }

  $thumbnail = null;
  $date_tag = null;
  //投稿・固定ページの場合
  if ($id) {
    //global $post;
    $post_data = get_post($id);
    setup_postdata($post_data);
    wp_reset_query();
… 省略 …

ここにもなんかリセットしてそうなのがいる!

    wp_reset_query();

wp_reset_query() は $wp_query とグローバルな投稿データを元のメインクエリのものに復帰させます。query_posts() をどうしても使わなければならない場合、その後にこの関数を呼び出すべきです。

重要:下記の例で説明するように、pre_get_posts アクションの使用を強く推奨します。これはクエリが実行される前にクエリのパラメータを変更することができます。

WP_Query や get_posts() の使用後に wp_reset_query() を呼び出す必要はありません。これらはメインクエリのオブジェクトを変更しないからですが、代わりに wp_reset_postdata() を呼び出してください。

https://wpdocs.osdn.jp/%E9%96%A2%E6%95%B0%E3%83%AA%E3%83%95%E3%82%A1%E3%83%AC%E3%83%B3%E3%82%B9/wp_reset_query

さっきのも怪しいですが、こっちのがズバリの原因っぽいですね。

対策検討

url_to_internal_blogcard_tag() 関数の wp_reset_query() と、url_to_internal_blogcard() 関数の wp_reset_postdata() が原因っぽいのはわかりましたが、これを削除してしまってよいかが問題です。

WordPress Codex を読む限り、不要と判断し消すことにしました。

wp_reset_query()

query_posts() と対になる関数のようですので、query_posts() を使っていないため不要と判断します。

wp_reset_postdata()

WP_Query や get_posts() と対になる関数のようですので、これらを使っていないため不要と判断します。

get_posts() とそっくりな get_post() は使われていますが、get_post() には内部的に WP_Query を使うような記載はないため、2回目以降のクエリによる $post グローバル変数の変更を復元するための本関数はたぶん不要なはず…

setup_postdata() も、WordPress Codex に「$post グローバル変数をセットしません」とあるため、$post グローバル変数を復元するための本関数は不要なはず…

ファイル修正方法

wp_reset_query()、wp_reset_postdata() を削除する方針は決まりましたが、親テーマの lib ファイルをいじるのは気が引けます…

と思っていたら、Cocoon はテンプレートだけじゃなく、lib 内のファイルも子テーマで上書きできてしまうようです。
あらー、便利です!

「lib」フォルダにあるファイルを子テーマで編集したいのですが・・・
お世話になります。phpの編集になりますが質問させてください。 親テーマ「lib」フォルダの中の「blogcard-in.php」などを編集したく、子テーマに「lib」フォルダを作り「blogcard-in.php」をコピーしてきて編集しましたが反映されず、functions.phpで requir...

回答を参考に、子テーマのスキンフォルダ直下の function.php に wp_reset_query()、wp_reset_query() を削除した関数を置いてオーバーロードすることにしました。

具体的な修正方法は対策の通りです。

wp_reset_postdata() の方は、今回の件にあまり関係ないかもしれませんが、必要そうにも見えないし、削除しても正しく動いていそうなので削除して様子を見ています。

今回も勉強になりました!

コメント