親サイトのREST APIを“読むだけ”で関連記事一覧を埋め込む:ショートコード & React実装例

未分類

親サイト(配信元)の WordPress REST API をクライアント表示専用で呼び出し、指定タグやキーワードに一致する記事一覧を参照側サイトへ埋め込む方法です。ここでは 投稿作成APIは扱わず、取得・表示に特化します。実装は ショートコード(PHP)Reactコンポーネント の2パターン。


前提と設計のポイント

  • 取得エンドポイント:/wp-json/wp/v2/posts
  • タグは ID指定が基本。/wp/v2/tags?slug=techスラッグ→ID解決posts?tags={id} に渡す。
  • 表示に必要な最小フィールドだけ返す:_fields=id,link,title,excerpt,modified,featured_media など。
  • サムネが必要なら _embed=1 を併用(wp:featuredmedia[0].source_url)。
  • CORS が閉じている場合は、参照側サーバから サーバサイド取得(PHP/Next.js)に切替える。
  • API負荷と体感速度のため 5〜15分キャッシュ推奨。

実装パターン①:ショートコード(参照側もWordPress)

参照側で サーバ側fetch → CORS非依存&キャッシュ容易。functions.php に以下を追加。

コード(functions.php)

// [remote_posts_by_tag base="https://parent.example.com/wp-json/wp/v2" tag_slug="tech" per_page="6" cache="900"]
add_shortcode('remote_posts_by_tag', function ($atts) {
  $a = shortcode_atts([
    'base'     => 'https://parent.example.com/wp-json/wp/v2',
    'tag_slug' => 'tech',
    'per_page' => 6,
    'page'     => 1,
    'cache'    => 900, // 秒
  ], $atts);

  $key = 'remote_posts_' . md5(serialize($a));
  if ($a['cache'] > 0 && ($cached = get_transient($key))) return $cached;

  // 1) タグID解決
  $resTag = wp_remote_get(sprintf('%s/tags?slug=%s&_fields=id,name', $a['base'], rawurlencode($a['tag_slug'])));
  if (is_wp_error($resTag)) return 'タグ取得に失敗しました。';
  $tags = json_decode(wp_remote_retrieve_body($resTag), true);
  if (empty($tags)) return '指定タグが見つかりません。';
  $tag_id = (int)$tags[0]['id'];

  // 2) 記事取得(必要最小の項目 + _embed)
  $url = add_query_arg([
    'per_page' => (int)$a['per_page'],
    'page'     => (int)$a['page'],
    'tags'     => $tag_id,
    'orderby'  => 'date',
    'order'    => 'desc',
    '_embed'   => 1,
    '_fields'  => 'id,link,title,excerpt,modified,featured_media,_links.wp:featuredmedia'
  ], $a['base'] . '/posts');

  $resPost = wp_remote_get($url, ['timeout' => 12]);
  if (is_wp_error($resPost)) return '記事取得に失敗しました。';
  $posts = json_decode(wp_remote_retrieve_body($resPost), true);

  ob_start(); ?>
  <div class="remote-grid">
    <?php foreach ($posts as $p):
      $media = $p['_embedded']['wp:featuredmedia'][0] ?? null;
      $img   = $media['source_url'] ?? '';
      $title = $p['title']['rendered'] ?? '';
      ?>
      <article class="remote-card">
        <?php if ($img): ?>
          <a href="<?php echo esc_url($p['link']); ?>" target="_blank" rel="noopener">
            <img src="<?php echo esc_url($img); ?>" alt="<?php echo esc_attr(wp_strip_all_tags($title)); ?>" loading="lazy">
          </a>
        <?php endif; ?>
        <h3 class="remote-title"><a href="<?php echo esc_url($p['link']); ?>" target="_blank" rel="noopener">
          <?php echo $title; ?>
        </a></h3>
        <div class="remote-excerpt"><?php echo $p['excerpt']['rendered'] ?? ''; ?></div>
        <time class="remote-date"><?php echo esc_html(mysql2date('Y-m-d', $p['modified'])); ?></time>
      </article>
    <?php endforeach; ?>
  </div>
  <?php
  $html = ob_get_clean();
  if ($a['cache'] > 0) set_transient($key, $html, (int)$a['cache']);
  return $html;
});

使い方(投稿本文に貼るだけ)

