親サイト(配信元)の 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パターンを押さえれば、マルチサイトなしでも「指定タグ・関連キーワードの記事一覧」を安全・軽量に埋め込めます。

コメント