複数RSSをまとめて表示する方法

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

はじめに

以下をCocoonで実現します。

CocoonにはRSSカード表示用のショートコードがありますが、複数のRSSをまとめて表示することはできません。 そこで、複数サイトのRSS情報を取得・マージし、最新の更新日付順に並び替えて表示できる独自のショートコードを追加します。

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

サイト間で更新頻度に偏りがある(一方のサイトの情報が古い)場合、最新記事だけで表示枠が埋まってしまい、古い方のサイトの記事が表示されません。

使用方法

URLオプションにカンマ区切りで複数のRSSを指定します。

[my_rss_merge url="https://site1.com/feed/, https://site2.com/feed/" site=1]

実装手順

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

add_shortcode('my_rss_merge', function ($atts) {

  // WordPressのRSS取得機能を読み込み
  include_once(ABSPATH . WPINC . '/feed.php');

  $atts = shortcode_atts([
    'url'          => '',            // カンマ区切りのRSS URL
    'count'        => 5,             // 表示件数
    'img'          => NO_IMAGE_RSS,  // 代替画像
    'target'       => '_blank',      // リンクのtarget属性
    'cache_minute' => 60,            // キャッシュ時間(分)
    'desc'         => 1,             // 説明文表示
    'date'         => 1,             // 日付表示
    'bold'         => 0,             // タイトル太字表示
    'arrow'        => 0,             // 矢印表示
    'class'        => null,          // 拡張クラス
    'site'         => 0,             // サイト名表示
  ], $atts, 'rss_merge');

  if (empty($atts['url'])) {
    return '';
  }

  // URL配列に変換
  $feed_urls = array_filter(array_map('trim', explode(',', $atts['url'])), function ($url) {
    return filter_var($url, FILTER_VALIDATE_URL) !== false;
  });

  if (empty($feed_urls)) {
    return '';
  }

  // キャッシュ用IDを生成
  $transient_id = 'rss_merge_' . md5(
    implode('|', $feed_urls) . '_' . $atts['count'] . '_' . $atts['desc'] . '_' . $atts['date'] . '_' . $atts['site']
  );

  // キャッシュがある場合
  $feed_contents = get_transient($transient_id);
  if ($feed_contents !== false) {
    return $feed_contents;
  }

  // SimplePieの内部キャッシュ・自動日付ソートを無効化
  $feed_options_callback = function ($feed) {
    $feed->enable_cache(false);
    $feed->enable_order_by_date(false);
  };
  add_action('wp_feed_options', $feed_options_callback);

  $items = [];

  // 各RSSを取得
  foreach ($feed_urls as $feed_url) {
    $rss = fetch_feed($feed_url);

    if (is_wp_error($rss)) {
      continue;
    }

    // サイト名を取得
    $site_title = trim((string) $rss->get_title());
    if (empty($site_title)) {
      $site_title = parse_url($feed_url, PHP_URL_HOST) ?? '';
    }

    // RSS内の全itemを取得
    $rss_items = $rss->get_items();
    if (is_array($rss_items)) {
      foreach ($rss_items as $item) {
        $items[] = [
          'item' => $item,
          'site' => $site_title,
        ];
      }
    }
  }

  // アクションを解除
  remove_action('wp_feed_options', $feed_options_callback);

  // 投稿日時で最新順にソート
  usort($items, function ($a, $b) {
    $date_a = $a['item']->get_date('c');
    $date_b = $b['item']->get_date('c');
    $time_a = $date_a ? strtotime($date_a) : 0;
    $time_b = $date_b ? strtotime($date_b) : 0;

    return $time_b - $time_a;
  });

  // 表示件数に制限
  $items = array_slice($items, 0, (int) $atts['count']);

  if (!empty($items)) {
    ob_start();

    // 日付フォーマットと抜粋長を取得
    $date_format = get_site_date_format();
    $excerpt_len = get_entry_card_excerpt_max_length();

    foreach ($items as $row) {
      $item = $row['item'];
      $site_title = $row['site'];

      // 記事本文を取得し空文字チェック
      $item_content = $item->get_content();
      if (!is_string($item_content)) {
        $item_content = '';
      }

      // 本文内から最初の画像URLを抽出
      $feed_img = $atts['img'];
      if (preg_match('/<img[^>]+src=["\']([^"\']+)["\']/i', $item_content, $matches)) {
        $feed_img = $matches[1];
      }

      $feed_url   = $item->get_permalink();
      $feed_title = str_replace(["\r\n", "\r", "\n"], '', $item->get_title() ?? '');
      $feed_date  = $item->get_date($date_format);

      $plain_text = wp_strip_all_tags($item_content);
      $feed_text  = get_content_excerpt($plain_text, $excerpt_len);

      // Cocoonテンプレートに渡す引数を構築
      $template_args = [
        'url'          => $feed_url,
        'title'        => $feed_title,
        'target'       => $atts['target'],
        'img'          => $feed_img,
        'site'         => $atts['site'],
        'site_title'   => $site_title,
        'desc'         => $atts['desc'],
        'date'         => $atts['date'],
        'text'         => $feed_text,
        'date_str'     => $feed_date,
        'cache_minute' => $atts['cache_minute'],
      ];

      cocoon_template_part('tmp/rss-card', null, $template_args);
    }

    $feed_content = ob_get_clean();
  } else {
    $domain_name = defined('THEME_NAME') ? THEME_NAME : 'default';
    $feed_content = '<p>' . __('RSSフィードを取得できません', $domain_name) . '</p>';
  }

  // 追加クラスを生成
  $card_class = get_additional_widget_entry_cards_classes([
    'bold'  => $atts['bold'],
    'arrow' => $atts['arrow'],
    'class' => $atts['class'],
  ]);

  $feed_contents = '<div class="rss-entry-cards widget-entry-cards' . $card_class . ' no-icon">' . $feed_content . '</div>';

  // キャッシュに保存
  set_transient($transient_id, $feed_contents, 60 * (int) $atts['cache_minute']);

  return $feed_contents;
});

