除外カテゴリー・除外アーカイブをウィジェットに完全に反映させる方法

Cocoonカスタマイズ集
この記事は約19分で読めます。
記事内に広告が含まれています。

はじめに

カレンダー

Cocoon設定→インデックス→「除外カテゴリー」や、投稿編集画面で除外アーカイブを設定しても、WordPressの標準ウィジェット(カテゴリー、タグクラウド、アーカイブ、カレンダーなど)にはその除外設定が反映されません。

そこで、除外カテゴリー、除外アーカイブの設定をウィジェットに反映させます。

この記事では、この方法について説明します。

ウィジェット側に課題が残る技術的背景

Cocoon設定などで特定の投稿やカテゴリーをインデックスから除外しても、ウィジェット側の表示やカウントにはその設定が残ってしまいます。
これには、WordPressの設計思想における2つの大きな理由があります。

メインクエリーとウィジェットの処理の独立性

WordPressでは、ページ本体を表示する際の「メインクエリー」と、サイドバーなどに配置される「ウィジェット」のデータ取得処理が完全に別個に動いています。

テーマ側の除外設定はメインクエリーのSQL(WHERE句など)に介入して特定の投稿を省きます。

しかし、ウィジェットが内部で行うデータベースへの問い合わせ(タームの取得や投稿数の集計)には自動的に連動しません。

そのため、ウィジェット側は除外された投稿の存在を検知できず、そのまま集計対象として扱ってしまいます。

「除外」と「非公開」の概念のズレ

もう一つの要因は、データに対するシステムの解釈のズレにあります。

「非公開」ステータスの投稿であれば、WordPressのコアシステムが自動的にあらゆる場所のカウントやリストから一律で除外します。

しかし、今回の「除外」設定は、投稿自体の公開ステータスは「公開(publish)」のまま、特定のインデックス一覧に「載せない」ようにしている状態にすぎません。
システム側から見れば「データベース上に存在する通常の公開データ」として扱われるため、ウィジェット側の「存在する項目をすべて集計する」という処理においては、そのままカウントされてしまうというギャップが生じます。

このため、ウィジェットのカテゴリー一覧に除外したはずのカテゴリー名が残ってしまったり、投稿数から除外されずにカウントされたりするという課題が残ります。

表1に、ウィジェットごとのデフォルト挙動と、除外カテゴリーを反映させた場合の挙動を示します。

ウィジェットごとの挙動
ウィジェットデフォルト挙動除外カテゴリー反映後の挙動
カテゴリー除外したカテゴリーもそのまま表示される除外したカテゴリーは非表示にする
タグクラウド除外カテゴリーの投稿も含めた投稿数でカウント・表示される除外カテゴリーの投稿をカウントから除外する(0件になる場合は非表示)
アーカイブ除外カテゴリーの投稿も含めた投稿数でカウント・月表示される除外カテゴリーの投稿をカウントから除外する(0件の月は非表示)
カレンダー除外カテゴリーの投稿日もリンク付きで表示され、クリックすると404ページになる(図1参照)除外カテゴリーのみの投稿日はリンクを無効化し、投稿のない月は非表示にする

実装手順

以下のコードをfunctions.phpに追加します。

/**
 * 子孫カテゴリーを含めて除外対象のカテゴリーIDを取得
 *
 * @return int[] カテゴリーIDの配列
 */
if (!function_exists('get_combined_excluded_category_ids')):
function get_combined_excluded_category_ids() {
  static $cached = null;

  // キャッシュを取得
  if ($cached !== null) {
    return $cached;
  }

  // 除外カテゴリーのベースIDを取得
  $base_ids = function_exists('get_archive_exclude_category_ids')
    ? (array) get_archive_exclude_category_ids()
    : [];

  $all_ids = $base_ids;

  // 子孫カテゴリーIDを取得して統合
  foreach ($base_ids as $cat_id) {
    $children = get_term_children((int) $cat_id, 'category');
    if (!is_wp_error($children)) {
      $all_ids = array_merge($all_ids, $children);
    }
  }

  // 重複を除外してキャッシュに登録
  return $cached = array_values(array_unique(array_map('intval', $all_ids)));
}
endif;