[remote_posts_by_tag tag_slug="tech" per_page="8" cache="600"]

拡張page 属性を受け取り、ページネーションUI(X-WP-TotalPages ヘッダを wp_remote_retrieve_header() で参照)を追加可能。

実装パターン②:Reactコンポーネント(フロント埋め込み)

SPA/MPA問わず利用可能。CORSが許可されている前提。許可されていない場合は アプリ側に軽量APIプロキシを用意してください。

コンポーネント(TypeScript任意)

import React, { useEffect, useState } from 'react';

type WPPost = {
  id: number;
  link: string;
  modified: string;
  title: { rendered: string };
  excerpt?: { rendered: string };
  _embedded?: { ['wp:featuredmedia']?: Array<{ source_url: string }> };
};

type Props = {
  base?: string;     // 例: 'https://parent.example.com/wp-json/wp/v2'
  tagSlug: string;   // 例: 'tech'
  perPage?: number;  // 例: 6
  page?: number;     // 例: 1
};

export const RemotePostsByTag: React.FC<Props> = ({
  base = 'https://parent.example.com/wp-json/wp/v2',
  tagSlug,
  perPage = 6,
  page = 1,
}) => {
  const [posts, setPosts] = useState<WPPost[]>([]);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;

    async function fetchData() {
      try {
        // 1) タグID解決
        const tagRes = await fetch(`${base}/tags?slug=${encodeURIComponent(tagSlug)}&_fields=id,name,slug`);
        const tags = await tagRes.json();
        const tagId = tags?.[0]?.id;
        if (!tagId) throw new Error('指定タグが見つかりません');

        // 2) 記事取得(_embed + _fields)
        const params = new URLSearchParams({
          per_page: String(perPage),
          page: String(page),
          tags: String(tagId),
          orderby: 'date',
          order: 'desc',
          _embed: '1',
          _fields: 'id,link,title,excerpt,modified,featured_media,_links.wp:featuredmedia'
        });

        const postRes = await fetch(`${base}/posts?${params.toString()}`);
        const data = await postRes.json();
        if (!cancelled) setPosts(data);
      } catch (e: any) {
        if (!cancelled) setError(e.message || '取得に失敗しました');
      }
    }

    fetchData();
    return () => { cancelled = true; };
  }, [base, tagSlug, perPage, page]);

  if (error) return <div className="wp-remote-error">{error}</div>;
  if (!posts.length) return <div className="wp-remote-loading">読み込み中…</div>;

  return (
    <div className="wp-remote-grid">
      {posts.map(p => {
        const img = p._embedded?.['wp:featuredmedia']?.[0]?.source_url || '';
        return (
          <article key={p.id} className="wp-remote-card">
            {img && (
              <a href={p.link} target="_blank" rel="noopener">
                <img src={img} alt="" loading="lazy" />
              </a>
            )}
            <h3 className="wp-remote-title">
              <a href={p.link} target="_blank" rel="noopener"
                 dangerouslySetInnerHTML={{ __html: p.title.rendered }} />
            </h3>
            <div className="wp-remote-excerpt"
                 dangerouslySetInnerHTML={{ __html: p.excerpt?.rendered || '' }} />
            <time className="wp-remote-date">{new Date(p.modified).toLocaleDateString()}</time>
          </article>
        );
      })}
    </div>
  );
};

導入例(任意のページ/ウィジェット内):

<RemotePostsByTag
  base="https://parent.example.com/wp-json/wp/v2"
  tagSlug="tech"
  perPage={8}
  page={1}
/>

注意:ブラウザから直接親サイトを叩くため、親側で Access-Control-Allow-Origin を適切に設定。難しければ自サイトに /api/remote-posts のような サーバ側プロキシを置き、そこから親APIへ取得→JSON返却にします(ISR/キャッシュも付けやすい)。


仕上げのチェックリスト

  • スラッグ→ID を必ず踏む(/tags?slug=)。
  • 表示だけなら GET + _fields で最小化、サムネは _embed
  • CORS 問題は サーバ側取得 or プロキシで解決。
  • 5〜15分キャッシュで負荷と体感を最適化。
  • ページネーションは page を引き回し、必要なら総ページ数も取得してUI化。

この2パターンを押さえれば、マルチサイトなしでも「指定タグ・関連キーワードの記事一覧」を安全・軽量に埋め込めます。

コメント

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