参考:REST APIで情報を取得する方法

RSSは複数サイトを横断的に取得できる汎用性がありますが、アイキャッチ画像を取得できない、構造化データが不足しているといった課題があります。

そこで、対象サイトがWordPressに限定されるものの、REST APIを利用することで、投稿日やアイキャッチ画像を含む正確な投稿データを取得し、Cocoonのエントリーカード形式で安定した表示が可能になります。

RSSでは見た目や情報の精度に課題がある場合の代替手段として、REST APIは有効な選択肢となります。

完成イメージ

エントリーカードに読書進捗バーを表示する方法 | ART+BEAT MANIFESTO
はじめにプラグイン「WP Reading Progress」などを使用せずに、記事の読書進捗を示すバーを表示します。記事ページ上部の進捗バーに加え、エントリーカードにも過去の読書進捗を表示できます。また、再度投稿ページを開く際には、前回の読...
カードタイプ「タイル」をCSSだけでMasonryレイアウトにする方法 | ART+BEAT MANIFESTO
はじめにCocoon設定→インデックス→カードタイプ「タイル」を選択すると、エントリーカードのサムネイル画像が元画像の縦横比を維持して表示されます。しかし、画像ごとに高さが異なるため、カードを隙間なく整列させるには、JavaScriptライ...
スキン配色ジェネレーター | ART+BEAT MANIFESTO
はじめに基本カラー本スキンは、外観→カスタマイズ→メイド・イン・ヘブン設定→基本カラーから、サイト全体の配色を変更できます(図1参照)。見出しなどのテキストカラーは白(#ffffff)固定とし、基本カラーのサイトカラー、背景カラー、フッター...

使用方法

以下にショートコードを示します。

[my_api_merge url="https://site1.com/,https://site2.com/"]

実装手順

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

add_shortcode('my_api_merge', function ($atts) {

  $atts = shortcode_atts([
    'url'          => '',            // カンマ区切りのRSS URL
    'count'        => 5,             // 表示件数
    'img'          => NO_IMAGE_RSS,  // 画像がない場合の代替画像
    'target'       => '_blank',      // リンクのtarget属性
    'cache_minute' => 60,            // キャッシュ時間(分)
    'desc'         => 1,             // 説明文表示
    'date'         => 1,             // 日付表示
    'bold'         => 0,             // タイトル太字表示
    'arrow'        => 0,             // 矢印表示
    'class'        => null,          // 拡張クラス
    'site'         => 0,             // サイト名表示
  ], $atts, 'api_merge');

  if (empty($atts['url'])) {
    return '';
  }

  // URL配列に変換
  $site_urls = array_filter(array_map('trim', explode(',', $atts['url'])), function ($url) {
    return filter_var($url, FILTER_VALIDATE_URL) !== false;
  });

  if (empty($site_urls)) {
    return '';
  }

  // キャッシュ用IDを生成
  $transient_id = 'api_merge_mod_' . md5(
    implode('|', $site_urls) . '_' . (int) $atts['count'] . '_' . (int) $atts['site'] . '_' . (int) $atts['desc'] . '_' . (int) $atts['date']
  );

  // キャッシュがある場合
  $feed_contents = get_transient($transient_id);
  if ($feed_contents !== false) {
    return $feed_contents;
  }

  $items = [];

  // 各対象サイトからREST API経由でデータを取得
  foreach ($site_urls as $site_url) {
    $site_url = trailingslashit($site_url);

    // 更新順で20件投稿を取得
    $endpoint = $site_url . 'wp-json/wp/v2/posts?per_page=20&orderby=modified&_embed';

    // APIリクエスト
    $res = wp_remote_get($endpoint, ['timeout' => 10]);
    if (is_wp_error($res)) {
      continue;
    }

    // JSONデコード
    $posts = json_decode(wp_remote_retrieve_body($res), true);
    if (!is_array($posts)) {
      continue;
    }

    // サイト名を取得
    $site_title = '';
    $info_res = wp_remote_get($site_url . 'wp-json/', ['timeout' => 10]);
    if (!is_wp_error($info_res)) {
      $info = json_decode(wp_remote_retrieve_body($info_res), true);
      if (!empty($info['name'])) {
        $site_title = trim((string) $info['name']);
      }
    }

    if (empty($site_title)) {
      $site_title = (string) parse_url($site_url, PHP_URL_HOST);
    }

    foreach ($posts as $post) {
      if (!is_array($post)) {
        continue;
      }
      $items[] = [
        'post'       => $post,
        'site_url'   => $site_url,
        'site_title' => $site_title,
      ];
    }
  }

  if (empty($items)) {
    return '';
  }

  // 更新日時で降順ソート
  $timezone = new DateTimeZone('Asia/Tokyo');
  usort($items, function ($a, $b) use ($timezone) {
    $modified_a = $a['post']['modified'] ?? '';
    $modified_b = $b['post']['modified'] ?? '';

    $date_a = !empty($modified_a) ? date_create($modified_a, $timezone) : false;
    $date_b = !empty($modified_b) ? date_create($modified_b, $timezone) : false;

    if ($date_a && $date_b) {
      return $date_b->getTimestamp() - $date_a->getTimestamp();
    }
    return 0;
  });

  // 表示件数に制限
  $items = array_slice($items, 0, (int) $atts['count']);

  ob_start();

  // 日付フォーマットと抜粋長を取得
  $date_format = get_site_date_format();
  $excerpt_len = (int) get_entry_card_excerpt_max_length();

  foreach ($items as $row) {
    $post       = $row['post'];
    $site_title = $row['site_title'];

    $raw_title = isset($post['title']['rendered']) ? (string) $post['title']['rendered'] : '';
    $title = trim(str_replace(["\r\n", "\r", "\n"], '', wp_strip_all_tags($raw_title)));
    $link = isset($post['link']) ? (string) $post['link'] : '';

    $modified_date = isset($post['modified']) ? (string) $post['modified'] : '';
    $dateval = !empty($modified_date) ? mysql2date($date_format, $modified_date) : '';

    $content = isset($post['content']['rendered']) ? (string) $post['content']['rendered'] : '';
    $plain_text = wp_strip_all_tags($content);
    $excerpt = get_content_excerpt($plain_text, $excerpt_len);

    $feed_img = $atts['img'];
    if (!empty($post['_embedded']['wp:featuredmedia'][0]['source_url'])) {
      $feed_img = (string) $post['_embedded']['wp:featuredmedia'][0]['source_url'];
    }

    // Cocoonのテンプレート引数を構築
    $template_args = [
      'url'          => esc_url($link),
      'title'        => $title,
      'target'       => $atts['target'],
      'img'          => $feed_img,
      'site'         => $atts['site'],
      'site_title'   => $site_title,
      'desc'         => $atts['desc'],
      'date'         => $atts['date'],
      'text'         => $excerpt,
      'date_str'     => $dateval,
      'cache_minute' => (int) $atts['cache_minute'],
    ];

    cocoon_template_part('tmp/rss-card', null, $template_args);
  }

  $feed_content = ob_get_clean();

  // 追加クラスを生成
  $card_class = get_additional_widget_entry_cards_classes([
    'bold'  => (int) $atts['bold'],
    'arrow' => (int) $atts['arrow'],
    'class' => $atts['class'],
  ]);

  $output = '<div class="rss-entry-cards widget-entry-cards' . $card_class . ' no-icon">' . $feed_content . '</div>';

  set_transient($transient_id, $output, 60 * (int) $atts['cache_minute']);

  return $output;
});

参考:RSS表示の代替としてブログカードを利用する方法

RSSは複数サイトを横断的に取得できる汎用性がありますが、アイキャッチ画像を取得できない、構造化データが不足しているといった課題があります。
また、REST APIはこれらの問題を解消できる一方で、WordPressサイトに限定されるという制約があります。

そこで、幅広いサイトを扱える代替手段として、RSSフィードから投稿URLのみを取得し、ブログカード形式で表示する方法を用います。
この方法であれば、WordPress以外のブログサービスも含めて統一的に扱うことが可能です。

ただし、article:published_timeやarticle:modified_timeは任意情報のため取得していません。
そのため、外部ブログカードでは投稿日が表示されず、記事がいつ投稿されたものか分からない点には注意が必要です。

完成イメージ

宵宮凪がマネに夜食を作る理由 第6話
「また明日 夜食作りにくるから──」 人気ガールズバンド『クロチェッタ』のマネージャーの岩戸は忙しい日々を過ごしていた。 激務の果ての徹夜明け、机の上で少し寝てしまったとある夜。 気づくと目の前には手作りの夜食とバンドメンバーの凪の姿が…!...
[第22話]島ロック - まの瀬/みはる | 少年ジャンプ+
こじらせた心が響かせる、真っすぐなロックンロール!離島で始まるバンドライフ青春譚。
[4話]スラップショットガールズ - 有波 | 少年ジャンプ+
虎走くるみは中学卒業を機に、親の都合で北海道へ引っ越すことに。彼氏に遠距離恋愛になることを告げると、別れを切り出されてしまう。彼氏と別れ、傷心のくるみが、茫然自失で向かったスケート場で出会ったのはイケメンとアイスホッケーだった。

使用方法

以下にショートコードを示します。

[my_rss_merge_blogcard]
オプション
オプション説明値初期値
count表示数5
urlRSSフィードのURL(必須)カンマ区切りで指定

実装手順

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

add_shortcode('my_rss_merge_blogcard', function ($atts) {

  // WordPressのRSS取得機能を読み込み
  include_once(ABSPATH . WPINC . '/feed.php');

  $atts = shortcode_atts([
    'url'          => '',            // カンマ区切りのRSS URL
    'count'        => 5,             // 表示件数
    'cache_minute' => 60,            // キャッシュ時間(分)
  ], $atts, 'rss_merge_blogcard');

  if (empty($atts['url'])) {
    return '';
  }

  // URL配列に変換
  $feed_urls = array_filter(array_map('trim', explode(',', $atts['url'])), function ($url) {
    return filter_var($url, FILTER_VALIDATE_URL) !== false;
  });

  if (empty($feed_urls)) {
    return '';
  }

  // キャッシュ用IDを生成
  $transient_id = 'rss_merge_bc_' . md5(implode('|', $feed_urls) . '_' . (int) $atts['count']);

  // キャッシュがある場合
  $cached_output = get_transient($transient_id);
  if ($cached_output !== false) {
    return $cached_output;
  }

  // SimplePieの内部キャッシュ・自動日付ソートを無効化
  $feed_options_callback = function ($feed) {
    $feed->enable_cache(false);
    $feed->enable_order_by_date(false);
  };
  add_action('wp_feed_options', $feed_options_callback);

  $items = [];

  // 各RSSを取得
  foreach ($feed_urls as $feed_url) {
    $rss = fetch_feed($feed_url);

    if (is_wp_error($rss)) {
      continue;
    }

    $rss_items = $rss->get_items();
    if (is_array($rss_items)) {
      foreach ($rss_items as $item) {
        $items[] = $item;
      }
    }
  }

  // アクションを解除
  remove_action('wp_feed_options', $feed_options_callback);

  if (empty($items)) {
    return '';
  }

  // 投稿日時で最新順にソート
  usort($items, function ($a, $b) {
    $date_a = $a->get_date('c');
    $date_b = $b->get_date('c');
    $time_a = $date_a ? strtotime($date_a) : 0;
    $time_b = $date_b ? strtotime($date_b) : 0;

    return $time_b - $time_a;
  });

  // 表示件数に制限
  $items = array_slice($items, 0, (int) $atts['count']);

  ob_start();

  foreach ($items as $item) {
    $post_url = $item->get_permalink();

    if (!$post_url) {
      continue;
    }

    echo do_shortcode('[blogcard url="' . esc_url($post_url) . '"]');
  }

  $output = ob_get_clean();

  set_transient($transient_id, $output, 60 * (int) $atts['cache_minute']);

  return $output;
});

RSS、REST API、OGPの制約と特徴まとめ

外部サイトの記事を取得・表示する方法として、RSS、REST API、OGPがありますが、それぞれ取得できる情報や制約が異なります。
以下の表に、実装時に特に重要となるポイントを整理します(表1参照)。

RSS、REST API、OGPの制約と特徴
利用ライブラリ・手段取得可能な主な情報主な課題・制限
RSSfetch_feed(WordPress標準)
  • タイトル
  • 本文
  • 公開日
  • カテゴリ
標準ではアイキャッチ画像が含まれない。
出力側のカスタマイズが必要。
REST APIWordPress REST API
  • タイトル
  • 抜粋
  • 本文
  • 投稿日
  • 更新日
  • アイキャッチ画像
WordPressサイト限定。
相手側がAPIを公開(有効化)している必要がある。
OGPOpen Graphライブラリ
  • タイトル
  • 説明文
  • 画像(og:image)
投稿日は任意項目。
サイトによって記述形式がバラバラで、取得精度が不安定。

Cocoon設定とRSSの課題について

Cocoon設定→インデックス→ 表示順を投稿日以外に変更した場合、その順序でRSSが生成されます。
しかし、RSSアイテムの<pubDate>は公開日であり、更新日ではありません。
そのため、リライトによって古い公開日の記事が再表示された場合でも、<pubDate>は過去日のままとなります。

この結果、複数サイトの情報をマージし、日付順にソートして最新数件をカード表示した際に、実際の表示順と日付情報が一致せず、正しく表示されない問題が発生します。

このような場合は、Cocoon標準のrssショートコードを複数指定し、各サイトごとに個別表示する必要があります。

さいごに

今回紹介した方法を使えば、複数のRSSフィードをまとめて取得し、CocoonのRSSカードデザインを維持したまま柔軟に表示できます。
キャッシュ制御や表示条件も細かく調整できるため、パフォーマンスと見た目の両立が可能です。

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