/**
 * 除外対象となるすべての投稿IDを取得
 *
 * @return int[] 投稿IDの配列
 */
if (!function_exists('get_combined_excluded_posts')):
function get_combined_excluded_posts() {
  static $cached = null;

  // キャッシュを取得
  if ($cached !== null) {
    return $cached;
  }

  // メタキー指定の除外投稿IDを取得
  $excluded_posts = get_posts([
    'post_type'      => 'post',
    'posts_per_page' => -1,
    'fields'         => 'ids',
    'no_found_rows'  => true,
    'meta_query'     => [[
      'key'   => 'the_page_no_archive',
      'value' => 1,
      'type'  => 'NUMERIC',
    ]],
  ]) ?: [];

  // 除外カテゴリーの投稿IDを取得
  $excluded_cats = get_combined_excluded_category_ids();

  if (!empty($excluded_cats)) {
    $cat_posts = get_posts([
      'post_type'      => 'post',
      'posts_per_page' => -1,
      'fields'         => 'ids',
      'no_found_rows'  => true,
      'category__in'   => $excluded_cats,
    ]);
    $excluded_posts = array_merge($excluded_posts, $cat_posts);
  }

  // 重複を除外してキャッシュに登録
  return $cached = array_values(array_unique(array_map('intval', $excluded_posts)));
}
endif;


/**
 * 条件に一致する投稿数を取得
 *
 * @param int|null $term_id タームID
 * @param string|null $taxonomy タクソノミー名
 * @param array $date 日付指定配列
 * @return int 投稿数
 */
if (!function_exists('get_post_count_combined')):
function get_post_count_combined($term_id = null, $taxonomy = null, $date = []) {
  static $cache = [];

  // 引数を基にキャッシュキーを設定
  $cache_key = md5(serialize([$term_id, $taxonomy, $date]));

  // キャッシュを取得
  if (isset($cache[$cache_key])) {
    return $cache[$cache_key];
  }

  // 除外投稿IDを取得
  $exclude_posts = get_combined_excluded_posts();

  // クエリ引数を設定
  $args = [
    'post_type'      => 'post',
    'post_status'    => 'publish',
    'posts_per_page' => 1,
    'fields'         => 'ids',
    'no_found_rows'  => false,
    'post__not_in'   => $exclude_posts,
  ];

  // タームの絞り込み条件を設定
  if ($term_id && $taxonomy) {
    $args['tax_query'] = [[
      'taxonomy' => $taxonomy,
      'field'    => 'term_id',
      'terms'    => $term_id,
    ]];
  }

  // 日付の絞り込み条件を設定
  if (!empty($date)) {
    $date_query = [];

    if (!empty($date['year'])) {
      $date_query['year'] = intval($date['year']);
    }
    if (!empty($date['month'])) {
      $date_query['month'] = intval($date['month']);
    }
    if (!empty($date['day'])) {
      $date_query['day'] = intval($date['day']);
    }

    if ($date_query) {
      $args['date_query'] = [$date_query];
    }
  }

  // クエリを実行して投稿数を取得
  $query = new WP_Query($args);

  return $cache[$cache_key] = (int) $query->found_posts;
}
endif;


/**
 * カテゴリーウィジェットの項目と投稿数を除外設定に合わせて変更
 *
 * @param array $terms ターム配列
 * @param array $taxonomies タクソノミー配列
 * @param array $args 引数
 * @return array 調整後のターム配列
 */
if (!function_exists('filter_cat_counts_combined')):
function filter_cat_counts_combined($terms, $taxonomies = [], $args = []) {
  static $running = false;

  // 再帰処理中またはカテゴリー以外は処理対象外
  if ($running || !in_array('category', (array) $taxonomies, true)) {
    return $terms;
  }

  $running = true;

  // 除外カテゴリーIDを取得
  $excluded_cats = get_combined_excluded_category_ids();

  // カテゴリーを走査して項目を変更
  foreach ($terms as $key => $term) {
    if (!is_object($term) || !isset($term->term_id)) {
      continue;
    }

    // 除外カテゴリーは処理対象外
    if (in_array((int) $term->term_id, $excluded_cats, true)) {
      unset($terms[$key]);
      continue;
    }

    // 除外投稿を反映した投稿数を取得
    $count = get_post_count_combined($term->term_id, 'category');

    // 投稿数が0件の場合は項目を削除
    if ($count === 0) {
      unset($terms[$key]);
    } else {
      $terms[$key]->count = $count;
    }
  }

  $running = false;

  return $terms;
}
endif;


/**
 * タグクラウドウィジェットの項目と投稿数を除外設定に合わせて変更
 *
 * @param array $terms ターム配列
 * @param array $taxonomies タクソノミー配列
 * @param array $args 引数
 * @return array 調整後のターム配列
 */
if (!function_exists('filter_tag_counts_combined')):
function filter_tag_counts_combined($terms, $taxonomies, $args) {
  static $running = false;

  // 再帰処理中またはタグ以外は処理対象外
  if ($running || !in_array('post_tag', (array) $taxonomies, true)) {
    return $terms;
  }

  $running = true;

  // タグを走査して投稿数を変更
  foreach ($terms as $key => $term) {
    if (!is_object($term) || !isset($term->term_id)) {
      continue;
    }

    // 除外投稿を反映した投稿数を取得
    $count = get_post_count_combined($term->term_id, 'post_tag');

    // 投稿数が0件の場合は項目を削除
    if ($count === 0) {
      unset($terms[$key]);
    } else {
      $terms[$key]->count = $count;
    }
  }

  $running = false;

  return $terms;
}
endif;


/**
 * アーカイブウィジェットのSQL文に除外投稿の条件を追加
 *
 * @param string $where WHERE句
 * @param array $r 引数
 * @return string 修正後のWHERE句
 */
if (!function_exists('filter_archive_where_combined')):
function filter_archive_where_combined($where, $r) {
  global $wpdb;

  static $running = false;
  if ($running) {
    return $where;
  }
  $running = true;

  // 除外投稿IDを取得
  $excluded_posts = get_combined_excluded_posts();

  // SQLのWHERE句に除外条件を追加
  if ($excluded_posts) {
    $placeholders = implode(',', array_fill(0, count($excluded_posts), '%d'));
    $where       .= $wpdb->prepare(" AND {$wpdb->posts}.ID NOT IN ($placeholders)", $excluded_posts);
  }

  $running = false;

  return $where;
}
endif;


/**
 * カレンダーウィジェットのナビゲーションや日付リンクを除外設定に合わせて変更
 *
 * @param string $content カレンダーのHTML文字列
 * @return string 調整後のHTML文字列
 */
if (!function_exists('filter_calendar_combined')):
function filter_calendar_combined($content) {
  // 除外投稿IDを取得
  $excluded_posts = get_combined_excluded_posts();

  // 除外投稿なしは処理対象外
  if (empty($excluded_posts)) {
    return $content;
  }

  // 年月を取得
  $year     = isset($GLOBALS['year'])     ? (int) $GLOBALS['year']     : 0;
  $monthnum = isset($GLOBALS['monthnum']) ? (int) $GLOBALS['monthnum'] : 0;

  // 年月未指定の場合は対象年月を設定
  if (empty($year) || empty($monthnum)) {
    $latest = get_posts([
      'posts_per_page' => 1,
      'post_status'    => 'publish',
      'fields'         => 'ids',
      'no_found_rows'  => true,
      'orderby'        => 'date',
      'order'          => 'DESC',
      'post__not_in'   => $excluded_posts,
    ]);

    if ($latest) {
      $p        = get_post($latest[0]);
      $year     = (int) get_the_date('Y', $p);
      $monthnum = (int) get_the_date('n', $p);
    } else {
      $year     = (int) current_time('Y');
      $monthnum = (int) current_time('n');
    }
  }

  // 有効な前後月を取得
  $find_valid_month = function ($dir) use ($year, $monthnum) {
    for ($i = 1; $i <= 12; $i++) {
      $m = $monthnum + $dir * $i;
      $y = $year;

      while ($m < 1) {
        $m += 12;
        $y--;
      }
      while ($m > 12) {
        $m -= 12;
        $y++;
      }

      if (get_post_count_combined(null, null, ['year' => $y, 'month' => $m]) > 0) {
        return [
          'url'   => get_month_link($y, $m),
          'label' => sprintf('%d月', $m),
        ];
      }
    }

    return null;
  };

  // 前月リンクを有効な月に変更
  if (preg_match('/<span class="wp-calendar-nav-prev">.*?<a href="[^"]+">.*?<\/a>.*?<\/span>/', $content, $match)) {
    $valid   = $find_valid_month(-1);
    $content = str_replace(
      $match[0],
      $valid ? '<span class="wp-calendar-nav-prev"><a href="' . esc_url($valid['url']) . '">« ' . esc_html($valid['label']) . '</a></span>' : '',
      $content
    );
  }

  // 次月リンクを有効な月に変更
  if (preg_match('/<span class="wp-calendar-nav-next">.*?<a href="[^"]+">.*?<\/a>.*?<\/span>/', $content, $match)) {
    $valid   = $find_valid_month(1);
    $content = str_replace(
      $match[0],
      $valid ? '<span class="wp-calendar-nav-next"><a href="' . esc_url($valid['url']) . '">' . esc_html($valid['label']) . ' »</a></span>' : '',
      $content
    );
  }

  // カレンダーの日付リンクを取得
  preg_match_all('/<a\s+[^>]*href="([^"]+)"[^>]*>(\d+)<\/a>/', $content, $matches);

  foreach ($matches[0] as $i => $full) {
    $link = $matches[1][$i];
    $day  = $matches[2][$i];

    // 日付リンクから年月日を取得
    if (preg_match('#/(\d{4})/(\d{2})/(\d{2})/#', $link, $d)) {
      $count = get_post_count_combined(null, null, [
        'year'  => intval($d[1]),
        'month' => intval($d[2]),
        'day'   => intval($d[3]),
      ]);

      // 投稿なしの日付リンクを削除
      if ($count === 0) {
        $content = str_replace($full, $day, $content);
      }
    }
  }

  return $content;
}
endif;


// ウィジェットインスタンスを変更
add_filter('widget_display_callback', function ($instance, $widget, $args) {
  if (!isset($widget->id_base)) {
    return $instance;
  }

  // ウィジェットIDとフィルターを設定
  $widget_filters = [
    'categories' => ['get_terms'        , 'filter_cat_counts_combined', 10, 3],
    'tag_cloud'  => ['get_terms'        , 'filter_tag_counts_combined', 10, 3],
    'archives'   => ['getarchives_where', 'filter_archive_where_combined', 10, 2],
    'calendar'   => ['get_calendar'     , 'filter_calendar_combined', 10, 1],
  ];

  // 対象ウィジェットにフィルターを登録
  if (isset($widget_filters[$widget->id_base])) {
    $f = $widget_filters[$widget->id_base];
    add_filter($f[0], $f[1], $f[2], $f[3]);
  }

  return $instance;
}, 10, 3);

さいごに

今回紹介した方法を使えば、除外カテゴリーや除外アーカイブの設定をウィジェットに反映させることができます。
これにより、カテゴリー、タグクラウド、アーカイブ、カレンダーウィジェットにおいて、除外した投稿やアーカイブを正確に非表示にし、投稿数やリンクの整合性を維持できます。
ウィジェットに反映されない問題を解消し、サイト全体の表示精度を高めたい場合に有効な手法です。

タイトルとURLをコピーしました