WordPress REST API の使い方
このページでは、WordPress REST API の基本から応用までを詳しく解説しています。REST API のルートやエンドポイントの構造、データ取得や投稿の作成・更新・削除の方法、認証の仕組み、カスタムフィールドの活用、独自のエンドポイントの作成方法まで、幅広くカバーしています。
記事の内容
- REST API の基礎: ルート、エンドポイント、グローバルパラメータなどの基本を解説
- リクエストの送信方法: Fetch API や @wordpress/fetch-api を使ったデータ取得・操作
- 認証とセキュリティ: アプリケーションパスワードや認証手法
- カスタムフィールドとエンドポイントの拡張: register_meta() や register_rest_field() による API のカスタマイズ
- 実践的な実装例: 投稿取得・作成・更新・削除の操作、アイキャッチ画像やカスタム投稿タイプの扱い
関連ページ:WordPress REST API カスタムエンドポイントの作成
更新日:2025年04月16日
作成日:2025年04月04日
WordPress REST API とは
WordPress REST API は、WordPress のデータ(投稿、ユーザー、コメントなど)を他のアプリやシステムとやり取りできる仕組みです。これを使うと、プラグインや外部のウェブサイトから WordPress の記事を取得したり、新しい投稿を作成したりできます。
例えば、WordPress のブロックエディター(Gutenberg)は、REST API を使って投稿データを取得・保存し、リアルタイムで編集内容を反映しています。
API(Application Programming Interface)は、アプリケーション同士がデータをやり取りするための仕組みです。WordPress には多くの API があり、REST API はその一つです。
REST(REpresentational State Transfer)は、Web API の設計原則の一つで、リソース(データ)を統一されたルールでやり取りする仕組みです。
WordPress REST API は、投稿、ページ、タクソノミー、カスタム投稿タイプなどのデータを扱うエンドポイントを提供します。これらのエンドポイントを通じて JSON データを送受信し、WordPress のデータを取得(GET)、更新(PUT)、作成(POST)、削除(DELETE)することができます。
代表的な REST API コマンド:
- GET → データを取得(例:投稿一覧を取得)
- POST → 新しいデータを作成(例:新規投稿を追加)
- PUT → 既存のデータを更新(例:投稿のタイトルを変更)
- DELETE → データを削除(例:投稿を削除)
例えば、投稿(Post)に関する API は /wp/v2/posts エンドポイントを使用し、GET /wp/v2/posts で投稿一覧を取得し、POST /wp/v2/posts で新規投稿を作成できます。
HTTP メソッド
これらの GET、POST、PUT/PATCH、DELETE などのコマンドは、HTTP メソッドとも呼ばれます。
REST API では、クライアント(例: JavaScript の fetch など)がサーバーにリクエストを送信するときに、「どのような操作をしたいのか」 を指定する必要があり、この操作を指定するためのものが HTTP メソッド(HTTP method)であり、「リクエストの種類」を表します。
メソッド | 役割 | 例(WordPress REST API) |
---|---|---|
GET | データの取得 | GET /wp/v2/posts (投稿一覧を取得) |
POST | データの作成 | POST /wp/v2/posts (新規投稿を作成) |
PUT | データの更新 | PUT /wp/v2/posts/123 (投稿 ID 123 の内容を更新) |
DELETE | データの削除 | DELETE /wp/v2/posts/123 (投稿 ID 123 を削除) |
WordPress ドキュメント: REST API Handbook
ルートとエンドポイント
WordPress REST API では、URI 構造を「ルート」と「エンドポイント」という概念で整理しています。
ルート(Route)
ルートは、API の基本的なパス(URI)を表し、どの種類のデータを扱うのかを決めるものです。
例えば、以下のルートは、それぞれ異なる種類のデータを扱います。
- 投稿データ(posts):
/wp-json/wp/v2/posts
- カテゴリーデータ(categories):
/wp-json/wp/v2/categories
ルートそのものはデータの種類を示すだけで、どのような操作を行うかは指定されていません。
エンドポイント(Endpoint)
エンドポイントは、「ルート + HTTPメソッド」の組み合わせによって定義され、特定の操作(GET、POST、PUT、DELETE など)を実行するための具体的な URI です。
例えば、投稿データ(/wp-json/wp/v2/posts)のルートには、以下のようなエンドポイントがあります。
HTTP メソッド | エンドポイント | 説明 |
---|---|---|
GET | /wp-json/wp/v2/posts | 投稿一覧を取得 |
POST | /wp-json/wp/v2/posts | 新規投稿を作成 |
GET | /wp-json/wp/v2/posts/1 | ID=1 の投稿を取得 |
PUT | /wp-json/wp/v2/posts/1 | ID=1 の投稿を更新 |
DELETE | /wp-json/wp/v2/posts/1 | ID=1 の投稿を削除 |
ルート自体も URI ですが、エンドポイントはルートと HTTP メソッド(動作)によって定義されます。
つまり、
- ルート:「どの種類のデータを扱うのか?」(例:/wp-json/wp/v2/posts)
- エンドポイント:「そのデータにどんな操作をするのか?」(例:GET で取得、POST で作成)
という関係になります。
リソース | ベースルート |
---|---|
Posts (投稿) | /wp-json/wp/v2/posts |
カスタム投稿タイプ | /wp-json/wp/v2/{custom_post_type} |
Pages (固定ページ) | /wp-json/wp/v2/pages |
Categories (カテゴリー) | /wp-json/wp/v2/categories |
Tags (タグ) | /wp-json/wp/v2/tags |
Taxonomies (タクソノミー) | /wp-json/wp/v2/taxonomies |
Media (メディア) | /wp-json/wp/v2/media |
Users (ユーザー) | /wp-json/wp/v2/users |
Post Types (投稿タイプ) | /wp-json/wp/v2/types |
Settings (設定) | /wp-json/wp/v2/settings |
エンドポイントと HTTP メソッド
WordPress REST API では、1つのルートに対して複数の HTTP メソッドを紐づけることができます。
これにより、同じルートでも異なるエンドポイントを定義できます。
例えば、
- GET /wp-json/wp/v2/posts は投稿一覧を取得
- POST /wp-json/wp/v2/posts は新規投稿を作成
というように、HTTP メソッドごとに異なる動作を持たせることができます。
REST API をテストする際の注意点
WordPress の REST API をテストする際は、パーマリンク設定を「基本」以外に設定します。
パーマリンク設定を「基本」で使用する場合、/wp-json/ の代わりに /?rest_route=/ を使用します。
ルートとエンドポイントの例
ブラウザで、WordPress サイトの /wp-json/ にアクセスすると、その URI に GET リクエストが送信され、JSON レスポンスが返されます。
例: https://example.com/wp-json/ (example.com の部分は環境に合わせて変更します)
以下は、ローカル環境の http://localhost/wp-sample/wp-json/ にアクセスする例です。返されるデータは、使用可能なルートと各ルート内で使用可能なエンドポイントを示す JSON レスポンスです。
一部のブラウザでは、JSON レスポンスを読みやすい形式で表示されますが、Chrome などでは読みやすく表示するには拡張機能(例 JSON Formatter)が必要です。Firefox ではデフォルトで、ビューを切り替えたり、リクエストヘッダーを調べたりすることができます。
上記の例は、/wp-json/ はルートであり、そのルートが GET リクエストを受信すると、データを表示するエンドポイントによって処理されます。
以下は /wp-json/wp/v2/posts で投稿(posts)のリソースにアクセスした場合の例です。
/wp-json/wp/v2/posts ルートは、投稿のリストを返す GET エンドポイントだけでなく、POST エンドポイントも提供します。
通常、同じルート (この場合は /wp-json/wp/v2/posts) には、データを取得するための GET、データを作成するための POST、データを削除するための DELETE など、さまざまな HTTP メソッドに対して異なるエンドポイントがあります。
REST API Handbook:
グローバルパラメータ
WordPress REST API には、リクエストやレスポンスの処理方法を制御する 「グローバルパラメータ(Global Parameters)」 があります。
これらは、特定のリソース(Posts, Pages, Comments, Users, Terms など)に限定されず、すべてのエンドポイントで共通して使用できる設定です。
グローバルパラメータの使い方
グローバルパラメータは、クエリ文字列(URL の末尾に追加する ?key=value 形式のデータ)として指定します。複数のパラメータを指定する場合は、& で区切ります。
例: /wp-json/wp/v2/posts?_fields=author,id,excerpt,title,link
通常、/wp-json/wp/v2/posts に GET リクエストを送ると、すべての投稿フィールドがレスポンスとして返されますが、_fields パラメータを追加すると、レスポンスで必要なフィールドだけを取得できます。
以下は GET /wp-json/wp/v2/posts ですべてのフィールドを取得したレスポンスの例です。
[
{
"id": 19,
"date": "2025-02-20T17:07:16",
"date_gmt": "2025-02-20T08:07:16",
"guid": {
"rendered": "http://localhost/wp-sample/?p=19"
},
"modified": "2025-02-26T19:30:37",
"modified_gmt": "2025-02-26T10:30:37",
"slug": "my-custom-posts-block",
"status": "publish",
"type": "post",
"link": "http://localhost/wp-sample/my-custom-posts-block/",
"title": {
"rendered": "My Custom Posts Block"
},
"content": {
"rendered": "...",
"protected": false
},
"excerpt": {
"rendered": "...",
"protected": false
},
"author": 1,
"featured_media": 0,
"comment_status": "open",
"ping_status": "open",
"sticky": false,
"template": "",
"format": "standard",
"meta": {
...
},
"categories": [
9
],
"tags": [],
"class_list": [
...
],
"acf": [],
"_links": {
...
}
},
...
]
以下は GET /wp-json/wp/v2/posts?_fields=author,id,excerpt,title,link で _fields を使用して必要なデータだけ取得したレスポンスの例です。
このように、グローバルパラメータを活用すれば、必要なデータだけを取得(レスポンスのサイズを軽量化)し、API のパフォーマンスを最適化できます。
[
{
"id": 19,
"link": "http://localhost/wp-sample/my-custom-posts-block/",
"title": {
"rendered": "My Custom Posts Block"
},
"excerpt": {
"rendered": "...",
"protected": false
},
"author": 1
},
...
]
WordPress REST API には _fields 以外にも以下のようないくつかのグローバルパラメータがあります。
これらはすべてのエンドポイントで使用可能で、リクエストの動作を制御したり、レスポンスのデータを最適化したりするのに使用できます。
パラメータ | 目的 | 使用例 |
---|---|---|
_fields | 必要なフィールドだけを取得 | ?_fields=id,title |
_embed | 関連データ(例: author の詳細)を含める | ?_embed=author |
_envelope | レスポンスをラップしてステータスなどを追加 | ?_envelope=true |
_method | HTTP メソッドをオーバーライド | ?_method=DELETE |
REST API Handbook: Global Parameters
ページネーションと並び順の制御
WordPress REST API では、ページネーション(複数ページに分ける処理) やデータの並び順を制御できます。
ページネーション(ページごとのデータ取得)
ページネーションは、次の 3 つのパラメータで調整できます。
- per_page:1ページあたりの取得件数
- page:取得するページ番号
- offset:スキップする件数(per_page と併用不可)
例:1ページに5件の投稿を表示する(per_page=5 を追加)
GET /wp-json/wp/v2/posts?_fields=author,id,excerpt,title,link&per_page=5
これにより、最初の5件の投稿のみが取得されます。
例:2ページ目 を取得する(page=2 を追加)
GET /wp-json/wp/v2/posts?_fields=author,id,excerpt,title,link&per_page=5&page=2
6件目以降の投稿が取得されます(5件ずつ表示の2ページ目)。
合計数と合計ページ数
利用可能なデータの合計数やページ数を判断するために、API は2つのヘッダーフィールドを返します。
- X-WP-Total : コレクション内のレコードの合計数
- X-WP-TotalPages : 利用可能なすべてのレコードを含むページの合計数
WordPress REST API で取得したデータには、投稿数やページ数は含まれていないので、それらが必要な場合は、上記 HTTP ヘッダーから取得することができます。
以下は fetch() を使って投稿データを取得する際に、投稿レコードの合計数とページの合計数を取得してコンソールに出力する例です。per_page の値を変えることで、ページの合計数は変わります。
fetch('http://example.com/wp-json/wp/v2/posts?per_page=1')
.then(response =>{
// 投稿レコードの合計数
const totalPosts = parseInt(response.headers.get("X-WP-Total"));
// 投稿レコードを含むページの合計数(この場合、per_page=1 を指定しているので、投稿レコードの合計数と同じ)
const totalPages = parseInt(response.headers.get("X-WP-TotalPages"));
console.log("Total Posts:", totalPosts, "Total Pages:", totalPages);
// レスポンスを JSON として解析して then() に渡す
return response.json()})
.then(data => {
// 投稿のデータをコンソールに出力
console.log(data);
});
データの並び順を変更する
投稿の順序を制御するには、次の2つのパラメータを使用します。
- orderby:並び替えの基準(例:title, date, id など)
- order:並び順(昇順 asc / 降順 desc)
例:例:投稿タイトルの昇順(A → Z)に並び替える(orderby=title&order=asc)
GET /wp-json/wp/v2/posts?_fields=author,id,excerpt,title,link&per_page=5&orderby=title&order=asc
投稿タイトルの アルファベット順(昇順) に並べ替えられます。
例:例:投稿日時の降順(新しい順)に並び替える(orderby=date&order=desc)
GET /wp-json/wp/v2/posts?_fields=author,id,excerpt,title,link&per_page=5&orderby=date&order=desc
最新の投稿から順に表示されます(デフォルト設定)。
REST API Handbook: Pagination
Schema(スキーマ)
スキーマ(Schema) は、REST API でやり取りするデータの 「設計図」や「ルール」 のことです。具体的には、API のエンドポイント(URL)で取得・送信できるデータの 型 や 構造、制約 を決めます。
REST API を操作するときは、以下の WP REST API の公式リファレンスが参考になります。
エンドポイントリファレンス: REST API Developer Endpoint Reference
エンドポイントリファレンスには、WordPress コアに同梱されているすべてのエンドポイントがリストされており、各エンドポイントのページでは 利用可能なデータのフィールド・型・制約 などがスキーマとして定義されています。
個々のエンドポイント (例:Posts) をクリックすると、そのエンドポイントの スキーマ(データ構造) を確認できます。スキーマは、特定のタイプのデータを取得または作成するときに、リソースに含まれるすべてのフィールドを定義します。
カスタム投稿タイプを作成した場合、そのエンドポイント(例:/wp/v2/books)は、デフォルトの posts エンドポイントに類似したスキーマを持ちます。これは、投稿とカスタム投稿タイプが WordPress の posts テーブルを共有しているためです。
エンドポイントのフィールドの多くは、対応する WordPress のデータベーステーブルのフィールドと一致していますが、一部異なる場合があります。例えば、posts エンドポイントの title フィールドは、データベースの post_title フィールドに対応します。この違いを理解し、API を操作するときは正しいフィールド名を使用する必要があります。
Context
context はエンドポイントでデータの取得モードを指定するプロパティです。
例えば、GET /wp/v2/posts のリクエスト時に context を指定すると、返されるデータの詳細レベル(取得できるデータ)が変わります。
context の値 | 説明 | 使用例 |
---|---|---|
view | デフォルト。公開情報のみ取得(認証不要) | 一般的な投稿・ページの取得 |
embed | 軽量データ(抜粋) | API レスポンスを軽くする |
edit | 非公開データ含む全ての情報を取得(認証必要) | 下書き・カスタムフィールドの取得 |
- GET /wp-json/wp/v2/posts?context=view ( 一般公開情報。デフォルト)
- GET /wp-json/wp/v2/posts?context=embed (軽量版データ)
- GET /wp-json/wp/v2/posts?context=edit (非公開データも取得)
context は REST API のレスポンスデータを制御するオプションで、デフォルトは view(公開情報のみ)です。context を指定しない場合は、デフォルトの view が適用されます。
管理者として詳細なデータを取得したい場合は edit(認証必須)を、API レスポンスを軽量化したい(取得するデータを少なくする)場合は embed を指定することができます。
また、グローバルパラメータの _fields を使えば、必要なフィールドのみを取得できるのでレスポンスの軽量化により パフォーマンスが向上します。例えば、_fields=title,link を指定すればタイトルとリンクのフィールドだけを取得することができ、この場合は、context=embed を指定する必要はありません。
例えば、投稿タイプ(Types)のエンドポイントで capabilities のデータを取得するには、context に edit を指定する必要があり、認証が必要になります。
WP REST API を使ってみる
WP REST API(WordPress REST API)を使用して WordPress サイトからデータを取得する例です。
この例では、book というカスタム投稿タイプを登録するプラグイン booklist を作成し、WP REST API を使用して book カスタム投稿タイプのデータを操作します。
プラグインディレクトリ(wp-content/plugins/)に booklist というフォルダを作成し、booklist.php という名前のプラグインファイルを作成します。
カスタム投稿タイプを登録
booklist.php に以下を記述して、book というカスタム投稿タイプを登録します。
カスタム投稿タイプを登録する関数 register_post_type() では、show_in_rest に true を設定してこのカスタム投稿タイプが REST API に公開されるようにします(32行目)。
これにより、wp-json/wp/v2/book ルートを参照すると、レスポンスにカスタム投稿タイプ book のデータが表示されます。
また、book ルートからは、実際には複数の book のデータを取得するので、rest_base を books(複数形)に設定して、wp-json/wp/v2/books ルートを参照するようにしています(33行目)。
その他にも rest_namespace を設定すると、REST API ルートの namespace URL(デフォルトは wp/v2)を変更することもできます。例えば、33行目のコメントを外すと、wp-json/wp/v2/books の代わりに wp-json/booklist/books でアクセスできます。
<?php
/**
* Plugin Name: Booklist
* Description: A plugin to manage books
* Version: 0.0.1
*
*/
if (! defined('ABSPATH')) {
exit; // Exit if accessed directly.
}
// カスタム投稿タイプ book の登録
add_action('init', 'booklist_register_book_post_type');
function booklist_register_book_post_type() {
$args = array(
'labels' => array(
'name' => 'Books',
'singular_name' => 'Book',
'menu_name' => 'Books',
'add_new' => 'Add New Book',
'add_new_item' => 'Add New Book',
'new_item' => 'New Book',
'edit_item' => 'Edit Book',
'view_item' => 'View Book',
'all_items' => 'All Books',
),
'public' => true,
'has_archive' => true,
'menu_icon' => 'dashicons-book',
'show_in_rest' => true, // REST API に公開する
'rest_base' => 'books', // REST API route の base URL を book から books に変更
//'rest_namespace' => 'booklist',
'supports' => array('title', 'editor', 'author', 'thumbnail', 'excerpt'),
);
register_post_type('book', $args);
}
プラグインを有効化して、book カスタム投稿タイプの投稿をいくつか作成しておきます。
管理用サブメニューページを作成
カスタム投稿タイプ book のデータを取得して、タイトルのリストを表示するページをダッシュボード(管理画面)に追加します。
以下を booklist.php に追加します。
add_submenu_page() を使って、管理画面のサイドバーの book カスタム投稿タイプのメニューにサブメニューを追加し、サブメニューページ(Book List 管理ページ)を作成します。
add_submenu_page() は admin_menu にフックし、サブメニューページのコンテンツ(HTML)は第6引数に指定したコールバック関数で描画(出力)し、コンテンツは div.wrap でラップします。
// サブメニューページ(Book List 管理ページ)を追加
add_action('admin_menu', 'booklist_add_submenu', 11);
function booklist_add_submenu() {
add_submenu_page(
'edit.php?post_type=book', // 親メニューのスラッグ
'Book List', // サブメニューページのタイトル
'Book List', // サブメニューページのメニューのタイトル
'edit_posts', // サブメニューページにアクセスできるユーザーの権限
'book-list', // サブメニューページを参照するスラッグ
'booklist_render_book_list' // サブメニューページを描画するコールバック関数
);
}
// サブメニューページの HTML を描画するコールバック関数
function booklist_render_book_list() {
?>
<div class="wrap" id="booklist-admin">
<h1>Actions</h1>
<button id="booklist-load-books">Load Books</button>
<h2>Books</h2>
<textarea id="booklist-listarea" cols="80" rows="15"></textarea>
</div>
<?php
}
add_submenu_page() の第1引数には親メニューのスラッグを指定します。この例の場合、カスタム投稿タイプ book のサブメニューなので edit.php?post_type=book のように指定します。
[参考]他の組み込みメニューの親スラッグには以下のようなものがあります。※ 設定のサブメニューページを作成する場合は、add_options_page() を使うこともできます。
組み込みメニュー | スラッグ |
---|---|
ダッシュボード | index.php |
投稿 | edit.php |
メディア | upload.php |
固定ページ | edit.php?post_type=page |
コメント | edit-comments.php |
テーマ | themes.php |
プラグイン | plugins.php |
ユーザ | users.php |
ツール | tools.php |
設定 | options-general.php |
カスタム投稿タイプ book のメニュー「Books」の下にサブメニュー「Book List」が追加され、クリックすると以下のようなサブメニューページ(Book List 管理ページ)が表示されます。
管理画面(サブメニューページ)に JavaScript を読み込む
REST API を使ってデータを取得するための JavaScript ファイル booklist-admin.js を作成します。
booklist.php に以下を追加して、作成した JavaScript ファイル booklist-admin.js を読み込みます。
管理画面で CSS や JavaScript をエンキューする(管理ダッシュボードでのみ読み込まれるようにする)には、admin_enqueue_scripts アクションフックを使用します。
以下では現在表示されている管理ページの識別子(スラッグ)を $hook に受取り、スラッグの値を調べて、このサブメニューページでのみ JavaScript を読み込むようにしています。
// admin_enqueue_scripts アクションフックを使用
add_action('admin_enqueue_scripts', 'booklist_admin_enqueue_scripts');
// 現在表示されている管理ページのスラッグを $hook に受取る
function booklist_admin_enqueue_scripts($hook) {
// サブメニューページのスラッグ
$allowed_pages = ['book_page_book-list'];
// $hook が対象のページ(サブメニューページ)であれば JavaScript を読み込む
if (in_array($hook, $allowed_pages)) {
wp_enqueue_script(
'booklist-admin',
plugin_dir_url(__FILE__) . '/booklist-admin.js',
array(),
'1.0.0',
true
);
}
}
[参考]管理画面(この場合はサブメニューページ)のスラッグは、以下を記述して管理画面にアクセスすると、右上にスラッグが表示されます(確認したら忘れずにに削除します)。
add_action('admin_enqueue_scripts', function($hook) {
echo '<h3 style="float:right; margin-right:30px;">スラッグ: '. $hook .'</h3>';
});
booklist-admin.js に alert( 'Hello from the Book list admin' ); と記述して、Book List 管理ページでのみ、アラートが表示されるのが確認できたら削除します。
関連ページ:CSS や JavaScript の読み込み
WP REST API へリクエストを送信
WP REST API のエンドポイントへ HTTP リクエストを送信するには、以下のような方法があります。
- Fetch API を使用
- Backbone.js client を使用
- @wordpress/fetch-api を使用
- @wordpress/core-data を使用
Fetch API
JavaScript の fetch() メソッドを使って HTTP リクエストを送信する例です。
他の3つの方法とは異なり、WordPress に依存しないので、WordPress がインストールされていない環境でも使用することもできます。
以下は投稿(posts)リソースのエンドポイント /wp-json/wp/v2/posts に GET リクエストを送信して、取得したデータをコンソールに出力する例です。
メソッドのデフォルトは GET なので、{method: 'GET'} は省略可能です。
また、エンドポイントの URL の example.com の部分は環境に合わせて適宜変更します。
fetch('http://example.com/wp-json/wp/v2/posts', {method: 'GET'})
.then(response => response.json()) // レスポンスを JSON として解析
.then(data => {
console.log(data); // 投稿のデータをコンソールに出力
});
上記を JavaScript ファイル booklist-admin.js に記述して、Book List 管理ページでコンソールを確認すると、以下のように投稿のデータが出力されます。
投稿のデータ(data)には、各投稿のデータが配列で格納されています。
以下は最初(インデックスが 0)の投稿のデータを展開して表示しています。コンテンツは content.rendered、リンクは link 、タイトルは title.rendered でアクセスできます。
上記の JavaScript を削除して、以下を記述します。
以下はサブメニューページで、ボタンをクリックすると、カスタム投稿タイプ book のエンドポイントからデータを取得して、データのタイトルとリンクをテキストエリアに出力するコードです。
カスタム投稿タイプのエンドポイントは /wp-json/wp/v2/{custom_post_type} になります。
この例の場合、カスタム投稿タイプのスラッグは book ですが、カスタム投稿タイプの登録で、rest_base を books(複数形)に設定しているので、エンドポイントは /wp-json/wp/v2/books になります。
// ボタン
const loadButton = document.getElementById("booklist-load-books");
// テキストエリア
const textarea = document.getElementById("booklist-listarea");
if (loadButton && textarea) {
// ボタンにクリックイベントのリスナーを設定
loadButton.addEventListener("click", () => {
// fetch() メソッドを使って HTTP リクエストを送信 ( example.com の部分は適宜変更します)
fetch("http://example.com/wp-json/wp/v2/books")
.then((response) => response.json())
.then((books) => {
textarea.value = ""; // 既存の内容をクリア
// 配列のデータ books を forEach で処理
books.forEach((book) => {
// テキストエリアに各投稿のタイトルとパーマリンクを出力
textarea.value += `${book.title.rendered}, ${book.link}\n`;
});
});
});
}
Book List 管理ページで、「Load Books」ボタンをクリックすると、テキストエリアにカスタム投稿タイプ book の各投稿のタイトルとパーマリンクのリストが表示されます。
Backbone.js client
WordPress には、Backbone.js を使って REST API に直接リクエストを送信できる仕組み(REST API JavaScript Client)が組み込まれています。これを利用すると、特別な設定をしなくても WordPress のデータ(投稿やユーザーなど)を取得・更新・削除できます。
但し、現在では次項の @wordpress/fetch-api パッケージを利用することが多いです。
モデルとコレクション
Backbone.js では、データの構造を「モデル」、データの集まりを「コレクション」という形で扱います。
- モデル(Model):記事(Post)やユーザー(User)など、個々のデータを表す
- コレクション(Collection):記事一覧やユーザー一覧など、複数のデータの集合を表す
WordPress の REST API によって提供されるデータは、このモデルとコレクションを使って管理します。
wp-api を依存関係に追加
Backbone.js を利用するには、wp-api を依存関係に追加する必要があります。
wp-api を依存関係に追加すると、wp-api.js(WordPress の REST API クライアント)が 自動で読み込まれ、以下のようなオブジェクトが JavaScript で使えるようになります。
- wp.api.models:投稿 (Posts)、ページ (Pages)、ユーザー (Users) などの個別データを扱う
- wp.api.collections:投稿一覧 (Posts)、ページ一覧 (Pages) などのリストデータを扱う
- wp.api.loadPromise:wp-api の読み込みが完了したことを確認するための Promise
プラグインファイルでの booklist-admin.js の読み込みで依存関係に 'wp-api' を追加します。
add_action('admin_enqueue_scripts', 'booklist_admin_enqueue_scripts');
function booklist_admin_enqueue_scripts($hook) {
$allowed_pages = ['book_page_book-list'];
if (in_array($hook, $allowed_pages)) {
wp_enqueue_script(
'booklist-admin',
plugin_dir_url(__FILE__) . '/booklist-admin.js',
array('wp-api'), // 依存関係を追加
'1.0.0',
true
);
}
}
以下は投稿のデータを取得してコンソールに出力する例です。
wp.api.collections は WordPress の REST API の コレクション(データのリスト) を管理するオブジェクトが格納されています。new wp.api.collections.Posts() で投稿一覧を管理するための Backbone.Collection インスタンスを作成します(この時点ではデータはまだ取得されていません)。
そして、作成した posts インスタンスに対して .fetch() を実行すると、/wp-json/wp/v2/posts へリクエストを送り、データが取得されると、コレクション (posts) に自動で追加され、WordPress の REST API から投稿データを取得できます。
const posts = new wp.api.collections.Posts();
posts.fetch().then(function (collection) {
console.log(collection); // 投稿のデータをコンソールに出力
});
以下は、Book List 管理ページで、「Load Books」ボタンをクリックすると、テキストエリアにカスタム投稿タイプ book の各投稿のタイトルとパーマリンクのリストを表示するコードを Backbone.js を使って書き換えたものです。
const loadButton = document.getElementById("booklist-load-books");
const textarea = document.getElementById("booklist-listarea");
if (loadButton && textarea) {
loadButton.addEventListener("click", () => {
// book 一覧を管理するための Backbone.Collection インスタンスを作成
const allBooks = new wp.api.collections.Books();
allBooks.fetch().then((books) => {
textarea.value = "";
books.forEach((book) => {
textarea.value += `${book.title.rendered}, ${book.link}\n`;
});
});
});
}
@wordpress/fetch-api
WordPress 5.0 でブロックエディターが導入されて以来、REST API にリクエストを送るための @wordpress/fetch-api パッケージが利用できるようになりました。
WordPress では、従来 Backbone.js ベースの wp-api を使って REST API にアクセスしていましたが、現在では @wordpress/fetch-api を使うほうが推奨されるケースが増えています。
@wordpress/fetch-api のメリット
- シンプルな API で扱いやすい
- 標準的な fetch に近い
- Backbone のモデル・コレクションを理解しなくてもよい
- 標準 fetch() に近く、ネイティブの挙動に沿っている
- 標準の fetch() をラップしただけ なので、JavaScript に詳しい開発者には直感的
- fetch() と同じように Promise ベースで動作するため、非同期処理を扱いやすい
- カスタム REST API にも柔軟に対応
- Backbone.js (wp-api) は WordPress の標準エンドポイント向けに設計 されている
- カスタムエンドポイントを作る場合、@wordpress/fetch-api のほうが使いやすい
- WordPress のセキュリティ機能と統合しやすい
- @wordpress/fetch-api は WordPress の認証システム(nonce など)と統合が簡単
- Backbone.js (wp-api) では、手動で nonce を管理する必要がある
- 不要な依存関係を減らせる
- Backbone.js を使うと、Backbone.js と Underscore.js に依存する
- @wordpress/fetch-api は 依存関係が少なく、軽量
wp-api-fetch を依存関係に追加
@wordpress/fetch-api を使用するには、JavaScript 依存関係に wp-api-fetch を追加する必要があります。プラグインファイルの booklist-admin.js の読み込みで依存関係に 'wp-api-fetch' を追加します。
add_action('admin_enqueue_scripts', 'booklist_admin_enqueue_scripts');
function booklist_admin_enqueue_scripts($hook) {
$allowed_pages = ['book_page_book-list'];
if (in_array($hook, $allowed_pages)) {
wp_enqueue_script(
'booklist-admin',
plugin_dir_url(__FILE__) . '/booklist-admin.js',
array('wp-api-fetch'), // 依存関係に wp-api-fetch を追加
'1.0.0',
true
);
}
}
wp.apiFetch()
wp.apiFetch() は WordPress の @wordpress/fetch-api パッケージの関数で、WordPress の REST API を簡単に操作できるように設計されています。この関数は fetch API のラッパーであり、WordPress の非同期リクエストを統一的に処理します。
wp.apiFetch() の書式
以下は wp.apiFetch() の基本的な書式です。
options | オブジェクト形式でリクエストの設定を指定。以下のオプションが利用できます。また、method や headers などの fetch() のすべてのリソースオプションをサポートしています。 |
---|---|
successCallback | リクエスト成功時のコールバック関数 |
errorCallback | エラー時のコールバック関数 |
オプション | 型 | 説明 |
---|---|---|
path | string | REST API のエンドポイント(例: /wp/v2/posts)。path に指定したエンドポイントには自動的に /wp-json/ が付与されます。 |
url | string | path の代わりに完全な URL を指定することで、WordPress 以外のエンドポイントにもアクセスできます。 |
data | object | リクエスト時に送信するデータのオブジェクト(POST や PUT の場合) |
parse | boolean | true(デフォルト)なら JSON をパース(解析)して返す |
method | string | HTTP メソッド(GET, POST, PUT, DELETE など)デフォルトは GET |
headers | object | カスタム HTTP ヘッダー(例: { 'X-Custom-Header': 'value' }) |
apiFetch の Promise 戻り値
fetch() とは異なり、apiFetch の Promise 戻り値は解析された JSON 結果に解決されます。
つまり、Promise 戻り値を json() を使って解析された JSON 結果に変換する必要がありません。
※ ヘッダー情報を取得する場合(総ページ数や投稿数などを取得する場合)など、レスポンスオブジェクトを直接取得する必要がある場合は、parse オプションを false として渡すことで、この動作を無効にして処理する必要があります。
data オプション
apiFetch の data はデフォルトで application/json を扱うようになっており、データの処理が簡単になっています。wp.apiFetch() では data にそのままオブジェクトを渡すだけで、内部で JSON 変換と Content-Type の設定を自動的に行ってくれます(投稿を作成 POST)。
以下は投稿のデータを取得してコンソールに出力する例です。
wp.apiFetch({ path: '/wp/v2/posts' }).then((posts) => {
console.log(posts); // 投稿のデータ(JSON)をコンソールに出力
}).catch((error) => {
console.error('Error:', error);
});
以下はオブジェクト形式のパラメータを URLSearchParams を使ってクエリ文字列に変換して、path に結合してリクエストを送る例です。※ addQueryArgs() を使ってパラメータを追加することもできます。
// クエリパラメータを作成
const params = {
per_page: 5, // 5件表示(デフォルト10)
context: "embed", // content の内容などを含めない(デフォルト 'view')
order: "asc", // 古い記事から表示(デフォルト 'desc')
};
// URLSearchParams でクエリ文字列を作成
const queryString = new URLSearchParams(params).toString();
// path にクエリ文字列を直接追加
wp.apiFetch({ path: `/wp/v2/posts?${queryString}` })
.then((posts) => console.log(posts))
.catch((error) => console.error("エラー:", error));
以下は、Book List 管理ページで、「Load Books」ボタンをクリックすると、テキストエリアにカスタム投稿タイプ book の各投稿のタイトルとパーマリンクのリストを表示するコードを @wordpress/fetch-api を使って書き換えたものです。
const loadButton = document.getElementById("booklist-load-books");
const textarea = document.getElementById("booklist-listarea");
if (loadButton && textarea) {
loadButton.addEventListener("click", () => {
wp.apiFetch({ path: "/wp/v2/books" })
.then((books) => {
textarea.value = "";
books.map((book) => {
textarea.value += `${book.title.rendered}, ${book.link}\n`;
});
})
.catch((error) => console.error("Failed to fetch books:", error));
});
}
@wordpress/core-data
ブロックを開発している場合は、REST API からデータにアクセスするための core-data パッケージも利用できます。
core-data は、コア WordPress エンティティへのアクセスと操作を簡素化することを目的としています。独自のストアを登録し、WordPress REST API からのデータを自動的に解決するセレクターをいくつか提供し、データを操作するためのアクション クリエーターをディスパッチします。
core-data は、React のさまざまな機能を利用するため、通常、ブロックのコンテキストで使用します。
以下は、ブロックで core-data モジュールを使用して、REST API から book カスタム投稿タイプのデータを取得する例です。単にエディター側で動作を確認するだけのものです。
以下を行うには Node.js(npm)がインストールされている必要があります。
まず、create-block ツールを使用して、booklist-block という新しいブロックを作成します。ターミナルでプラグインディレクトリ(wp-content/plugins)に移動して以下を実行します。
% npx @wordpress/create-block@latest --variant=dynamic booklist-block
プラグインディレクトリにブロックの雛形(booklist-block)が作成されます。
作成されたディレクトリに移動して、npm start を実行して開発を開始します。
% cd booklist-block
% npm start
src ディレクトリの edit.js を以下のように書き換えます。
@wordpress/data パッケージから useSelect フックをインポートします。
useSelect は、登録されたセレクタからデータを取得できるフックです。
useSelect は最初の引数としてコールバック関数を受け入れ、core ストアの getEntityRecords セレクタを使用して REST API から book の投稿データを取得します。
取得した投稿データは books 変数に格納されます。
そして、books オブジェクトをループして book のタイトルとリンクを出力しています。
import { __ } from "@wordpress/i18n";
import { useBlockProps } from "@wordpress/block-editor";
import "./editor.scss";
// @wordpress/data から useSelect をインポート
import { useSelect } from "@wordpress/data";
export default function Edit() {
// core ストアから postType が book の投稿データを取得
const books = useSelect(
// select 関数で core ストアにアクセスして getEntityRecords セレクターで book の投稿データを取得
(select) => select("core").getEntityRecords("postType", "book"),
[],
);
// 取得したデータ(books)を map() でループして各投稿のタイトルにリンクを設定してレンダリング
return (
<div {...useBlockProps()}>
{books &&
books.map((book) => (
<p>
<a href={book.link}>{book.title.rendered}</a>
</p>
))}
</div>
);
}
プラグインを有効化して、任意の投稿に作成したブロック(Booklist Block)を挿入すると、例えば以下のように book の投稿のリストが表示されます。
[プラグインファイルでのエラー]@wordpress/create-block@4.63.0 で生成されたブロックの雛形のプラグインファイルでは、register_block_type によるブロックの登録が変更になっていてエラーになってしまいました。More efficient block type registration in 6.8
そのため、取り敢えず以下のように以前の登録方法に変更しています。
<?php
/**
* Plugin Name: Booklist Block
* Description: Example block scaffolded with Create Block tool.
* Version: 0.1.0
* Requires at least: 6.7
* Requires PHP: 7.4
* Author: The WordPress Contributors
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: booklist-block
*
* @package CreateBlock
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// 以下に変更
function create_block_booklist_block_block_init() {
register_block_type( __DIR__ . '/build/booklist-block' );
}
add_action( 'init', 'create_block_booklist_block_block_init' );
@wordpress/core-data の使い方やダイナミックブロックの作成方法については、以下を御覧ください。
Authentication(認証)
WordPress REST API の認証方法
デフォルトでは、WordPress REST API は、WordPress ダッシュボードへのログイン時に使用されるのと同じCookie ベースの認証を使用します。
認証が必要な REST API エンドポイント(非公開データの取得やデータの変更を伴うリクエストなど)には、この認証 Cookie が必要です。
たとえば、ブロックエディター(Gutenberg)は、この仕組みを利用して API にアクセスします。
その他の認証方法
REST API には、Cookie ベースの認証以外にも、いくつかの認証方法が用意されています。主なものとして、以下の方法があります。
- アプリケーションパスワード(Application Passwords)
- JSON Web Token(JWT) プラグイン
- OAuth 1.0a プラグイン
アプリケーションパスワードの設定方法
アプリケーションパスワードは、各ユーザーごとに設定可能で、WordPress ダッシュボードのパスワードを共有せずに API 認証を行うことができる WordPress の組み込み機能です。
手順:
- ダッシュボードの 「ユーザー」 メニューをクリックし、対象のユーザーを選択して、プロフィール編集画面を開く。
- 画面の下部にある 「アプリケーションパスワード」 セクションへ移動。
- 新しいアプリケーションパスワードの名前(任意の名前)を入力し、「新しいアプリケーションパスワードの追加」ボタンをクリック。
- パスワードが自動生成されるので、安全な場所にコピーして保管します。(一度閉じると再表示できない)
- 必要に応じて、漏洩時にはここからパスワードを無効化できます。また、パスワードを忘れた場合は、取り消して再作成できます。
アプリケーションパスワードは、REST API のテストツールでリクエストをテストする際にも便利です。
ローカル環境の場合
アプリケーションパスワードの生成は原則として HTTPS が有効になっている必要があります。HTTPS が無効なサイトでは以下のような説明が表示されます。
開発時のローカル環境でアプリケーションパスワードを使用する場合は、設定ファイル wp-config.php で環境タイプ WP_ENVIRONMENT_TYPE を local に設定すれば利用できます。
define( 'WP_ENVIRONMENT_TYPE', 'local' ); // 環境タイプ WP_ENVIRONMENT_TYPE を local に設定
より高度な認証が必要な場合
モバイルアプリや外部サービスとの連携など、より高度な認証が求められる場合は、JSON Web Token(JWT) 認証や OAuth 1.0a などの利用を検討します。
OAuth は、複数のユーザーが API を安全に利用できるようにするための標準的な認証プロトコルです。
REST API テストツール
REST API リクエストをテストするために利用できるツールは多数あります。
例えば、PhpStorm を使用する場合は HTTP クライアントが組み込まれており、VS Code を使用する場合は Postcode などの拡張機能があります。
また、Hoppscotch や Postman などのスタンドアロンツールもあります。ターミナルで curl コマンドを使用して REST API エンドポイントをテストすることもできます。
Postman
Postman は GUI で API をテストするツールの定番の1つです。
Postman を使用すると、HTTP リクエストを作成し、API エンドポイントに送信することができるので、REST API の動作をテストしたり、リクエストとレスポンスのデータを確認したりできます。
ダウンロードしてインストールして使用するか、Weアプリを使用することができ、制限はありますが無料で利用することができます。
ダウンロードして使う場合、インストーラを実行後、アカウント作成画面が表示されますが、登録しなくとも API リクエストの実行など基本的な機能は使用できます。試しに Postman を使用してみたい場合はスキップして、「Continue without an account」をクリックして使用できます。
「Continue without an account」をクリックすると、以下のような画面が表示されます。アカウントを登録しない場合は、Workspaces や Collections などの機能は使えませんが、リクエストを送信してテストすることはできます。
投稿一覧を取得(GET)
例えば、カスタム投稿タイプ book のエンドポイント「http://xxxxxx/wp-json/wp/v2/books」を URL 入力欄に入力し、「Send」をクリックするとリクエストが送信されてレスポンスエリアに JSON レスポンスが表示されます。
新しいリクエストを作成するには、タブの「+」をクリックします。または、同じタブでメソッドやエンドポイントの URL を変更してリクエストすることもできます。
新規投稿を作成(POST)
今度はメソッドを POST に変更し、「Send」をクリックして同じエンドポイントにリクエストを送信してみます。今回は認証されていないため、ステータスが 401 でエラーメッセージが表示されます。
リクエストを認証するには、Auth タブをクリックし、ドロップダウンから Basic Auth を選択します。そしてユーザー名と作成したアプリケーションパスワードを入力します。
再度リクエストを送信すると、今度はコンテンツを指定していないので、ステータスが 400 で「本文、タイトル、抜粋が空欄です。」というメッセージが表示されます。
Body タブをクリックし、raw ラジオボタンを選択して作成します。次に、ドロップダウンから JSON を選択し、例えば以下のような JSON を入力します。
{
"title": "My Postman Book",
"content": "This is my Postman book",
"status": "publish"
}
「Send」をクリックします。
新規に book の投稿が作成され、作成した book の JSON レスポンスが返されます。
ダッシュボードで book カスタム投稿タイプを確認すると、作成した投稿が表示されるはずです。
投稿を更新(PUT)
book を更新するには、book を追加するときと同じリクエスト構成を使用しますが、エンドポイント URL を変更して book の ID を含め、メソッドを PUT に変更します。Body タブで必要に応じてタイトルやコンテンツを変更します。
投稿を削除(DELETE)
book を削除するには、book を更新する場合と同じエンドポイント URL を使用しますが、リクエストメソッドを DELETE に変更し、リクエスト本文でデータを送信しません。
また、投稿を削除すると、実際には投稿がゴミ箱に移動され、完全に削除されるわけではありません。これは、WordPress ダッシュボードの動作と一致します。
wp.apiFetch() を使ってリクエストを送信
「WP REST API を使ってみる」で作成したプラグイン booklist に、wp.apiFetch() を使って投稿を作成、更新、削除する機能を追加します。
以下は現時点でのプラグインファイル(booklist.php)です。
プラグインファイルでは、カスタム投稿タイプ book の登録、サブメニューページ(Book List 管理ページ)の追加とそのマークアップの出力、及び JavaScript ファイルの読み込みが記述されています。
<?php
/**
* Plugin Name: Booklist
* Description: A plugin to manage books
* Version: 0.0.1
*
*/
if (! defined('ABSPATH')) {
exit; // Exit if accessed directly.
}
// カスタム投稿タイプ book の登録
add_action('init', 'booklist_register_book_post_type');
function booklist_register_book_post_type() {
$args = array(
'labels' => array(
'name' => 'Books',
'singular_name' => 'Book',
'menu_name' => 'Books',
'add_new' => 'Add New Book',
'add_new_item' => 'Add New Book',
'new_item' => 'New Book',
'edit_item' => 'Edit Book',
'view_item' => 'View Book',
'all_items' => 'All Books',
),
'public' => true,
'has_archive' => true,
'menu_icon' => 'dashicons-book',
'show_in_rest' => true, // REST API に公開する
'rest_base' => 'books', // REST API route の base URL を book から books に変更
'supports' => array('title', 'editor', 'author', 'thumbnail', 'excerpt'),
);
register_post_type('book', $args);
}
// サブメニューページ(Book List 管理ページ)を追加
add_action('admin_menu', 'booklist_add_submenu', 11);
function booklist_add_submenu() {
add_submenu_page(
'edit.php?post_type=book',
'Book List',
'Book List',
'edit_posts', // 管理者のみに制限する場合は 'manage_options'
'book-list',
'booklist_render_book_list'
);
}
// サブメニューページのHTMLを描画するコールバック関数
function booklist_render_book_list() {
?>
<div class="wrap" id="booklist-admin">
<h1>Actions</h1>
<button id="booklist-load-books">Load Books</button>
<h2>Books</h2>
<textarea id="booklist-listarea" cols="70" rows="10"></textarea>
</div>
<?php
}
// サブメニューページに JavaScript ファイル(booklist-admin.js)を読み込む
add_action('admin_enqueue_scripts', 'booklist_admin_enqueue_scripts');
function booklist_admin_enqueue_scripts($hook) {
$allowed_pages = ['book_page_book-list'];
if (in_array($hook, $allowed_pages)) {
wp_enqueue_script(
'booklist-admin',
plugin_dir_url(__FILE__) . '/booklist-admin.js',
array('wp-api-fetch'), // 依存関係に wp-api-fetch を追加
'1.0.0',
true
);
}
}
投稿を取得(GET)
以下は現時点でのBook List 管理ページで読み込んでいる JavaScript(booklist-admin.js)です。
「Load Books」ボタンをクリックすると、wp.apiFetch() を使ってカスタム投稿タイプ book の各投稿のタイトルとパーマリンクを取得してそのリストを表示します。
wp.apiFetch() オプションのメソッドは省略していますが、デフォルトの GET が適用されます。
// 「Load Books」ボタン
const loadButton = document.getElementById("booklist-load-books");
// 出力先のテキストエリア
const textarea = document.getElementById("booklist-listarea");
if (loadButton && textarea) {
loadButton.addEventListener("click", () => {
wp.apiFetch({ path: "/wp/v2/books" })
.then((books) => {
// 出力先のテキストエリアの値をクリア
textarea.value = "";
books.map((book) => {
textarea.value += `${book.title.rendered}, ${book.link}\n`;
});
})
.catch((error) => console.error("Failed to fetch books:", error));
});
}
booklist-admin.js を以下のように書き換え、投稿 ID も表示するようにします。
エンドポイント wp/v2/books のレスポンスには content や excerpt などの不要なデータが含まれるため、以下では _fields グローバルパラメータを指定して、必要な id, title, link のみ取得することでデータ量を減らすようにしています。
また、textarea.value += ... をループするのではなく、map() で配列を作成し、join("\n") で改行を挿入しつつ結合することで、文字列結合の処理を最適化 しています。
以下の場合、map() は ["タイトル(ID)リンク", "タイトル(ID)リンク",...]
のような新しい配列を作るので、.join("\n") を使って配列を 1つの文字列に結合しています。
const loadButton = document.getElementById("booklist-load-books");
const textarea = document.getElementById("booklist-listarea");
if (loadButton && textarea) {
loadButton.addEventListener("click", () => {
// 必要なフィールドのみを指定してデータを取得(不要なデータの取得を除外)
wp.apiFetch({ path: "/wp/v2/books?_fields=id,title,link" })
.then((books) => {
textarea.value = books
.map((book) => `${book.title.rendered} (${book.id}) ${book.link}`) // 投稿 ID も表示
.join("\n"); // `map()` + `join()` で文字列を直接生成
})
.catch((error) => console.error("Failed to fetch books:", error));
});
}
Load More ボタンを追加
ページネーションのパラメータ per_page(1ページあたりの取得件数)のデフォルトは10件です。そのため現時点では、投稿が10件以上ある場合は、最新の投稿10件だけを取得して表示します。
per_page に指定した件数以上の投稿がある場合は追加で投稿を取得できるように、追加で読み込むための「Load More」ボタンを配置します。
HTML マークアップの変更
HTMLを描画するコールバック関数に Load More ボタンを追加します。Load More ボタンは、初期状態では style="display:none;" を指定して非表示にします。
// サブメニューページのHTMLを描画するコールバック関数
function booklist_render_book_list() {
?>
<div class="wrap" id="booklist-admin">
<h1>Actions</h1>
<div>
<button id="booklist-load-books">Load Books</button>
</div>
<h2>Books</h2>
<div>
<textarea id="booklist-listarea" cols="70" rows="20"></textarea>
</div>
<div>
<button id="booklist-load-more" style="display:none;">Load More</button>
</div>
</div>
<?php
}
JavaScript の変更
booklist-admin.js を以下のように書き換えます。
投稿を取得する処理を関数 loadBooks() として定義します。
この関数では現在のページ番号(currentPage)を引数として受け取るようにし、最初に投稿を読み込む際(Load Books ボタンがクリックされた時)はページ番号として 1 を渡し、追加で投稿を読み込む Load More ボタンがクリックされた際は、クリックされる度にページ番号を1ずつ増加して渡します。
そして、エンドポイントに page パラメータと per_page パラメータを設定し、それぞれ現在のページ番号と1ページあたりの取得件数を指定します。
総ページ数は、取得したデータには含まれていないので、レスポンスヘッダー X-WP-TotalPages から取得し、変数 totalPages に格納します。
Load More ボタンは、現在のページ番号が総ページ数より小さい場合は表示するようにします。
let currentPage = 1; // 現在のページ(最初は1)
const loadButton = document.getElementById("booklist-load-books");
const loadMoreButton = document.getElementById("booklist-load-more");
const textarea = document.getElementById("booklist-listarea");
// 1ページあたりの取得件数
const perPage = 10;
function loadBooks(page = 1) {
wp.apiFetch({
// page と per_page のパラメータを追加
path: `/wp/v2/books?_fields=id,title,link&page=${page}&per_page=${perPage}`,
parse: false // JSON を解析しない(レスポンスヘッダーを取得するために必要)
})
.then((response) => {
// 総ページ数
const totalPages = parseInt(response.headers.get("X-WP-TotalPages"), 10);
return response.json().then((books) => {
// 取得した投稿のデータがあれば
if (books.length > 0) {
const newLine = page === 1 ? "" : "\n"; // 改行文字
textarea.value += newLine + books
.map((book) => `${book.title.rendered} (${book.id}) ${book.link}`)
.join("\n");
// 「Load More」ボタンの表示判定(現在のページが最後のページ未満なら表示)
loadMoreButton.style.display = currentPage < totalPages ? 'inline-block' : 'none';
} else {
loadMoreButton.style.display = 'none';
}
});
})
.catch((error) => console.error("Failed to fetch books:", error));
}
// 最初に投稿を読み込む(Load Books ボタンがクリックされた時の動作)
loadButton.addEventListener("click", () => {
textarea.value = ''; // 既存の投稿を消去
currentPage = 1; // ページをリセット
loadBooks(currentPage);
});
// Load More ボタンがクリックされた時の動作
loadMoreButton.addEventListener("click", () => {
currentPage++; // 次のページ
loadBooks(currentPage);
});
総ページ数
総ページ数や取得したレコードの合計数は、レスポンスのヘッダーから取得する必要があります。
wp.apiFetch() は通常 JSON を自動的に解析しますが、ヘッダー情報を取得するには、オプションの parse: false を設定してレスポンスオブジェクトを直接取得する必要があります。これにより、ヘッダー情報を response.headers.get() で取得することができます(上記17行目)。
そのため、上記では response.json() でレスポンスを JSON として解析しています(19行目)。
以下は1ページあたりの取得件数を10とした場合の投稿の総ページ数と投稿の総数を出力する例です。
wp.apiFetch({
path: "/wp/v2/posts?&per_page=10", // 投稿のエンドポイント
parse: false
}).then((response) => {
const totalPosts = response.headers.get("X-WP-Total");
const totalPages = response.headers.get("X-WP-TotalPages");
console.log("Total Posts:", totalPosts, "Total Pages:", totalPages);
});
並び順を制御
orderby(並び替えの基準)のプルダウンと order(並び順)のラジオボタンを追加し、それに基づいて投稿を取得するようにします。
HTML の更新
HTMLを描画するコールバック関数に、プルダウンとラジオボタンを追加します。
// サブメニューページのHTMLを描画するコールバック関数
function booklist_render_book_list() {
?>
<div class="wrap" id="booklist-admin">
<h1>Actions</h1>
<div>
<label for="booklist-orderby">Order by:</label>
<select id="booklist-orderby">
<option value="date">Date</option>
<option value="title">Title</option>
<option value="modified">Last Modified</option>
<option value="id">ID</option>
</select>
<label>
<input type="radio" name="booklist-order" value="asc"> Ascending
</label>
<label>
<input type="radio" name="booklist-order" value="desc" checked> Descending
</label>
</div>
<div>
<button id="booklist-load-books">Load Books</button>
</div>
<h2>Books</h2>
<textarea id="booklist-listarea" cols="70" rows="20"></textarea>
<div>
<button id="booklist-load-more" style="display:none;">Load More</button>
</div>
</div>
<?php
}
JavaScript の更新
booklist-admin.js を以下のように書き換えます。
選択された orderby と order の値に基づいて投稿を取得するように、エンドポイントのパラメータに orderby と order を追加します。
let currentPage = 1;
const loadButton = document.getElementById("booklist-load-books");
const loadMoreButton = document.getElementById("booklist-load-more");
const textarea = document.getElementById("booklist-listarea");
const perPage = 10;
// orderby のセレクト要素(プルダウン)
const orderbySelect = document.getElementById("booklist-orderby");
function loadBooks(page = 1) {
// orderby のセレクト要素で選択された値
const orderby = orderbySelect.value;
// order のラジオボタンで選択された値
const order = document.querySelector('[name="booklist-order"]:checked').value;
wp.apiFetch({
// エンドポイントに orderby と order を追加
path: `/wp/v2/books?_fields=id,title,link&page=${page}&per_page=${perPage}&orderby=${orderby}&order=${order}`,
parse: false
})
.then((response) => {
const totalPages = parseInt(response.headers.get("X-WP-TotalPages"), 10);
return response.json().then((books) => {
if (books.length > 0) {
const newLine = page === 1 ? "" : "\n";
textarea.value += newLine + books
.map((book) => `${book.title.rendered} (${book.id}) ${book.link}`)
.join("\n");
loadMoreButton.style.display = currentPage < totalPages ? 'inline-block' : 'none';
} else {
loadMoreButton.style.display = 'none';
}
});
})
.catch((error) => console.error("Failed to fetch books:", error));
}
loadButton.addEventListener("click", () => {
textarea.value = '';
currentPage = 1;
loadBooks(currentPage);
});
loadMoreButton.addEventListener("click", () => {
currentPage++;
loadBooks(currentPage);
});
orderby
投稿やカスタム投稿タイプの orderby には、以下のような値を指定できます。
orderby の値 | 説明 |
---|---|
author | 投稿の 作成者 ID を基準に昇順または降順で並び替える。 |
date | 投稿の 公開日時(post_date) を基準に昇順または降順で並び替える。 |
id | 投稿の ID(post_id) を基準に昇順または降順で並び替える。 |
include | include パラメータで指定された ID の順番通りに並び替える(手動で並び順を決められる)。 |
modified | 投稿の 最終更新日時(post_modified) を基準に昇順または降順で並び替える。 |
parent | 親投稿(post_parent)を基準に並び替える。カスタム投稿タイプや階層構造のある投稿(例: 固定ページ)で使用。 |
relevance | 検索クエリとの関連度 を基準に並び替える。検索時に search パラメータと組み合わせて使用。 |
slug | 投稿の スラッグ(post_name) を基準に昇順または降順で並び替える。 |
include_slugs | include にスラッグを指定した場合、その順番通りに並び替える。 |
title | 投稿の タイトル(post_title) を基準に昇順または降順で並び替える。(ただし、title はアルファベット順ソートのため、数値が含まれると意図しない並び順になる可能性あり)。 |
addQueryArgs
前述のコードでは wp.apiFetch() に指定している path が長くなってきたので、addQueryArgs() を使ってクエリパラメータをより読みやすくし、URL 文字列の組み立てミスを防ぐようにします。
addQueryArgs() は WordPress の @wordpress/url パッケージに含まれる関数で、URL に対して明示的にクエリパラメータを追加できます。
addQueryArgs() を使用するには、@wordpress/url からインポートするか、wp_enqueue_script() の依存関係に wp-url を指定する必要があります。
但し、wp-url は wp-api-fetch の依存関係に含まれているため、この例では wp-api-fetch を依存関係に指定しているので、追加の依存関係の指定は不要です。
以下は addQueryArgs() の基本的な書式です。
import { addQueryArgs } from '@wordpress/url';
const newUrl = addQueryArgs(url, params);
または、依存関係として wp-api-fetch(または wp-url)を指定した場合 は wp.url.addQueryArgs で使用できます。この例では以下の書式を使用します。
const newUrl = wp.url.addQueryArgs(url, params);
引数 | 型 | 説明 |
---|---|---|
url | string | ベースとなる URL |
params | object | 追加・更新するクエリパラメータのオブジェクト |
使用例
クエリパラメータの追加
const url = 'https://example.com/books';
const newUrl = wp.url.addQueryArgs(url, { orderby: 'title', order: 'asc', per_page: 10 });
console.log(newUrl);
// 出力: "https://example.com/books?orderby=title&order=asc&per_page=10"
addQueryArgs() は、元の URL にすでにクエリパラメータがある場合でも適切に追加できます。
const url = 'https://example.com/books?category=fiction';
const newUrl = wp.url.addQueryArgs(url, { orderby: 'title', order: 'asc' });
console.log(newUrl);
// 出力: "https://example.com/books?category=fiction&orderby=title&order=asc"
配列を指定すると、addQueryArgs() は URLSearchParams の仕様に沿って、配列を「キー+インデックス」の形式でエンコードします(デコード後の形は tags[0]=1&tags[1]=2&tags[2]=3 となります)。
const url = 'https://example.com/books';
const newUrl = wp.url.addQueryArgs(url, { tags: [1, 2, 3] });
console.log(newUrl);
// 出力: "https://example.com/books?tags%5B0%5D=1&tags%5B1%5D=2&tags%5B2%5D=3"
例えば、以下は addQueryArgs() を使うと、
wp.apiFetch({
path: `/wp/v2/books?_fields=id,title,link&page=${page}&per_page=${perPage}&orderby=${orderby}&order=${order}`,
parse: false
})
以下のように書き換えることができます。
wp.apiFetch({
// addQueryArgs() で明示的にクエリパラメータを追加
path: wp.url.addQueryArgs("/wp/v2/books", {
_fields: "id,title,link",
page: page,
per_page: perPage,
orderby: orderby,
order: order
}),
parse: false
})
booklist-admin.js の全体のコードは以下のようになります。
let currentPage = 1;
const loadButton = document.getElementById("booklist-load-books");
const loadMoreButton = document.getElementById("booklist-load-more");
const textarea = document.getElementById("booklist-listarea");
const perPage = 10;
const orderbySelect = document.getElementById("booklist-orderby");
function loadBooks(page = 1) {
const orderby = orderbySelect.value;
const order = document.querySelector('[name="booklist-order"]:checked').value;
wp.apiFetch({
// addQueryArgs() で明示的にクエリパラメータを追加
path: wp.url.addQueryArgs("/wp/v2/books", {
_fields: "id,title,link",
page: page,
per_page: perPage,
orderby: orderby,
order: order
}),
parse: false
})
.then((response) => {
const totalPages = parseInt(response.headers.get("X-WP-TotalPages"), 10);
return response.json().then((books) => {
if (books.length > 0) {
const newLine = page === 1 ? "" : "\n";
textarea.value += newLine + books
.map((book) => `${book.title.rendered} (${book.id}) ${book.link}`)
.join("\n");
loadMoreButton.style.display = currentPage < totalPages ? 'inline-block' : 'none';
} else {
loadMoreButton.style.display = 'none';
}
});
})
.catch((error) => console.error("Failed to fetch books:", error));
}
loadButton.addEventListener("click", () => {
textarea.value = '';
currentPage = 1;
loadBooks(currentPage);
});
loadMoreButton.addEventListener("click", () => {
currentPage++;
loadBooks(currentPage);
});
URLSearchParams を使う
addQueryArgs() を使わずに、オブジェクト形式のパラメータを URLSearchParams を使ってクエリ文字列に変換して、path に結合してリクエストを送ることもできます。
let currentPage = 1;
const loadButton = document.getElementById("booklist-load-books");
const loadMoreButton = document.getElementById("booklist-load-more");
const textarea = document.getElementById("booklist-listarea");
const perPage = 5;
const orderbySelect = document.getElementById("booklist-orderby");
function loadBooks(page = 1) {
const orderby = orderbySelect.value;
const order = document.querySelector('[name="booklist-order"]:checked').value;
// クエリパラメータを作成
const params = {
_fields: "id,title,link",
page: page,
per_page: perPage,
orderby: orderby,
order: order
};
// URLSearchParams でクエリ文字列を作成
const queryString = new URLSearchParams(params).toString();
wp.apiFetch({
// path にクエリ文字列を直接追加
path: `/wp/v2/books?${queryString}`,
parse: false
})
.then((response) => {
const totalPages = parseInt(response.headers.get("X-WP-TotalPages"), 10);
return response.json().then((books) => {
if (books.length > 0) {
const newLine = page === 1 ? "" : "\n";
textarea.value += newLine + books
.map((book) => `${book.title.rendered} (${book.id}) ${book.link}`)
.join("\n");
loadMoreButton.style.display = currentPage < totalPages ? 'inline-block' : 'none';
} else {
loadMoreButton.style.display = 'none';
}
});
})
.catch((error) => console.error("Failed to fetch books:", error));
}
loadButton.addEventListener("click", () => {
textarea.value = '';
currentPage = 1;
loadBooks(currentPage);
});
loadMoreButton.addEventListener("click", () => {
currentPage++;
loadBooks(currentPage);
});
Book List 管理ページを開いて、Load Books をクリックすると、カスタム投稿タイプ book の投稿を10件取得して、そのタイトルと ID、及びリンクを表示します。
投稿が10件以上ある場合は、Load More ボタンが表示され、追加で投稿を読み込むことができます。また、プルダウンから並び順の基準を選択したり、ラジオボタンで昇順・降順を切り替えることができます。
投稿を作成(POST)
WP REST API と api-fetch を使用して、カスタム投稿タイプ book の投稿を作成します。
そのためには、タイトルとコンテンツのフィールドを POST リクエストとして books エンドポイントに渡す必要があります。
プラグインファイル(booklist.php)の HTML を描画するコールバック関数に、投稿のタイトルを入力する input 要素、コンテンツを入力する textarea 要素、リクエストを送信する button 要素のマークアップを追加します。
// サブメニューページのHTMLを描画するコールバック関数
function booklist_render_book_list() {
?>
<div class="wrap" id="booklist-admin">
//・・・中略・・・
<div style="width:50%;">
<h2>Add Book</h2>
<div>
<label for="booklist-book-title">Book Title</label><br>
<input type="text" id="booklist-book-title" placeholder="Title">
</div>
<div>
<label for="booklist-book-content">Book Content</label><br>
<textarea id="booklist-book-content" cols="70" rows="10"></textarea>
</div>
<div>
<button id="booklist-submit-book">Add</button>
</div>
</div>
</div>
<?php
}
そして、wp.apiFetch() を使用して books エンドポイントへのリクエストを作成します。
以下は wp.apiFetch() の options パラメータの設定部分です。
path に books エンドポイントへのパスを設定し、method にリクエストメソッド POST に設定します。
別途タイトルとコンテンツを変数に取得しておき、data にデータオブジェクトとして渡します。その際、データの status には "publish" を指定します(指定しない場合は「下書き」として作成されます)。
wp.apiFetch({
path: "/wp/v2/books/", // エンドポイントへのパス
method: "POST", // リクエストメソッド
data: { // データオブジェクト
title: title,
content: content,
status: "publish", // 公開済みに
},
})
wp.apiFetch() の data
wp.apiFetch() の data は fetch() の body に相当しますが、wp.apiFetch() はデフォルトで application/json を扱うようになっており、データの処理が簡単になっています。
具体的には、wp.apiFetch() では data にそのままオブジェクトを渡すだけで、内部で JSON 変換と Content-Type の設定を自動的に行ってくれます。そのため、JSON.stringify() でデータを JSON 文字列に変換する必要も、headers に Content-Type: application/json を設定する必要もありません。
但し、POST(または PUT)リクエストで JSON データを送信する場合、ネストされたオブジェクトや配列は正しく送れない可能性もあるようです。その場合は、data ではなく、body を使用し、JSON.stringify() で JSON 形式の文字列にに変換し、headers で Content-Type: application/json を明示的に指定します。
booklist-admin.js に以下を追加します。
// POST リクエスト送信用ボタン
const submitBookButton = document.getElementById("booklist-submit-book");
if (submitBookButton) {
// 上記ボタンにクリックイベントのリスナーを設定
submitBookButton.addEventListener("click", () => {
// タイトル入力欄の値
const title = document.getElementById("booklist-book-title").value;
// コンテンツ入力欄の値
const content = document.getElementById("booklist-book-content").value;
// タイトルとコンテンツが入力されていない場合はアラートを表示して終了
if (!title || !content) {
alert("Both title and content are required.");
return;
}
// POST リクエストを送信
wp.apiFetch({
path: "/wp/v2/books/",
method: "POST",
data: {
title: title,
content: content,
status: "publish",
},
})
.then((result) => {
// 成功した場合
alert("Book saved!");
console.log("Book created:", result);
})
.catch((error) => {
// 失敗(エラー)の場合
console.error("Failed to create book:", error);
alert("Failed to save book.");
});
});
}
data の代わりに body を使う場合は、上記の16ー24行目を以下のように書き換えます。
wp.apiFetch({
path: "/wp/v2/books/",
method: "POST",
// data の代わりに body を使う場合
body: JSON.stringify({
title: title,
content: content,
status: "publish",
}),
// Content-Type に application/json を指定
headers: {
"Content-Type": "application/json",
},
})
Book List 管理ページを開き、タイトルとコンテンツを入力して、Add ボタンをクリックすると、「Book saved!」というアラートが表示されます。
アラートを消すと、コンソールに「Book created」という文字列に続いてレスポンスが出力され、新規に投稿が作成されます。LoadLoad Books ボタンをクリックすると、追加された投稿もリストに表示されます。
投稿を更新(PUT)
以下は PUT メソッドを使ってカスタム投稿タイプ book の投稿を更新する例です。
booklist.php の HTML を描画するコールバック関数に以下の投稿のID とタイトルを入力する input 要素、コンテンツを入力する textarea 要素、リクエストを送信する button 要素のマークアップを追加します。
// サブメニューページのHTMLを描画するコールバック関数
function booklist_render_book_list() {
?>
//・・・中略・・・
<div style="width:50%;">
<h2>Update Book</h2>
<div>
<label for="booklist-update-book-id">Book ID</label><br>
<input type="text" id="booklist-update-book-id" placeholder="ID">
</div>
<div>
<label for="booklist-update-book-title">Book Title</label><br>
<input type="text" id="booklist-update-book-title" placeholder="Title">
</div>
<div>
<label for="booklist-update-book-content">Book Content</label><br>
<textarea id="booklist-update-book-content" cols="70" rows="10"></textarea>
</div>
<div>
<button id="booklist-update-book">Update</button>
</div>
</div>
</div>
<?php
}
booklist-admin.js に以下を追加します。
id とタイトル、コンテンツを取得し、その際、trim() で 余計なスペースを削除します。そして、id が入力され数値であることと、タイトルまたはコンテンツのどちらかが入力されていることを確認します。
続いて、エンドポイントに id を指定して GET メソッドで現在のデータを取得し(変更がない項目は上書きしないように)、そのデータを元に PUT メソッドを使って投稿を更新します。
また、return wp.apiFetch({...}) として return することで、Promise を次の .then() に渡して、成功時にコンソールに更新後のデータ(updatedBook)を出力するようにしています。
// PUT リクエスト送信用ボタン
const updateBookButton = document.getElementById("booklist-update-book");
if (updateBookButton) {
updateBookButton.addEventListener("click", function () {
// id の値
const id = document.getElementById("booklist-update-book-id").value.trim();
const title = document.getElementById("booklist-update-book-title").value.trim();
const content = document.getElementById("booklist-update-book-content").value.trim();
// id が数値であることを確認
if (!id || isNaN(id) || !(title || content)) {
alert("ID は数値であり、タイトルまたはコンテンツのどちらかが必要です。");
return;
}
// まず現在のデータを取得(エンドポイントに id を指定)
wp.apiFetch({ path: `/wp/v2/books/${id}`, method: "GET" })
.then((currentBook) => {
// 取得したデータを元に更新データを作成(非同期処理を return して次の .then() に渡す)
return wp.apiFetch({
// エンドポイントに id を追加
path: `/wp/v2/books/${id}`,
method: "PUT",
data: {
title: title || currentBook.title.rendered, // タイトルが空なら元の値を保持
content: content || currentBook.content.rendered, // コンテンツが空なら元の値を保持
status: currentBook.status, // 投稿の状態を維持
},
});
})
.then((updatedBook) => {
// 成功時の処理
alert("Book updated successfully!");
// updatedBook は return された更新データ
console.log("Book updated:", updatedBook);
})
.catch((error) => {
// エラーハンドリング
console.error("Failed to update book:", error);
alert("Failed to update book: " + (error.message || "Unknown error"));
});
});
}
data の代わりに body を使う場合は、24-28行目を以下のように書き換えます。
body: JSON.stringify({
title: title || currentBook.title.rendered, // タイトルが空なら元の値を保持
content: content || currentBook.content.rendered, // コンテンツが空なら元の値を保持
status: currentBook.status, // 投稿の状態を維持
}),
headers: {
"Content-Type": "application/json",
},
以下は Promise チェーンの代わりに async / await を使って書き換えた例です。
// PUT リクエスト送信用ボタン
const updateBookButton = document.getElementById("booklist-update-book");
if (updateBookButton) {
// async / await を使う場合
updateBookButton.addEventListener("click", async () => {
const id = document.getElementById("booklist-update-book-id").value.trim();
const title = document.getElementById("booklist-update-book-title").value.trim();
const content = document.getElementById("booklist-update-book-content").value.trim();
// id が数値であることを確認
if (!id || isNaN(id) || !(title || content)) {
alert("ID は数値であり、タイトルまたはコンテンツのどちらかが必要です。");
return;
}
try {
// まず現在のデータを取得
const currentBook = await wp.apiFetch({ path: `/wp/v2/books/${id}`, method: "GET" });
// 変更する値だけ上書き
const updatedBook = await wp.apiFetch({
path: `/wp/v2/books/${id}`,
method: "PUT",
data: {
title: title || currentBook.title.rendered, // タイトルが空なら元の値を保持
content: content || currentBook.content.rendered, // コンテンツが空なら元の値を保持
status: currentBook.status, // 投稿の状態を維持
},
});
alert("Book updated successfully!");
console.log("Book updated:", updatedBook);
} catch (error) {
console.error("Failed to update book:", error);
alert("Failed to update book: " + (error.message || "Unknown error"));
}
});
}
Book List 管理ページを開き、Update Book で ID とタイトル、コンテンツを入力して、Update ボタンをクリックすると、「Book updated successfully!」というアラートが表示されます。
アラートを消すと、コンソールに「Book updated:」という文字列に続いてレスポンスが出力され、指定した投稿が更新されます。以下は ID とコンテンツを入力して更新した例です。
投稿を削除(DELETE)
以下は DELETE メソッドを使ってカスタム投稿タイプ book の投稿を削除する例です。
booklist.php の HTML を描画するコールバック関数に以下の投稿のID とタイトルを入力する input 要素、投稿を完全に削除するためのチェックボックス(Force Delete)、リクエストを送信する button 要素のマークアップを追加します。
// サブメニューページのHTMLを描画するコールバック関数
function booklist_render_book_list() {
?>
//・・・中略・・・
<div style="width:50%;">
<h2>Delete Book</h2>
<div>
<label for="booklist-delete-book-id">Book ID</label><br>
<input type="text" id="booklist-delete-book-id" placeholder="ID">
</div>
<div>
<input type="checkbox" id="booklist-delete-force">
<label for="booklist-delete-force">Force Delete</label>
</div>
<div>
<button id="booklist-delete-book">Delete</button>
</div>
</div>
<?php
}
booklist-admin.js に以下を追加します。
投稿を完全に削除するためのチェックボックスのチェック状態を取得します。チェックが入っている場合は、投稿をゴミ箱に移動するのではなく、完全に削除します。
wp.apiFetch() で DELETE リクエストを送信する際に、チェックが入っている場合は、data に force: true を指定します。
DELETE メソッドのデフォルトの動作では、投稿は完全に削除されず、ゴミ箱(Trash)に移動するだけなので、完全に削除 する場合は force: true を指定する必要があります。
// DELETE リクエスト送信用ボタン
const deleteBookButton = document.getElementById("booklist-delete-book");
if (deleteBookButton) {
deleteBookButton.addEventListener("click", function () {
const id = document.getElementById("booklist-delete-book-id").value.trim();
// チェック状態を取得
const forceDelete = document.getElementById("booklist-delete-force").checked;
// id が数値であることを確認
if (!id || isNaN(id)) {
alert("ID は数値ででなければなりません。");
return;
}
// DELETE リクエストを送信
wp.apiFetch({
path: `/wp/v2/books/${id}`,
method: "DELETE",
// チェックされている場合のみ `force: true` を設定
data: forceDelete ? { force: true } : {},
})
.then((result) => {
// 成功した場合
alert("Book deleted!");
console.log("Book deleted:", result);
})
.catch((error) => {
// 失敗(エラー)の場合
console.error("Failed to delete book:", error);
alert("Failed to delete book: " + (error.message || "Unknown error"));
});
});
}
Book List 管理ページを開き、Delete Book で ID を入力して、Delete ボタンをクリックすると、「Book deleted!」というアラートが表示され、ID で指定された投稿はゴミ箱に移動します。
「Force Delete」にチェックを入れた場合は、ゴミ箱に移動ではなく、完全に削除されます。
アラートを消すと、コンソールに「Book deleted:」という文字列に続いてレスポンスが出力されます。以下は ID を入力して投稿をゴミ箱へ移動した例です。
最終的なコード
以下はプラグインファイルと Book List 管理ページで読み込んでいる JavaScript の最終的なコードです。
<?php
/**
* Plugin Name: Booklist
* Description: A plugin to manage books
* Version: 0.0.1
*
*/
if (! defined('ABSPATH')) {
exit; // Exit if accessed directly.
}
// カスタム投稿タイプ book の登録
add_action('init', 'booklist_register_book_post_type');
function booklist_register_book_post_type() {
$args = array(
'labels' => array(
'name' => 'Books',
'singular_name' => 'Book',
'menu_name' => 'Books',
'add_new' => 'Add New Book',
'add_new_item' => 'Add New Book',
'new_item' => 'New Book',
'edit_item' => 'Edit Book',
'view_item' => 'View Book',
'all_items' => 'All Books',
),
'public' => true,
'has_archive' => true,
'menu_icon' => 'dashicons-book',
'show_in_rest' => true, // REST API に公開する
'rest_base' => 'books', // REST API route の base URL を book から books に変更
'supports' => array('title', 'editor', 'author', 'thumbnail', 'excerpt'),
);
register_post_type('book', $args);
}
// サブメニューページを追加
add_action('admin_menu', 'booklist_add_submenu', 11);
function booklist_add_submenu() {
add_submenu_page(
'edit.php?post_type=book', // 親メニューのスラッグ
'Book List', // サブメニューページのタイトル
'Book List', // サブメニューページのメニューのタイトル
'edit_posts', // サブメニューページにアクセスできるユーザーの権限
'book-list', // サブメニューページを参照するスラッグ
'booklist_render_book_list' // サブメニューページを描画するコールバック関数
);
}
// サブメニューページのHTMLを描画するコールバック関数
function booklist_render_book_list() {
?>
<div class="wrap" id="booklist-admin">
<h1>Actions</h1>
<div style="margin-top:20px">
<label for="booklist-orderby">Order by:</label>
<select id="booklist-orderby">
<option value="date">Date</option>
<option value="title">Title</option>
<option value="modified">Last Modified</option>
<option value="id">ID</option>
</select>
<label>
<input type="radio" name="booklist-order" value="asc"> Ascending
</label>
<label>
<input type="radio" name="booklist-order" value="desc" checked> Descending
</label>
</div>
<div style="margin-top:20px">
<button id="booklist-load-books">Load Books</button>
</div>
<h2>Books</h2>
<textarea id="booklist-listarea" cols="70" rows="20"></textarea>
<div>
<button id="booklist-load-more" style="display:none;">Load More</button>
</div>
<div style="width:50%; margin-top:300px">
<h2>Add Book</h2>
<div>
<label for="booklist-book-title">Book Title</label><br>
<input type="text" id="booklist-book-title" placeholder="Title">
</div>
<div>
<label for="booklist-book-content">Book Content</label><br>
<textarea id="booklist-book-content" cols="70" rows="10"></textarea>
</div>
<div>
<button id="booklist-submit-book">Add</button>
</div>
</div>
<div style="width:50%;">
<h2>Update Book</h2>
<div>
<label for="booklist-update-book-id">Book ID</label><br>
<input type="text" id="booklist-update-book-id" placeholder="ID">
</div>
<div>
<label for="booklist-update-book-title">Book Title</label><br>
<input type="text" id="booklist-update-book-title" placeholder="Title">
</div>
<div>
<label for="booklist-update-book-content">Book Content</label><br>
<textarea id="booklist-update-book-content" cols="70" rows="10"></textarea>
</div>
<div>
<button id="booklist-update-book">Update</button>
</div>
</div>
<div style="width:50%;">
<h2>Delete Book</h2>
<div>
<label for="booklist-delete-book-id">Book ID</label><br>
<input type="text" id="booklist-delete-book-id" placeholder="ID">
</div>
<div>
<input type="checkbox" id="booklist-delete-force">
<label for="booklist-delete-force">Force Delete</label>
</div>
<div>
<button id="booklist-delete-book">Delete</button>
</div>
</div>
</div>
<?php
}
// サブメニューページに JavaScript ファイル(booklist-admin.js)を読み込む
add_action('admin_enqueue_scripts', 'booklist_admin_enqueue_scripts');
function booklist_admin_enqueue_scripts($hook) {
$allowed_pages = ['book_page_book-list'];
if (in_array($hook, $allowed_pages)) {
wp_enqueue_script(
'booklist-admin',
plugin_dir_url(__FILE__) . '/booklist-admin.js',
array('wp-api-fetch'),
'1.0.0',
true
);
}
}
// 投稿を取得してリスト表示する処理
let currentPage = 1;
const loadButton = document.getElementById("booklist-load-books");
const loadMoreButton = document.getElementById("booklist-load-more");
const textarea = document.getElementById("booklist-listarea");
const perPage = 10;
const orderbySelect = document.getElementById("booklist-orderby");
// 投稿を取得する関数
function loadBooks(page = 1) {
const orderby = orderbySelect.value;
const order = document.querySelector('[name="booklist-order"]:checked').value;
wp.apiFetch({
path: wp.url.addQueryArgs("/wp/v2/books", {
_fields: "id,title,link",
page: page,
per_page: perPage,
orderby: orderby,
order: order
}),
parse: false
})
.then((response) => {
const totalPages = parseInt(response.headers.get("X-WP-TotalPages"), 10);
return response.json().then((books) => {
if (books.length > 0) {
const newLine = page === 1 ? "" : "\n";
textarea.value += newLine + books
.map((book) => `${book.title.rendered} (${book.id}) ${book.link}`)
.join("\n");
loadMoreButton.style.display = currentPage < totalPages ? 'inline-block' : 'none';
} else {
loadMoreButton.style.display = 'none';
}
});
})
.catch((error) => console.error("Failed to fetch books:", error));
}
// Load Books ボタンのイベントリスナー
loadButton.addEventListener("click", () => {
textarea.value = '';
currentPage = 1;
loadBooks(currentPage);
});
// Load More ボタンのイベントリスナー
loadMoreButton.addEventListener("click", () => {
currentPage++;
loadBooks(currentPage);
});
// POST リクエスト送信用ボタン
const submitBookButton = document.getElementById("booklist-submit-book");
if (submitBookButton) {
// 上記ボタンにクリックイベントのリスナーを設定
submitBookButton.addEventListener("click", () => {
// タイトル入力欄の値
const title = document.getElementById("booklist-book-title").value;
// コンテンツ入力欄の値
const content = document.getElementById("booklist-book-content").value;
// タイトルとコンテンツが入力されていない場合はアラートを表示して終了
if (!title || !content) {
alert("Both title and content are required.");
return;
}
// POST リクエストを送信
wp.apiFetch({
path: "/wp/v2/books/",
method: "POST",
data: {
title: title,
content: content,
status: "publish",
},
})
.then((result) => {
// 成功した場合
alert("Book saved!");
console.log("Book created:", result);
})
.catch((error) => {
// 失敗(エラー)の場合
console.error("Failed to create book:", error);
alert("Failed to save book.");
});
});
}
// PUT リクエスト送信用ボタン
const updateBookButton = document.getElementById("booklist-update-book");
if (updateBookButton) {
updateBookButton.addEventListener("click", function () {
const id = document.getElementById("booklist-update-book-id").value.trim();
const title = document.getElementById("booklist-update-book-title").value.trim();
const content = document.getElementById("booklist-update-book-content").value.trim();
// id が数値であることを確認
if (!id || isNaN(id) || !(title || content)) {
alert("ID は数値であり、タイトルまたはコンテンツのどちらかが必要です。");
return;
}
// まず現在のデータを取得
wp.apiFetch({ path: `/wp/v2/books/${id}`, method: "GET" })
.then((currentBook) => {
// 取得したデータを元に更新データを作成(非同期処理を return して次の .then() に渡す)
return wp.apiFetch({
path: `/wp/v2/books/${id}`,
method: "PUT",
data: {
title: title || currentBook.title.rendered, // タイトルが空なら元の値を保持
content: content || currentBook.content.rendered, // コンテンツが空なら元の値を保持
status: currentBook.status, // 投稿の状態を維持
},
});
})
.then((updatedBook) => {
// 成功時の処理
alert("Book updated successfully!");
// updatedBook は return された更新データ
console.log("Book updated:", updatedBook);
})
.catch((error) => {
// エラーハンドリング
console.error("Failed to update book:", error);
alert("Failed to update book: " + (error.message || "Unknown error"));
});
});
}
// DELETE リクエスト送信用ボタン
const deleteBookButton = document.getElementById("booklist-delete-book");
if (deleteBookButton) {
deleteBookButton.addEventListener("click", function () {
const id = document.getElementById("booklist-delete-book-id").value.trim();
// チェック状態を取得
const forceDelete = document.getElementById("booklist-delete-force").checked;
// id が数値であることを確認
if (!id || isNaN(id)) {
alert("ID は数値ででなければなりません。");
return;
}
// DELETE リクエストを送信
wp.apiFetch({
path: `/wp/v2/books/${id}`,
method: "DELETE",
// チェックされている場合のみ `force: true` を設定
data: forceDelete ? { force: true } : {},
})
.then((result) => {
// 成功した場合
alert("Book deleted!");
console.log("Book deleted:", result);
})
.catch((error) => {
// 失敗(エラー)の場合
console.error("Failed to delete book:", error);
alert("Failed to delete book: " + (error.message || "Unknown error"));
});
});
}
固定ページで投稿を取得して表示
以下は wp.apiFetch() を使って、固定ページ(フロントエンド)で、取得した投稿のタイトルにリンクを付けて表示する例です。
固定ページを作成
新規に固定ページを作成し、スラッグを fetch-posts にします(任意のスラッグを付けられますが、異なる名前をつけた場合は JavaScript の読み込みでもその値を使用します)。
そして、カスタム HTML ブロックを挿入し、以下の HTML を記述します。
Load Posts ボタンをクリックすると、セレクトボックスのプルダウンで選択した並び替えの基準で、ラジオボタンで選択した並び順で投稿を取得して、ul#fetch-posts-list にリストを表示します。
<div>
<label for="fetch-posts-orderby">Order by:</label>
<select id="fetch-posts-orderby">
<option value="date">Date</option>
<option value="title">Title</option>
<option value="modified">Last Modified</option>
<option value="id">ID</option>
</select>
<label>
<input type="radio" name="fetch-posts-order" value="asc"> Ascending
</label>
<label>
<input type="radio" name="fetch-posts-order" value="desc" checked> Descending
</label>
</div>
<div style="margin-top:20px">
<button id="fetch-posts-load-posts">Load Posts</button>
</div>
<ul id="fetch-posts-list"></ul>
<div style="margin-top:20px">
<button id="fetch-posts-load-more" style="display:none;">Load More</button>
</div>
フロントエンド側は以下のように表示されます。
JavaScript を作成
テーマに JavaScript を追加します。
この例ではデフォルトのテーマ Twenty-Twenty-Five の assets フォルダの中に js フォルダを作成し、その中に fetch-posts.js というファイルを作成します。
fetch-posts.js には、取り敢えず以下を記述しておきます。
console.log('hello from fetch-post.js');
functions.php
functions.php で上記で作成した fetch-posts.js を、スラッグが fetch-posts の固定ページで読み込むように以下を追加します。
function fetch_posts_enqueue_scripts() {
if (is_page('fetch-posts')) { // 固定ページ「fetch-posts」のみで読み込む
wp_enqueue_script(
'fetch-posts-script',
get_template_directory_uri() . '/assets/js/fetch-posts.js',
array('wp-api-fetch'), // wp-api-fetch を依存関係として登録
'1.0.0',
true // フッターで読み込む
);
}
}
add_action('wp_enqueue_scripts', 'fetch_posts_enqueue_scripts');
これでスラッグが fetch-posts の固定ページを表示して、開発ツールでコンソールを確認すると、JavaScript により「hello from fetch-post.js」と出力されます。
JavaScript を更新
fetch-posts.js から console.log('hello from fetch-post.js'); を削除して、以下のように書き換えます。
内容的には、投稿を取得(GET)とほぼ同じですが、リンク文字列(post.link)とタイトル文字列(post.title.rendered)を念の為サニタイズしています。
タイトル文字列は HTML のマークアップが含まれている可能性があるので、sanitizeHtml という関数を定義して、HTML を除去するようにしています。
リンク文字列は WordPress REST API が正しく動作していれば悪意のあるスクリプトが含まれることはありませんが、念の為、URL が http: または https: で始まる場合のみ許可するようにしています。
// URL サニタイズ用の関数
const sanitizeUrl = (url) => {
try {
const sanitizedUrl = new URL(url);
if (sanitizedUrl.protocol === 'http:' || sanitizedUrl.protocol === 'https:') {
// protocol が 'http:' または 'https:' の場合のみ許可
return sanitizedUrl.href;
}
} catch (e) {
console.warn("Invalid URL:", url);
}
return ''; // 無効な URL は空文字を返す
};
// HTML からテキストを取得する関数
const sanitizeHtml = (htmlRendered) => {
if (!htmlRendered) return "";
const elem = document.createElement("div");
elem.innerHTML = htmlRendered;
return elem.textContent || elem.innerText || "";
};
// 投稿を取得してリスト表示する処理
let currentPage = 1;
const loadButton = document.getElementById("fetch-posts-load-posts");
const loadMoreButton = document.getElementById("fetch-posts-load-more");
const postList = document.getElementById("fetch-posts-list");
const perPage = 5;
const orderbySelect = document.getElementById("fetch-posts-orderby");
// 投稿を取得する関数
function loadPosts(page = 1) {
const orderby = orderbySelect.value;
const order = document.querySelector('[name="fetch-posts-order"]:checked').value;
wp.apiFetch({
path: wp.url.addQueryArgs("/wp/v2/posts", {
_fields: "id,title,link",
page: page,
per_page: perPage,
orderby: orderby,
order: order
}),
parse: false
})
.then((response) => {
const totalPages = parseInt(response.headers.get("X-WP-TotalPages"), 10);
return response.json().then((posts) => {
if (posts.length > 0) {
const html = posts.map((post) => {
const sanitizedLink = sanitizeUrl(post.link); // URL をサニタイズ
const sanitizedTitle = sanitizeHtml(post.title.rendered); // タイトルをサニタイズ
return `<li><a href="${sanitizedLink}">${sanitizedTitle} (ID: ${post.id})</a></li>`;
}).join("");
postList.insertAdjacentHTML('beforeend', html);
loadMoreButton.style.display = currentPage < totalPages ? 'inline-block' : 'none';
} else {
loadMoreButton.style.display = 'none';
}
});
})
.catch((error) => console.error("Failed to fetch posts:", error));
}
// Load Posts ボタンのイベントリスナー
loadButton.addEventListener("click", () => {
postList.innerHTML = '';
currentPage = 1;
loadPosts(currentPage);
});
// Load More ボタンのイベントリスナー
loadMoreButton.addEventListener("click", () => {
currentPage++;
loadPosts(currentPage);
});
フロントエンドで Load Posts ボタンをクリックすると、投稿が5件、セレクトボックスとラジオボタンで指定した条件で取得されます。
投稿が5件以上ある場合は、Load More ボタンが表示され、追加で投稿を取得することができます。
アイキャッチ画像を取得
投稿にアイキャッチ画像が設定されていれば取得して表示する例です。
クエリパラメータに _embed: 'wp:featuredmedia'
を追加します(38行目)。また、_fields パラメータに _links.wp:featuredmedia
を追加します(33行目)。
グローバルパラメータの _fields に _embedded のみを追加で指定してもアイキャッチ画像のリンクは取得できません。_embed を_fieldsと一緒に使用するには、フィールドに _embedded と _links を追加します(以下では _embedded は省略しています)。参考:_embed
_embedded のデータにアクセスするには、例えば、指定したサイズの画像がない場合などでエラーにならないようにオプショナルチェイニング ?. を使用しています(51行目)。
const sanitizeUrl = (url) => {
try {
const sanitizedUrl = new URL(url);
if (sanitizedUrl.protocol === 'http:' || sanitizedUrl.protocol === 'https:') {
return sanitizedUrl.href;
}
} catch (e) {
console.warn("Invalid URL:", url);
}
return '';
};
const sanitizeHtml = (htmlRendered) => {
if (!htmlRendered) return "";
const elem = document.createElement("div");
elem.innerHTML = htmlRendered;
return elem.textContent || elem.innerText || "";
};
let currentPage = 1;
const loadButton = document.getElementById("fetch-posts-load-posts");
const loadMoreButton = document.getElementById("fetch-posts-load-more");
const postList = document.getElementById("fetch-posts-list");
const perPage = 5;
const orderbySelect = document.getElementById("fetch-posts-orderby");
function loadPosts(page = 1) {
const orderby = orderbySelect.value;
const order = document.querySelector('[name="fetch-posts-order"]:checked').value;
wp.apiFetch({
path: wp.url.addQueryArgs("/wp/v2/posts", {
// _links.wp:featuredmedia を追加
_fields: "id,title,link, _links.wp:featuredmedia", // または "id,title,link,_links.wp:featuredmedia,_embedded",
page: page,
per_page: perPage,
orderby: orderby,
order: order,
_embed: 'wp:featuredmedia' // _embed に featuredmedia を指定
}),
parse: false
})
.then((response) => {
const totalPages = parseInt(response.headers.get("X-WP-TotalPages"), 10);
return response.json().then((posts) => {
console.log(posts);
if (posts.length > 0) {
const html = posts.map((post) => {
const sanitizedLink = sanitizeUrl(post.link);
const sanitizedTitle = sanitizeHtml(post.title?.rendered);
// アイキャッチ画像の thumbnail サイズの URL を取得
const featuredMedia = post._embedded?.["wp:featuredmedia"]?.[0]?.media_details?.sizes?.thumbnail?.source_url;
// アイキャッチ画像の URL を取得できれば img 要素を作成し、取得できなければ空文字列を変数に代入
const image = featuredMedia ? `<img src="${featuredMedia}" alt="${sanitizedTitle}" />` : '';
// アイキャッチ画像を追加
return `<li><a href="${sanitizedLink}">${sanitizedTitle} (ID: ${post.id}) ${image}</a></li>`;
}).join("");
postList.insertAdjacentHTML('beforeend', html);
loadMoreButton.style.display = currentPage < totalPages ? 'inline-block' : 'none';
} else {
loadMoreButton.style.display = 'none';
}
});
})
.catch((error) => console.error("Failed to fetch posts:", error));
}
loadButton.addEventListener("click", () => {
postList.innerHTML = '';
currentPage = 1;
loadPosts(currentPage);
});
loadMoreButton.addEventListener("click", () => {
currentPage++;
loadPosts(currentPage);
});
但し、上記の方法では、アイキャッチ画像の URL は管理者など編集権限があるユーザーとしてログインしていない(認証されていない)と取得できません(関連項目:アイキャッチ画像を取得)。
カテゴリーを選択
WordPress REST API のカテゴリーのエンドポイントを使い、カテゴリー一覧を取得し、セレクトボックスでユーザーがカテゴリーを選択できるようにします。
HTML を更新
固定ページに記述している HTML を以下のように変更して、カテゴリーを選択するセレクトボックスの select 要素とラベルの label 要素を追加します(2-5行目)。
<div class="fetch-post-wrapper">
<div>
<label for="fetch-posts-category">Category:</label>
<select id="fetch-posts-category"></select>
</div>
<div style="margin-top:20px">
<label for="fetch-posts-orderby">Order by:</label>
<select id="fetch-posts-orderby">
<option value="date">Date</option>
<option value="title">Title</option>
<option value="modified">Last Modified</option>
<option value="id">ID</option>
</select>
<label>
<input type="radio" name="fetch-posts-order" value="asc"> Ascending
</label>
<label>
<input type="radio" name="fetch-posts-order" value="desc" checked> Descending
</label>
</div>
<div style="margin-top:20px">
<button id="fetch-posts-load-posts">Load Posts</button>
</div>
<ul id="fetch-posts-list"></ul>
<div style="margin-top:20px">
<button id="fetch-posts-load-more" style="display:none;">Load More</button>
</div>
</div>
JavaScript を更新
カテゴリー一覧は、カテゴリーのエンドポイント(/wp/v2/categories)を使って取得できます。
カテゴリーを取得してセレクトボックスにオプションを追加する関数 loadCategories() を定義します。
カテゴリーのエンドポイントからは、id と name を取得できれば良いので、_fields パラメータに id と name を指定します。option 要素の生成では、デフォルトの All Categories を作成し、value を空にし、このオプションが選択された場合は、投稿を取得する際にパラメータにカテゴリーを含めません。
// カテゴリーを取得してセレクトボックスに追加する関数
function loadCategories() {
wp.apiFetch({ path: "/wp/v2/categories?_fields=id,name" })
.then((categories) => {
categorySelect.innerHTML = '<option value="">All Categories</option>' +
categories.map(cat => `<option value="${cat.id}">${sanitizeHtml(cat.name)}</option>`).join('');
})
.catch((error) => console.error("Failed to fetch categories:", error));
}
投稿を取得する関数 loadPosts() では、クエリパラメータを別途定義し、カテゴリーが選択されていれば、クエリパラメータに追加します。
そして、 wp.apiFetch() の path オプションに addQueryArgs() を使ってパラメータを追加します。
// 投稿を取得する関数
function loadPosts(page = 1) {
const orderby = orderbySelect.value;
const order = document.querySelector('[name="fetch-posts-order"]:checked').value;
// セレクトボックスで選択されたカテゴリーの値
const category = categorySelect.value;
// クエリパラメータを作成
const query = {
_fields: "id,title,link",
page: page,
per_page: perPage,
orderby: orderby,
order: order
};
// カテゴリーが選択されていれば、クエリパラメータに追加
if (category) query.categories = category;
// addQueryArgs() で path にクエリパラメータを追加
wp.apiFetch({ path: wp.url.addQueryArgs("/wp/v2/posts", query), parse: false })
.then((response) => {
const totalPages = parseInt(response.headers.get("X-WP-TotalPages"), 10);
return response.json().then((posts) => {
if (posts.length > 0) {
const html = posts.map((post) => {
const sanitizedLink = sanitizeUrl(post.link);
const sanitizedTitle = sanitizeHtml(post.title.rendered);
return `<li><a href="${sanitizedLink}">${sanitizedTitle} (ID: ${post.id})</a></li>`;
}).join("");
postList.insertAdjacentHTML('beforeend', html);
loadMoreButton.style.display = currentPage < totalPages ? 'inline-block' : 'none';
} else {
loadMoreButton.style.display = 'none';
}
});
})
.catch((error) => console.error("Failed to fetch posts:", error));
}
fetch-posts.js のコード全体は以下のようになります。
const sanitizeUrl = (url) => {
try {
const sanitizedUrl = new URL(url);
if (sanitizedUrl.protocol === 'http:' || sanitizedUrl.protocol === 'https:') {
return sanitizedUrl.href;
}
} catch (e) {
console.warn("Invalid URL:", url);
}
return '';
};
const sanitizeHtml = (htmlRendered) => {
if (!htmlRendered) return "";
const elem = document.createElement("div");
elem.innerHTML = htmlRendered;
return elem.textContent || elem.innerText || "";
};
let currentPage = 1;
const loadButton = document.getElementById("fetch-posts-load-posts");
const loadMoreButton = document.getElementById("fetch-posts-load-more");
const postList = document.getElementById("fetch-posts-list");
const perPage = 5;
const orderbySelect = document.getElementById("fetch-posts-orderby");
// カテゴリーを選択するセレクトボックス
const categorySelect = document.getElementById("fetch-posts-category");
// カテゴリーを取得してセレクトボックスに追加する関数
function loadCategories() {
wp.apiFetch({ path: "/wp/v2/categories?_fields=id,name" })
.then((categories) => {
categorySelect.innerHTML = '<option value="">All Categories</option>' +
categories.map(cat => `<option value="${cat.id}">${sanitizeHtml(cat.name)}</option>`).join('');
})
.catch((error) => console.error("Failed to fetch categories:", error));
}
function loadPosts(page = 1) {
const orderby = orderbySelect.value;
const order = document.querySelector('[name="fetch-posts-order"]:checked').value;
// セレクトボックスで選択されたカテゴリーの値
const category = categorySelect.value;
// クエリパラメータを作成
const query = {
_fields: "id,title,link",
page: page,
per_page: perPage,
orderby: orderby,
order: order
};
// カテゴリーが選択されていれば、クエリパラメータに追加
if (category) query.categories = category;
// addQueryArgs() で path にクエリパラメータを追加
wp.apiFetch({ path: wp.url.addQueryArgs("/wp/v2/posts", query), parse: false })
.then((response) => {
const totalPages = parseInt(response.headers.get("X-WP-TotalPages"), 10);
return response.json().then((posts) => {
if (posts.length > 0) {
const html = posts.map((post) => {
const sanitizedLink = sanitizeUrl(post.link);
const sanitizedTitle = sanitizeHtml(post.title.rendered);
return `<li><a href="${sanitizedLink}">${sanitizedTitle} (ID: ${post.id})</a></li>`;
}).join("");
postList.insertAdjacentHTML('beforeend', html);
loadMoreButton.style.display = currentPage < totalPages ? 'inline-block' : 'none';
} else {
loadMoreButton.style.display = 'none';
}
});
})
.catch((error) => console.error("Failed to fetch posts:", error));
}
// 初期カテゴリーの取得(カテゴリーを取得してセレクトボックスに追加)
loadCategories();
loadButton.addEventListener("click", () => {
postList.innerHTML = '';
currentPage = 1;
loadPosts(currentPage);
});
loadMoreButton.addEventListener("click", () => {
currentPage++;
loadPosts(currentPage);
});
デフォルトの All Categories を選択した状態では、全ての投稿が取得され、カテゴリーを選択すると、そのカテゴリーに属する投稿だけが取得されます。
投稿タイプを選択
投稿タイプ(Post Types)のエンドポイントから投稿タイプ一覧を取得し、セレクトボックスでユーザーが投稿タイプを選択できるようにします。
HTML を更新
固定ページに記述している HTML を以下のように変更して、カテゴリーの代わりに、投稿タイプを選択するセレクトボックスの select 要素とラベルの label 要素に置き換えます(2-5行目)。
<div class="fetch-post-wrapper">
<div>
<label for="fetch-posts-post-types">Post Type:</label>
<select id="fetch-posts-post-types"></select>
</div>
<div style="margin-top:20px">
<label for="fetch-posts-orderby">Order by:</label>
<select id="fetch-posts-orderby">
<option value="date">Date</option>
<option value="title">Title</option>
<option value="modified">Last Modified</option>
<option value="id">ID</option>
</select>
<label>
<input type="radio" name="fetch-posts-order" value="asc"> Ascending
</label>
<label>
<input type="radio" name="fetch-posts-order" value="desc" checked> Descending
</label>
</div>
<div style="margin-top:20px">
<button id="fetch-posts-load-posts">Load Posts</button>
</div>
<ul id="fetch-posts-list"></ul>
<div style="margin-top:20px">
<button id="fetch-posts-load-more" style="display:none;">Load More</button>
</div>
</div>
JavaScript を更新
投稿タイプ一覧は、投稿タイプのエンドポイント(/wp/v2/types)を使って取得できます。
投稿タイプを取得してセレクトボックスにオプションを追加する関数 loadPostTypes() を定義します。
function loadPostTypes() {
wp.apiFetch({ path: "/wp/v2/types" })
.then((types) => {
postTypeSelect.innerHTML = ""; // 初期化
// types はオブジェクト
Object.keys(types).forEach((type) => {
// typeData は各投稿タイプのオブジェクト(詳細データ)
const typeData = types[type];
const option = document.createElement("option");
option.value = typeData.rest_base; // REST API route の base URL
option.textContent = typeData.name;
postTypeSelect.appendChild(option);
});
})
.catch((error) => console.error("Failed to fetch post types:", error));
}
wp.apiFetch({ path: "/wp/v2/types" }) が返す types は、配列ではなくオブジェクトなので、map() や forEach() ではループできないため、Object.keys() を使って処理しています。
Object.keys(types) で ["post", "page", "attachment", "wp_navigation", ...] のようなキー(投稿タイプのスラッグ)の配列を取得して、forEach() でキー(投稿タイプ)をループ処し、typeData = types[type] で、投稿タイプの詳細データを取得しています。
/wp/v2/types のレスポンスデータは オブジェクトのプロパティとして各投稿タイプが格納されていて、types は、オブジェクトのキーに投稿タイプ(post, page, attachment)が入っています。
{
"post": {
"name": "Posts",
"slug": "post",
"rest_base": "posts",
・・・
},
"page": {
"name": "Pages",
"slug": "page",
"rest_base": "pages",
・・・
},
"attachment": {
"name": "Media",
"slug": "attachment",
"rest_base": "media",
・・・
},
"wp_navigation": {
"name": "Navigation Menus",
"slug": "wp_navigation",
"rest_base": "navigation",
・・・
}
}
更新した fetch-posts.js のコード全体は以下のようになります。
投稿タイプを選択するセレクトボックスを取得し(32行目)、loadPostTypes() で取得した投稿タイプをセレクトボックスのオプションに追加します(85行目)。
投稿一覧の取得では、投稿タイプを選択するセレクトボックスで選択された値をエンドポイントに指定します(56行目)。
const sanitizeUrl = (url) => {
try {
const sanitizedUrl = new URL(url);
if (sanitizedUrl.protocol === 'http:' || sanitizedUrl.protocol === 'https:') {
return sanitizedUrl.href;
}
} catch (e) {
console.warn("Invalid URL:", url);
}
return '';
};
const sanitizeHtml = (htmlRendered) => {
if (!htmlRendered) return "";
const elem = document.createElement("div");
elem.innerHTML = htmlRendered;
return elem.textContent || elem.innerText || "";
};
let currentPage = 1;
const loadButton = document.getElementById("fetch-posts-load-posts");
const loadMoreButton = document.getElementById("fetch-posts-load-more");
const postList = document.getElementById("fetch-posts-list");
const perPage = 5;
const orderbySelect = document.getElementById("fetch-posts-orderby");
// 投稿タイプを選択するセレクトボックス
const postTypeSelect = document.getElementById("fetch-posts-post-types");
function loadPostTypes() {
wp.apiFetch({ path: "/wp/v2/types" })
.then((types) => {
postTypeSelect.innerHTML = ""; // 初期化
// types はオブジェクト
Object.keys(types).forEach((type) => {
// typeData は各投稿タイプのオブジェクト
const typeData = types[type];
const option = document.createElement("option");
option.value = typeData.rest_base; // REST API route の base URL
option.textContent = typeData.name;
postTypeSelect.appendChild(option);
});
})
.catch((error) => console.error("Failed to fetch post types:", error));
}
function loadPosts(page = 1) {
const orderby = orderbySelect.value;
const order = document.querySelector('[name="fetch-posts-order"]:checked').value;
wp.apiFetch({
path: wp.url.addQueryArgs(`/wp/v2/${postTypeSelect.value}`, {
_fields: "id,title,link",
page: page,
per_page: perPage,
orderby: orderby,
order: order,
}),
parse: false,
})
.then((response) => {
const totalPages = parseInt(response.headers.get("X-WP-TotalPages"), 10);
return response.json().then((posts) => {
if (posts.length > 0) {
const html = posts.map((post) => {
const sanitizedLink = sanitizeUrl(post.link);
const sanitizedTitle = sanitizeHtml(post.title.rendered);
return `<li><a href="${sanitizedLink}">${sanitizedTitle} (ID: ${post.id})</a></li>`;
}).join("");
postList.insertAdjacentHTML('beforeend', html);
loadMoreButton.style.display = currentPage < totalPages ? 'inline-block' : 'none';
} else {
loadMoreButton.style.display = 'none';
}
});
})
.catch((error) => console.error("Failed to fetch posts:", error));
}
// ページロード時に投稿タイプを取得
document.addEventListener("DOMContentLoaded", loadPostTypes);
loadButton.addEventListener("click", () => {
postList.innerHTML = '';
currentPage = 1;
loadPosts(currentPage);
});
loadMoreButton.addEventListener("click", () => {
currentPage++;
loadPosts(currentPage);
});
但し、上記のコードの場合、投稿タイプを選択するセレクトボックスには、全ての投稿タイプが表示されてしまいます。
不要な投稿タイプを除外(1)
適切な投稿タイプのみをフィルタリングする方法としては、viewable や capabilities プロパティを使う方法があります。関連ページ:ダイナミックブロックの作成(投稿タイプの選択)
但し、viewable や capabilities プロパティは Context が edit に設定されているため、以下の方法は管理者など編集権限があるユーザーとしてログインしていないと利用できません。
投稿タイプを取得してセレクトボックスに追加する関数 loadPostTypes() を以下のように書き換えます。
エンドポイントのパラメータに context=edit を追加し、viewable や capabilities にアクセスできるようにします(context のデフォルトは view)。
そして viewable が true(サイトのフロントエンドで閲覧可能な投稿タイプ)であり、capabilities.edit_posts が true(編集できる投稿タイプ)である場合に option を作成します。
もし、メディアを除外する場合は、条件に && typeData.slug !== "attachment" を追加します。
function loadPostTypes() {
// context に edit を指定
wp.apiFetch({ path: "/wp/v2/types?context=edit" })
.then((types) => {
postTypeSelect.innerHTML = ""; // 初期化
Object.keys(types).forEach((type) => {
const typeData = types[type];
// フィルター条件:
// 1. viewable が true であること
// 2. capabilities.edit_posts が true であること(省略可能)
if (typeData.viewable && typeData.capabilities?.edit_posts) {
const option = document.createElement("option");
option.value = typeData.rest_base; // REST API route の base URL
option.textContent = typeData.name;
postTypeSelect.appendChild(option);
}
});
})
.catch((error) => console.error("Failed to fetch post types:", error));
}
更新した fetch-posts.js のコード全体は以下のようになります(変更は loadPostTypes() の部分のみ) 。
const sanitizeUrl = (url) => {
try {
const sanitizedUrl = new URL(url);
if (sanitizedUrl.protocol === 'http:' || sanitizedUrl.protocol === 'https:') {
return sanitizedUrl.href;
}
} catch (e) {
console.warn("Invalid URL:", url);
}
return '';
};
const sanitizeHtml = (htmlRendered) => {
if (!htmlRendered) return "";
const elem = document.createElement("div");
elem.innerHTML = htmlRendered;
return elem.textContent || elem.innerText || "";
};
let currentPage = 1;
const loadButton = document.getElementById("fetch-posts-load-posts");
const loadMoreButton = document.getElementById("fetch-posts-load-more");
const postList = document.getElementById("fetch-posts-list");
const perPage = 5;
const orderbySelect = document.getElementById("fetch-posts-orderby");
// 投稿タイプを選択するセレクトボックス
const postTypeSelect = document.getElementById("fetch-posts-post-types");
function loadPostTypes() {
// context に edit を指定
wp.apiFetch({ path: "/wp/v2/types?context=edit" })
.then((types) => {
postTypeSelect.innerHTML = ""; // 初期化
Object.keys(types).forEach((type) => {
const typeData = types[type];
// フィルター条件:
// 1. viewable が true であること
// 2. capabilities.edit_posts が true であること
if (typeData.viewable && typeData.capabilities?.edit_posts) {
const option = document.createElement("option");
option.value = typeData.rest_base; // REST API route の base URL
option.textContent = typeData.name;
postTypeSelect.appendChild(option);
}
});
})
.catch((error) => console.error("Failed to fetch post types:", error));
}
function loadPosts(page = 1) {
const orderby = orderbySelect.value;
const order = document.querySelector('[name="fetch-posts-order"]:checked').value;
wp.apiFetch({
path: wp.url.addQueryArgs(`/wp/v2/${postTypeSelect.value}`, {
_fields: "id,title,link",
page: page,
per_page: perPage,
orderby: orderby,
order: order,
}),
parse: false,
})
.then((response) => {
const totalPages = parseInt(response.headers.get("X-WP-TotalPages"), 10);
return response.json().then((posts) => {
if (posts.length > 0) {
const html = posts.map((post) => {
const sanitizedLink = sanitizeUrl(post.link); // URL をサニタイズ
const sanitizedTitle = sanitizeHtml(post.title.rendered); // タイトルをサニタイズ
return `<li><a href="${sanitizedLink}">${sanitizedTitle} (ID: ${post.id})</a></li>`;
}).join("");
postList.insertAdjacentHTML('beforeend', html);
loadMoreButton.style.display = currentPage < totalPages ? 'inline-block' : 'none';
} else {
loadMoreButton.style.display = 'none';
}
});
})
.catch((error) => console.error("Failed to fetch posts:", error));
}
// ページロード時に投稿タイプを取得
document.addEventListener("DOMContentLoaded", loadPostTypes);
loadButton.addEventListener("click", () => {
postList.innerHTML = '';
currentPage = 1;
loadPosts(currentPage);
});
loadMoreButton.addEventListener("click", () => {
currentPage++;
loadPosts(currentPage);
});
これで、投稿タイプを選択するセレクトボックスには、投稿、固定ページ、メディア、カスタム投稿タイプ(Books)のみが表示されます。※ 但し、管理者としてログインしている必要があります。
ログインしていない状態で、ページを表示すると、セレクトボックスには投稿タイプが表示されず、コンソールを確認すると「この投稿タイプの投稿を編集する権限がありません。」というエラーが表示されます。
不要な投稿タイプを除外(2)
以下は、除外する投稿タイプのリストを作成し、除外リストにない投稿タイプのみ表示する例です。
この場合、管理者としてログインしていなくても機能します。但し、メディアは管理者としてログインしていないと取得できないようなので、除外しています。
投稿タイプを取得してセレクトボックスに追加する関数 loadPostTypes() を以下のように書き換えます。
viewable や capabilities にアクセスする必要はないので、エンドポイントのパラメータには context は指定しません。
除外する投稿タイプのキー名のリスト(配列)を作成し、 Array インスタンスのメソッド includes() と論理否定 (!) 演算子を使って除外します。
function loadPostTypes() {
wp.apiFetch({ path: "/wp/v2/types" })
.then((types) => {
postTypeSelect.innerHTML = ""; // 初期化
Object.keys(types).forEach((type) => {
// 除外する投稿タイプのリスト
const excludedTypes = [
"attachment", // メディア
"nav_menu_item", // ナビゲーションメニューの項目
"wp_block", // パターン
"wp_font_face", // フォントフェイス
"wp_font_family", // フォントファミリー
"wp_global_styles", // グローバルスタイル
"wp_navigation", // ナビゲーションメニュー
"wp_template", // テンプレート
"wp_template_part", // テンプレートパーツ
];
// 除外リストにない投稿タイプのみ表示(option に設定)
if (!excludedTypes.includes(type)) {
const option = document.createElement("option");
const typeData = types[type];
option.value = typeData.rest_base;
option.textContent = typeData.name;
postTypeSelect.appendChild(option);
}
});
})
.catch((error) => console.error("Failed to fetch post types:", error));
}
更新した fetch-posts.js のコード全体は以下のようになります(変更は loadPostTypes() の部分のみ) 。
const sanitizeUrl = (url) => {
try {
const sanitizedUrl = new URL(url);
if (sanitizedUrl.protocol === 'http:' || sanitizedUrl.protocol === 'https:') {
return sanitizedUrl.href;
}
} catch (e) {
console.warn("Invalid URL:", url);
}
return '';
};
const sanitizeHtml = (htmlRendered) => {
if (!htmlRendered) return "";
const elem = document.createElement("div");
elem.innerHTML = htmlRendered;
return elem.textContent || elem.innerText || "";
};
let currentPage = 1;
const loadButton = document.getElementById("fetch-posts-load-posts");
const loadMoreButton = document.getElementById("fetch-posts-load-more");
const postList = document.getElementById("fetch-posts-list");
const perPage = 5;
const orderbySelect = document.getElementById("fetch-posts-orderby");
const postTypeSelect = document.getElementById("fetch-posts-post-types");
function loadPostTypes() {
wp.apiFetch({ path: "/wp/v2/types" })
.then((types) => {
postTypeSelect.innerHTML = ""; // 初期化
Object.keys(types).forEach((type) => {
// 除外する投稿タイプのリスト
const excludedTypes = [
"attachment", // メディア
"nav_menu_item", // ナビゲーションメニューの項目
"wp_block", // パターン
"wp_font_face", // フォントフェイス
"wp_font_family", // フォントファミリー
"wp_global_styles", // グローバルスタイル
"wp_navigation", // ナビゲーションメニュー
"wp_template", // テンプレート
"wp_template_part", // テンプレートパーツ
];
// 除外リストにない投稿タイプのみ表示
if (!excludedTypes.includes(type)) {
const option = document.createElement("option");
const typeData = types[type];
option.value = typeData.rest_base;
option.textContent = typeData.name;
postTypeSelect.appendChild(option);
}
});
})
.catch((error) => console.error("Failed to fetch post types:", error));
}
function loadPosts(page = 1) {
const orderby = orderbySelect.value;
const order = document.querySelector('[name="fetch-posts-order"]:checked').value;
wp.apiFetch({
path: wp.url.addQueryArgs(`/wp/v2/${postTypeSelect.value}`, {
_fields: "id,title,link",
page: page,
per_page: perPage,
orderby: orderby,
order: order,
}),
parse: false,
})
.then((response) => {
const totalPages = parseInt(response.headers.get("X-WP-TotalPages"), 10);
return response.json().then((posts) => {
if (posts.length > 0) {
const html = posts.map((post) => {
const sanitizedLink = sanitizeUrl(post.link);
const sanitizedTitle = sanitizeHtml(post.title.rendered);
return `<li><a href="${sanitizedLink}">${sanitizedTitle} (ID: ${post.id})</a></li>`;
}).join("");
postList.insertAdjacentHTML('beforeend', html);
loadMoreButton.style.display = currentPage < totalPages ? 'inline-block' : 'none';
} else {
loadMoreButton.style.display = 'none';
}
});
})
.catch((error) => console.error("Failed to fetch posts:", error));
}
// ページロード時に投稿タイプを取得
document.addEventListener("DOMContentLoaded", loadPostTypes);
loadButton.addEventListener("click", () => {
postList.innerHTML = '';
currentPage = 1;
loadPosts(currentPage);
});
loadMoreButton.addEventListener("click", () => {
currentPage++;
loadPosts(currentPage);
});
即時実行関数を使う
これまでの例では、currentPage, loadButton などの変数は全てグローバル変数となっていますが、即時実行関数 (IIFE) を使うとグローバル変数を隠蔽する(グローバル名前空間の汚染を避ける)ことができます。
IIFE は Immediately Invoked Function Expression の省略形です。
即時実行関数 (IIFE) にするには、スクリプト全体を (() => { スクリプト })();
で囲むだけです。
(() => {
// ここにスクリプトを記述
})();
前述の fetch-posts.js は以下のように記述することができます。
このように記述することで、すべての変数 (sanitizeUrl, currentPage, loadButton など) が IIFE 内部のローカルスコープに閉じ込められるため、他のスクリプトと変数名が衝突するリスクを防げます。
(() => {
const sanitizeUrl = (url) => {
try {
const sanitizedUrl = new URL(url);
if (sanitizedUrl.protocol === 'http:' || sanitizedUrl.protocol === 'https:') {
return sanitizedUrl.href;
}
} catch (e) {
console.warn("Invalid URL:", url);
}
return '';
};
const sanitizeHtml = (htmlRendered) => {
if (!htmlRendered) return "";
const elem = document.createElement("div");
elem.innerHTML = htmlRendered;
return elem.textContent || elem.innerText || "";
};
let currentPage = 1;
const loadButton = document.getElementById("fetch-posts-load-posts");
const loadMoreButton = document.getElementById("fetch-posts-load-more");
const postList = document.getElementById("fetch-posts-list");
const perPage = 5;
const orderbySelect = document.getElementById("fetch-posts-orderby");
const postTypeSelect = document.getElementById("fetch-posts-post-types");
function loadPostTypes() {
wp.apiFetch({ path: "/wp/v2/types" })
.then((types) => {
postTypeSelect.innerHTML = ""; // 初期化
Object.keys(types).forEach((type) => {
// 除外する投稿タイプのリスト
const excludedTypes = [
"attachment", // メディア
"nav_menu_item", // ナビゲーションメニューの項目
"wp_block", // パターン
"wp_font_face", // フォントフェイス
"wp_font_family", // フォントファミリー
"wp_global_styles", // グローバルスタイル
"wp_navigation", // ナビゲーションメニュー
"wp_template", // テンプレート
"wp_template_part", // テンプレートパーツ
];
// 除外リストにない投稿タイプのみ表示
if (!excludedTypes.includes(type)) {
const option = document.createElement("option");
const typeData = types[type];
option.value = typeData.rest_base;
option.textContent = typeData.name;
postTypeSelect.appendChild(option);
}
});
})
.catch((error) => console.error("Failed to fetch post types:", error));
}
function loadPosts(page = 1) {
const orderby = orderbySelect.value;
const order = document.querySelector('[name="fetch-posts-order"]:checked').value;
wp.apiFetch({
path: wp.url.addQueryArgs(`/wp/v2/${postTypeSelect.value}`, {
_fields: "id,title,link",
page: page,
per_page: perPage,
orderby: orderby,
order: order,
}),
parse: false,
})
.then((response) => {
const totalPages = parseInt(response.headers.get("X-WP-TotalPages"), 10);
return response.json().then((posts) => {
if (posts.length > 0) {
const html = posts.map((post) => {
const sanitizedLink = sanitizeUrl(post.link);
const sanitizedTitle = sanitizeHtml(post.title.rendered);
return `<li><a href="${sanitizedLink}">${sanitizedTitle} (ID: ${post.id})</a></li>`;
}).join("");
postList.insertAdjacentHTML('beforeend', html);
loadMoreButton.style.display = currentPage < totalPages ? 'inline-block' : 'none';
} else {
loadMoreButton.style.display = 'none';
}
});
})
.catch((error) => console.error("Failed to fetch posts:", error));
}
// ページロード時に投稿タイプを取得
document.addEventListener("DOMContentLoaded", loadPostTypes);
loadButton.addEventListener("click", () => {
postList.innerHTML = '';
currentPage = 1;
loadPosts(currentPage);
});
loadMoreButton.addEventListener("click", () => {
currentPage++;
loadPosts(currentPage);
});
})();
REST API でカスタムフィールドを使う
WordPress の REST API は広範なデータ型のスキーマをサポートしていますが、カスタムフィールドのようにコアスキーマに含まれないデータを保存・取得する必要がある場合もあります。
REST API でカスタムフィールドを扱うには、以下の方法があります。
- register_meta() や register_post_meta() を使用する:
特定のカスタムフィールドを REST API に公開し、適切なスキーマや権限を設定できます。 - register_rest_field() を使用する:
既存の REST API エンドポイントに独自のフィールドを追加し、データの取得や更新のロジックを定義できます。
これにより、REST API リクエストでカスタムフィールドのデータを適切に扱うことができます。
カスタムフィールド (メタデータとも呼ばれます) は、投稿タイプに固有の追加データを保存するための仕組みで、内部的には、カスタムフィールドは postmeta テーブルに、投稿 ID に紐づくキーと値のペアとして保存されます。
WP REST API を使用すると、カスタムフィールドを含む投稿データを取得・更新できます。但し、デフォルトではカスタムフィールドのデータは REST API で公開されないため、適切に登録する必要があります。
特定のカスタムフィールドを REST API に公開
REST API を通じてカスタムフィールドを扱うには、対象の投稿タイプのメタプロパティにキーと値のペアのオブジェクトを渡します。
そのためには、事前に register_meta() や register_post_meta() 関数を使用してカスタムフィールドを REST API に登録する必要があります。この際、適切なスキーマや権限を設定することで、REST API 経由での取得・更新を制御できます。
register_meta()
register_meta() は投稿、ユーザー、タクソノミーなど、様々なオブジェクトのメタデータを登録できる汎用的な関数です。
引数 | 型 | 説明 |
---|---|---|
$object_type | string | メタデータを関連付けるオブジェクトの種類(例: 'post', 'comment', 'user', 'term') |
$meta_key | string | 登録するメタキー(カスタムフィールド名) |
$args | array | メタデータのオプション |
オプション | 型 | 説明 |
---|---|---|
single | bool | true にすると 1 つの値のみを保存、false だと配列として複数の値を保存可能 |
type | string | メタデータの型('string', 'boolean', 'integer', 'number', 'array', 'object') |
default | mixed | 値が設定されていない場合のデフォルト値 |
show_in_rest | bool/array | REST API で公開する場合は true、または詳細なスキーマを含む配列を指定 |
auth_callback | callable | メタデータの読み書き権限を制御するコールバック関数 |
object_subtype | string | $object_type で指定したオブジェクトを特定のカスタム投稿タイプやタクソノミーに対してメタデータを登録する場合に使用。register_post_meta() では内部的に $post_type を object_subtype に指定しているので不要。 |
register_post_meta()
register_post_meta() は、投稿(カスタム投稿タイプを含む)のメタデータを登録するための専用関数です。register_post_meta() は、register_meta() を投稿専用にラップした関数で、内部的に $post_type を register_meta() の object_subtype に指定しています。
引数 | 型 | 説明 |
---|---|---|
$post_type | string | メタデータを関連付ける投稿タイプ(例: 'post', 'page', 'custom_post_type') |
$meta_key | string | 登録するメタキー(カスタムフィールド名) |
$args | array | メタデータのオプション(register_meta() と共通) |
- 投稿(カスタム投稿含む)のメタデータ登録には register_post_meta() を使うのがベストプラクティス
- タクソノミーやコメントメタデータの登録には register_meta() を使う
register_meta() や register_post_meta() は、プラグインファイルや functions.php に記述します。
使用例
例えば、投稿(post)に location というカスタムフィールドを登録したい場合は、プラグインファイルや functions.php で、次のように register_post_meta() 関数を使用します。
この処理は、init アクションフック内で実行することが推奨されます。また、REST API でカスタムフィールドを扱うには、show_in_rest プロパティを true に設定する必要があります。
add_action( 'init', 'my_post_register_meta');
function my_post_register_meta() {
register_post_meta(
'post', // 投稿タイプ
'location',
array(
'single' => true,
'type' => 'string',
'default' => '',
'show_in_rest' => true, // REST API でカスタムフィールドを扱う
)
);
}
さらに、スキーマを指定することで、API レスポンスのデータ構造を明確に定義できます。
show_in_rest にスキーマを指定することで、API のレスポンスに適切な情報を含めることができます。
add_action('init', 'my_post_register_meta');
function my_post_register_meta() {
register_post_meta(
'post', // 投稿タイプ
'location',
array(
'single' => true,
'type' => 'string',
'default' => '',
'show_in_rest' => array(
'schema' => array(
'type' => 'string',
'description' => '投稿の場所情報',
'context' => array('view', 'edit', 'embed'),
),
),
)
);
}
これにより、カスタムフィールドを投稿の REST API スキーマに追加できるようになり、REST API を使用してカスタムフィールドにデータを投稿することも可能になります。
REST API を使用してカスタムフィールドにデータを投稿する場合、リクエストボディ(body)のメタオブジェクト(meta)でカスタムフィールドをキーと値のペアとして渡します。
{
"title": "New Post Title",
"content": "New Post Content",
"status": "publish",
"meta": {
"location": "Tokyo"
}
}
これをテストするには、例えば、REST API テストツールの Postman で新しい POST リクエストを作成し、そのリクエストを posts ルートに送信します。
リクエストが成功すると、レスポンスの meta フィールドの中に指定したカスタムフィールド location が確認できます。
また、作成した投稿を確認するとカスタムフィールドが設定されています。
GET クエリで検索可能に
GET でパラメータを指定して特定のメタデータ(例: meta[location]=Tokyo)を持つ投稿を取得する場合、以下では期待通りには取得できず、全ての投稿が取得されてしまいます。
GET /wp-json/wp/v2/posts?meta_key=location&meta_value=Tokyo
filter フックを使って meta_query を拡張
方法としては、rest_{post_type}_query フックを利用して、meta_query をカスタマイズします。
以下のコードをプラグインファイルまたは functions.php に追加します。
REST API で投稿を取得するときのクエリ($args)をカスタマイズする関数を定義し、rest_post_query フックを使い、REST API の投稿検索クエリをカスタマイズしています。関数の第2引数 $request は API リクエストのデータ(GET で渡されたパラメータ)です。
function my_rest_filter_by_meta_location($args, $request) {
if (isset($request['meta']['location'])) {
$args['meta_query'][] = array(
'key' => 'location',
'value' => sanitize_text_field($request['meta']['location']),
'compare' => 'LIKE', // 部分一致(完全一致なら '=' に変更)
);
}
return $args;
}
add_filter('rest_post_query', 'my_rest_filter_by_meta_location', 10, 2);
クエリの meta パラメータに "location" が指定されているかチェックし(2行目)、$request['meta']['location'] に値が設定されている場合のみ、WP_Query の meta_query に検索条件を追加しています。
- key : 検索対象のカスタムフィールド名(この場合 "location")。
- value : 検索する値(例: "Tokyo")。 sanitize_text_field() を使って安全なデータに変換。
- compare : 検索条件 LIKE は部分一致。完全一致にしたい場合は = に変更。
そして、修正した $args(検索条件付き)を返しています。
投稿を検索
例えば、location=Tokyo の投稿を検索するには以下を指定します。
GET /wp-json/wp/v2/posts?meta[location]=Tokyo
これで、location=Tokyo の投稿のみを取得できます。
エンドポイントに独自のフィールドを追加
register_rest_field() を使って、既存の REST API エンドポイントに独自データのフィールド(カスタムフィールド)を追加し、データの取得や更新のロジックを定義することができます。
register_rest_field()
register_rest_field() は WP REST API に独自のフィールドを追加するための関数です。
特定のエンドポイント(投稿、カスタム投稿タイプ、ユーザー、コメントなど)のトップレベルのフィールドにカスタムフィールドなど独自のフィールドを追加することができます。
引数 | 型 | 説明 |
---|---|---|
$object_type | string または array | フィールドを追加するオブジェクトのタイプ(例: 'post', 'page', 'custom_post_type', 'user' など) |
$attribute | string | REST API に追加するカスタムフィールドの名前 |
$args | array | 追加するフィールドの詳細設定(取得・更新用のコールバック、スキーマ) |
$args には、以下のキーを含む配列を指定できます。
オプション | 型 | 説明 |
---|---|---|
get_callback | callable | REST API のレスポンスにフィールドを追加するときに実行される(フィールドの値を取得する)関数。以下のパラメータを受け取ります。
|
update_callback | callable | REST API を通じてフィールドを更新するときに実行される関数。以下のパラメータを受け取ります。
|
schema | array | フィールドのデータのスキーマを定義。以下は指定できる主なキーです。
|
register_rest_field() は基本的に rest_api_init にフックする必要があります(REST API が正しく初期化された後に実行するため)。
以下は、register_rest_field() を使って WordPress の REST API のエンドポイントのトップレベルのフィールドにスタムフィールドを追加する例です。
具体的には、投稿(post)に location というカスタムフィールドを追加し、REST API 経由で取得・更新できるようにします。前項の register_post_meta() を使う場合と同じカスタムフィールド location を使用するので、前項で定義した my_post_register_meta() は削除しておきます。
add_action('rest_api_init', function () {
register_rest_field(
'post', // 対象の投稿タイプ(ここでは通常の投稿)
'location', // 追加するフィールド名
array(
'get_callback' => 'my_get_location', // 値を取得する関数
'update_callback' => 'my_update_location', // 値を更新する関数
'schema' => array( // フィールドのデータ仕様(スキーマ)
'description' => '投稿のロケーション情報', // フィールドの説明
'type' => 'string', // 文字列データ
'context' => array('view', 'edit'), // view(閲覧時)と edit(編集時)に利用可能
),
)
);
});
// カスタムフィールドの値を取得する関数
function my_get_location($object, $field_name) {
return get_post_meta($object['id'], $field_name, true);
}
// カスタムフィールドの値を更新する関数
function my_update_location($value, $object, $field_name) {
return update_post_meta($object->ID, $field_name, sanitize_text_field($value));
}
get_callback
カスタムフィールドの値を取得する関数 my_get_location() は、get_post_meta() を使って該当する投稿の location の値を取得し、REST API のレスポンスに含めます。
第1引数 $object は配列なので、連想配列のキー id(小文字)を指定して投稿の ID を取得し、第2引数 $field_nam は register_rest_field() の第2引数の location が渡されます。get_post_meta() の第3引数は true を指定して単一の値を取得しています。
update_callback
カスタムフィールドの値を更新する関数 my_update_location() は、update_post_meta() を使って投稿のカスタムフィールド location の値を更新します。
第1引数 $object は更新対象のオブジェクト(WP_Post や WP_User などのオブジェクト)なので $object->ID で ID プロパティにアクセスしています。リクエストで送信された値($value)は sanitize_text_field() でサニタイズして、update_post_meta() の第3引数に渡します。
リクエスト
register_rest_field() を使用した場合、REST API へのリクエストのボディでは、カスタムフィールド location はトップレベルに指定します。
{
"title": "New Post Title 2",
"content": "New Post Content 2",
"status": "publish",
"location": "Kyoto"
}
例えば、REST API テストツールの Postman で新しい POST リクエストを作成し、そのリクエストを posts ルートに送信します。
リクエストが成功すると、レスポンスのトップレベルのフィールドの中に指定したカスタムフィールド location が確認できます。
また、作成した投稿を確認するとカスタムフィールドが設定されています。
GET クエリで検索可能に
register_rest_field() を使ってエンドポイントに独自データのフィールドを追加した場合も、register_meta() や register_post_meta()を使用してカスタムフィールドを登録した場合同様、REST API のクエリパラメータ(?location=Tokyo)でのフィルタリングは 自動的には動作しません。
WordPress の REST API では、デフォルトでカスタムフィールドをフィルタリングできないため、カスタムフィールドを元に検索するためのカスタムクエリを追加する必要があります。
以下のコードを追加します。コードの内容は GET クエリで検索可能にとほぼ同じです。
rest_post_query フィルターを使用して、?location=Tokyo のようなリクエストで location カスタムフィールドを検索できるように、REST API の投稿検索時のクエリ ($args) をカスタマイズします。
$request['location'] に値が設定されている場合のみ、フィルタリングを適用します。
そして WordPress の WP_Query に meta_query(メタデータを使った検索条件)を追加しています。
- key : 検索対象のカスタムフィールドのキー(location)
- value : ユーザーが検索した値(例: "Tokyo")
- compare : 検索方法(LIKE は部分一致、= は完全一致)
add_filter('rest_post_query', function ($args, $request) {
if (isset($request['location'])) {
$args['meta_query'][] = array(
'key' => 'location',
'value' => sanitize_text_field($request['location']),
'compare' => 'LIKE', // 部分一致(完全一致なら '=' に変更)
);
}
return $args;
}, 10, 2);
これで、location=Tokyo の投稿は GET /wp-json/wp/v2/posts?location=Tokyo で取得できます。
どちらを使うべきか?
REST API でカスタムフィールドを使う場合、register_post_meta() と register_rest_field() のどちらを使うかは、API の設計次第で、開発の目的によります。
方法 | 追加される場所 | リクエスト時の指定方法 | 使いどころ |
---|---|---|---|
register_post_meta() | meta の中 | "meta": { "location": "Tokyo" } | カスタムフィールドを(meta の中で)整理して管理したいとき |
register_rest_field() | トップレベル | "location": "Tokyo" | API のレスポンスをシンプルにしたいとき |
また、それぞれの関数には以下のような役割の違いがあります。
関数名 | 役割 | REST API との関連 |
---|---|---|
register_post_meta() | WordPress のメタデータを登録し、REST API で扱えるようにする | 'show_in_rest' => true に設定すると、自動的に REST API に追加される |
register_rest_field() | 既存の REST API エンドポイントにカスタムフィールドを追加 | get_callback や update_callback を指定して独自のデータ処理を定義できる |
register_post_meta() を使用すると、以下のように show_in_rest => true を指定するだけで、REST API にメタデータが自動的に追加されます。
register_post_meta(
'post',
'location',
array(
'single' => true,
'type' => 'string',
'show_in_rest' => true, // これだけで自動的に REST API で利用可能
)
);
以下のように REST API でカスタムフィールドのデータを加工して返したい(get_callback を定義したい)場合やデータの保存時に追加のバリデーションや加工を行いたい(update_callback を定義したい)場合は、register_post_meta() の show_in_rest では対応できません。
register_rest_field(
'post',
'location',
array(
'get_callback' => function($object, $field_name) {
// 例: すべて大文字で返す
return strtoupper(get_post_meta($object['id'], $field_name, true));
},
'update_callback' => function($value, $object, $field_name) {
return update_post_meta($object->ID, $field_name, sanitize_text_field($value));
},
'schema' => array(
'description' => 'Location metadata for the post',
'type' => 'string',
'context' => array('view', 'edit'),
),
)
);
register_meta() / register_post_meta() を使う方法の場合、実行する必要のある追加のコード(コールバック関数)を追加しないため、よりパフォーマンスの高いオプションでもあります。
一方、register_rest_field() を使う利点は、データが返される前、または保存される前に、データに対して追加の処理を実行できることです。
例えば、データベースに保存される前に、データに対して何らかの検証を実行できます。また、get_callback 関数と update_callback 関数にフックを追加して、データに対して追加の処理を実行したり、他の開発者がカスタムフィールドを拡張できるようにしたりすることもできます。欠点は、実行する必要があるコードが増えるため、API リクエストに若干のオーバーヘッドが追加されることです。
カスタム投稿タイプでの実装例
以下はこれまで使用してきた book カスタム投稿タイプのエンドポイントに isbn というカスタムフィールドを追加する例です。
まず、カスタム投稿タイプの登録でカスタムフィールドが使用できるように supports に custom-fields を追加します。
// カスタム投稿タイプ book の登録
add_action('init', 'booklist_register_book_post_type');
function booklist_register_book_post_type() {
$args = array(
'labels' => array(
'name' => 'Books',
'singular_name' => 'Book',
'menu_name' => 'Books',
'add_new' => 'Add New Book',
'add_new_item' => 'Add New Book',
'new_item' => 'New Book',
'edit_item' => 'Edit Book',
'view_item' => 'View Book',
'all_items' => 'All Books',
),
'public' => true,
'has_archive' => true,
'menu_icon' => 'dashicons-book',
// REST API に公開する
'show_in_rest' => true,
// REST API route の base URL を book から books に変更
'rest_base' => 'books',
// カスタムフィールドをサポート
'supports' => array('title', 'editor', 'author', 'thumbnail', 'excerpt', 'custom-fields'),
);
register_post_type('book', $args);
}
続いて、register_rest_field() を使って book エンドポイントのトップレベルのフィールドにスタムフィールド isbn を追加します。
// book エンドポイントのトップレベルのフィールドにスタムフィールド isbn を追加
add_action('rest_api_init', 'booklist_add_rest_fields');
function booklist_add_rest_fields() {
register_rest_field(
'book',
'isbn',
array(
'get_callback' => 'booklist_rest_get_isbn',
'update_callback' => 'booklist_rest_update_isbn',
'schema' => array(
'description' => __('The ISBN of the book'), // フィールドの説明
'type' => 'string', // 文字列データ
'context' => array('view', 'edit'), // view(閲覧時)と edit(編集時)に利用可能
),
)
);
}
// get_callback(カスタムフィールドの値を取得する関数)
function booklist_rest_get_isbn($object, $field_name) {
return get_post_meta($object['id'], $field_name, true);
}
// update_callback(カスタムフィールドの値を更新する関数)
function booklist_rest_update_isbn($value, $object, $field_name) {
return update_post_meta($object->ID, $field_name, sanitize_text_field($value));
}
そして、GET でパラメータを指定して ?isbn=12345 のようなリクエストで isbn カスタムフィールドを持つ book カスタム投稿タイプの投稿を取得できるように、rest_{post_type}_query フック(rest_book_query フック)を使ってカスタムクエリを追加します。
// GET クエリで検索可能に
add_filter('rest_book_query', function ($args, $request) {
if (isset($request['isbn'])) {
$args['meta_query'][] = array(
'key' => 'isbn',
'value' => sanitize_text_field($request['isbn']),
'compare' => '=', // 完全一致
);
}
return $args;
}, 10, 2);
この例の場合、register_rest_field() を使ってエンドポイントのトップレベルのフィールドにカスタムフィールド isbn を追加しているので、REST API へのリクエストのボディでは、カスタムフィールド isbn はトップレベルに指定します。
{
"title": "New Book with ISBN",
"content": "New Book with ISBN Content",
"status": "publish",
"isbn": "123456789-0"
}
以下は、REST API テストツールの Postman で新しい POST リクエストを作成し、そのリクエストを books ルートに送信しています。
以下は、GET /wp-json/wp/v2/books?isbn=123456789-0 でカスタムフィールド isbn の値が 123456789-0 の投稿を取得しています。
カスタムフィールドの値を検証
register_rest_field() のカスタムフィールドの値を更新する update_callback で、指定されたカスタムフィールド isbn の値を検証する例です。
前述のコードのカスタムフィールドの値を更新する関数では isbn の値を sanitize_text_field() で処理していますが、値を検証して10桁または13桁の数字でなければ、エラーにします。
isbn の値を検証する関数 booklist_validate_isbn() を定義します。この関数は、指定された isbn の値からハイフンを削除し、preg_match() で正規表現を使って10桁または13桁の数字であれば true(1)を返し、そうでなければ false(0)を返します。
そして register_rest_field() のカスタムフィールドの値を更新するコールバックで定義した関数を使って値を検証し、10桁または13桁の数字でなければ、WP_Error クラスを使ってエラーにします。
// isbn の値を検証する関数
function booklist_validate_isbn($isbn) {
// ハイフンを削除
$isbn = str_replace('-', '', $isbn);
// 10桁または13桁の数字であれば true を返す
return preg_match('/^\d{10}(\d{3})?$/', $isbn);
}
// update_callback
function booklist_rest_update_isbn($value, $object, $field_name) {
if (!booklist_validate_isbn($value)) {
// 値が正しくなければ、エラーにする
return new WP_Error('invalid_isbn', __('Invalid ISBN format.', 'booklist'), array('status' => 400));
}
return update_post_meta($object->ID, $field_name, sanitize_text_field($value));
}
WP_Error
WP_Error は WordPress でエラーハンドリングを行うためのクラスです。REST API やプラグイン開発時に、適切なエラーメッセージや HTTP ステータスコードを返すために使用されます。
パラメータ | 型 | 説明 |
---|---|---|
$code | string | int | エラーコード(文字列または数値) |
$message | string | エラーメッセージ。 |
$data | mixed | 追加情報(オプション、配列やステータスコードなど)。 |
以下は基本的な使い方です。
return new WP_Error(
'invalid_isbn', // エラーコード
__('Invalid ISBN format.', 'booklist'), // ユーザー向けのメッセージ(この例では翻訳関数を適用)
array('status' => 400) // HTTP ステータスコード
);
新しい POST リクエストを作成して、book の投稿を作成する際に、正しくない ISBN の値を指定すると以下のようにエラーになり、投稿は作成されません。
プラグイン booklist を更新
管理画面(Book List 管理ページ)で book カスタム投稿タイプの投稿を取得、作成、更新するプラグイン booklist に ISBN の入力欄を追加して、投稿を取得、作成、更新する際にカスタムフィールド isbn を指定できるようにする例です。
プラグインファイル booklist.php の投稿を取得して表示する部分の HTML に以下を追加します。
<div><!-- 追加 -->
<label for="booklist-isbn">ISBN</label><br>
<input type="text" id="booklist-isbn" placeholder="ISBN">
</div>
Book List 管理ページで読み込んでいる JavaScript(booklist-admin.js)の投稿を取得する関数を以下のように書き換えると、 ISBN 入力欄に値が入力されていれば、その値をクエリパラメータの isbn に追加して、指定された ISBN の投稿を取得して表示します。
wp/v2/books のレスポンスに isbn も含めるように _fields に isbn を追加します。
クエリパラメータは別途定義しておき、ISBN 入力欄に isbn が入力されていれば、クエリパラメータに isbn を追加して指定された ISBN の値を持つ投稿のみを取得し、isbn が入力されていなければ、クエリパラメータに isbn を追加しません(isbn の値に関係なく取得します)。
出力する際は、ISBN(book.isbn)が投稿に含まれていれば (ISBN: xxxx) のような形式で出力します。
// 投稿を取得する関数
function loadBooks(page = 1) {
const orderby = orderbySelect.value;
const order = document.querySelector('[name="booklist-order"]:checked').value;
// ISBN 入力欄の値
const isbn = document.getElementById("booklist-isbn").value.trim();
// クエリパラメータを別途定義
const query = {
_fields: "id,title,link,isbn", // isbn を追加
page: page,
per_page: perPage,
orderby: orderby,
order: order,
};
// isbn が入力されていれば、クエリパラメータに追加
if (isbn) query.isbn = isbn;
// addQueryArgs() で path にクエリパラメータを追加
wp.apiFetch({
path: wp.url.addQueryArgs("/wp/v2/books", query),
parse: false,
})
.then((response) => {
const totalPages = parseInt(response.headers.get("X-WP-TotalPages"), 10);
return response.json().then((books) => {
if (books.length > 0) {
const newLine = page === 1 ? "" : "\n";
textarea.value += newLine + books
.map((book) => {
// isbn が取得できれば (ISBN: 123456789-0) のような形式で出力
const isbn = book.isbn ? ` (ISBN: ${book.isbn}) `: "";
return `${book.title.rendered} (${book.id}) ${book.link} ${isbn}`}
).join("\n");
loadMoreButton.style.display = currentPage < totalPages ? 'inline-block' : 'none';
} else {
loadMoreButton.style.display = 'none';
}
});
})
.catch((error) => console.error("Failed to fetch books:", error));
}
以下は、投稿を作成・更新する際もカスタムフィールド isbn を入力できるようにしたコードの全体です。
<?php
/**
* Plugin Name: Booklist
* Description: A plugin to manage books
* Version: 0.0.1
*
*/
if (! defined('ABSPATH')) {
exit; // Exit if accessed directly.
}
// カスタム投稿タイプ book の登録
add_action('init', 'booklist_register_book_post_type');
function booklist_register_book_post_type() {
$args = array(
'labels' => array(
'name' => 'Books',
'singular_name' => 'Book',
'menu_name' => 'Books',
'add_new' => 'Add New Book',
'add_new_item' => 'Add New Book',
'new_item' => 'New Book',
'edit_item' => 'Edit Book',
'view_item' => 'View Book',
'all_items' => 'All Books',
),
'public' => true,
'has_archive' => true,
'menu_icon' => 'dashicons-book',
// REST API に公開する
'show_in_rest' => true,
// REST API route の base URL を book から books に変更
'rest_base' => 'books',
// カスタムフィールドをサポート
'supports' => array('title', 'editor', 'author', 'thumbnail', 'excerpt', 'custom-fields'),
);
register_post_type('book', $args);
}
// book エンドポイントのトップレベルのフィールドにスタムフィールド isbn を追加
add_action('rest_api_init', 'booklist_add_rest_fields');
function booklist_add_rest_fields() {
register_rest_field(
'book',
'isbn',
array(
'get_callback' => 'booklist_rest_get_isbn',
'update_callback' => 'booklist_rest_update_isbn',
'schema' => array(
'description' => __('The ISBN of the book'), // フィールドの説明
'type' => 'string', // 文字列データ
'context' => array('view', 'edit'), // view(閲覧時)と edit(編集時)に利用可能
),
)
);
}
// get_callback
function booklist_rest_get_isbn($object, $field_name) {
return get_post_meta($object['id'], $field_name, true);
}
// isbn の値を検証する関数
function booklist_validate_isbn($isbn) {
// ハイフンを削除
$isbn = str_replace('-', '', $isbn);
// 10桁または13桁の数字であれば true を返す
return preg_match('/^\d{10}(\d{3})?$/', $isbn);
}
// update_callback
function booklist_rest_update_isbn($value, $object, $field_name) {
if (!booklist_validate_isbn($value)) {
// 値が正しくなければ、エラーにする
return new WP_Error(
'invalid_isbn',
__('Invalid ISBN format.', 'booklist'),
array('status' => 400)
);
}
return update_post_meta($object->ID, $field_name, sanitize_text_field($value));
}
// GET クエリで検索可能に
add_filter('rest_book_query', function ($args, $request) {
if (isset($request['isbn'])) {
$args['meta_query'][] = array(
'key' => 'isbn',
'value' => sanitize_text_field($request['isbn']),
'compare' => '=', // 完全一致
);
}
return $args;
}, 10, 2);
// サブメニューページを追加
add_action('admin_menu', 'booklist_add_submenu', 11);
function booklist_add_submenu() {
add_submenu_page(
'edit.php?post_type=book', // 親メニューのスラッグ
'Book List', // サブメニューページのタイトル
'Book List', // サブメニューページのメニューのタイトル
'edit_posts', // サブメニューページにアクセスできるユーザーの権限
'book-list', // サブメニューページを参照するスラッグ
'booklist_render_book_list' // サブメニューページを描画するコールバック関数
);
}
// サブメニューページのHTMLを描画するコールバック関数
function booklist_render_book_list() {
?>
<div class="wrap" id="booklist-admin">
<h1>Actions</h1>
<div style="margin-top:20px">
<label for="booklist-orderby">Order by:</label>
<select id="booklist-orderby">
<option value="date">Date</option>
<option value="title">Title</option>
<option value="modified">Last Modified</option>
<option value="id">ID</option>
</select>
<label>
<input type="radio" name="booklist-order" value="asc"> Ascending
</label>
<label>
<input type="radio" name="booklist-order" value="desc" checked> Descending
</label>
</div>
<div style="margin-top:20px"><!-- 追加 -->
<label for="booklist-isbn">ISBN</label><br>
<input type="text" id="booklist-isbn" placeholder="ISBN">
</div>
<div style="margin-top:20px">
<button id="booklist-load-books">Load Books</button>
</div>
<h2>Books</h2>
<textarea id="booklist-listarea" cols="90" rows="20"></textarea>
<div>
<button id="booklist-load-more" style="display:none;">Load More</button>
</div>
<div style="width:50%; margin-top:300px">
<h2>Add Book</h2>
<div>
<label for="booklist-book-title">Book Title</label><br>
<input type="text" id="booklist-book-title" placeholder="Title">
</div>
<div>
<label for="booklist-book-content">Book Content</label><br>
<textarea id="booklist-book-content" cols="70" rows="10"></textarea>
</div>
<div><!-- 追加 -->
<label for="booklist-book-isbn">ISBN</label><br>
<input type="text" id="booklist-book-isbn" placeholder="ISBN">
</div>
<div style="margin-top:20px">
<button id="booklist-submit-book">Add</button>
</div>
</div>
<div style="width:50%;">
<h2>Update Book</h2>
<div>
<label for="booklist-update-book-id">Book ID</label><br>
<input type="text" id="booklist-update-book-id" placeholder="ID">
</div>
<div>
<label for="booklist-update-book-title">Book Title</label><br>
<input type="text" id="booklist-update-book-title" placeholder="Title">
</div>
<div>
<label for="booklist-update-book-content">Book Content</label><br>
<textarea id="booklist-update-book-content" cols="70" rows="10"></textarea>
</div>
<div><!-- 追加 -->
<label for="booklist-update-book-isbn">ISBN</label><br>
<input type="text" id="booklist-update-book-isbn" placeholder="ISBN">
</div>
<div style="margin-top:20px">
<button id="booklist-update-book">Update</button>
</div>
</div>
<div style="width:50%;">
<h2>Delete Book</h2>
<div>
<label for="booklist-delete-book-id">Book ID</label><br>
<input type="text" id="booklist-delete-book-id" placeholder="ID">
</div>
<div>
<input type="checkbox" id="booklist-delete-force">
<label for="booklist-delete-force">Force Delete</label>
</div>
<div>
<button id="booklist-delete-book">Delete</button>
</div>
</div>
</div>
<?php
}
// サブメニューページに JavaScript ファイル(booklist-admin.js)を読み込む
add_action('admin_enqueue_scripts', 'booklist_admin_enqueue_scripts');
function booklist_admin_enqueue_scripts($hook) {
$allowed_pages = ['book_page_book-list'];
if (in_array($hook, $allowed_pages)) {
wp_enqueue_script(
'booklist-admin',
plugin_dir_url(__FILE__) . '/booklist-admin.js',
array('wp-api-fetch'),
'1.0.0',
true
);
}
}
// 投稿を取得してリスト表示する処理
let currentPage = 1;
const loadButton = document.getElementById("booklist-load-books");
const loadMoreButton = document.getElementById("booklist-load-more");
const textarea = document.getElementById("booklist-listarea");
const perPage = 10;
const orderbySelect = document.getElementById("booklist-orderby");
// 投稿を取得する関数
function loadBooks(page = 1) {
const orderby = orderbySelect.value;
const order = document.querySelector('[name="booklist-order"]:checked').value;
// ISBN 入力欄の値
const isbn = document.getElementById("booklist-isbn").value.trim();
// クエリパラメータを別途定義
const query = {
_fields: "id,title,link,isbn", // isbn を追加
page: page,
per_page: perPage,
orderby: orderby,
order: order,
};
// isbn が入力されていれば、クエリパラメータに追加
if (isbn) query.isbn = isbn;
// addQueryArgs() で path にクエリパラメータを追加
wp.apiFetch({
path: wp.url.addQueryArgs("/wp/v2/books", query),
parse: false,
})
.then((response) => {
const totalPages = parseInt(response.headers.get("X-WP-TotalPages"), 10);
return response.json().then((books) => {
if (books.length > 0) {
const newLine = page === 1 ? "" : "\n";
textarea.value += newLine + books
.map((book) => {
// isbn が取得できれば (ISBN: 123456789-0) のような形式で出力
const isbn = book.isbn ? ` (ISBN: ${book.isbn}) `: "";
return `${book.title.rendered} (${book.id}) ${book.link} ${isbn}`}
).join("\n");
loadMoreButton.style.display = currentPage < totalPages ? 'inline-block' : 'none';
} else {
loadMoreButton.style.display = 'none';
}
});
})
.catch((error) => console.error("Failed to fetch books:", error));
}
loadButton.addEventListener("click", () => {
textarea.value = "";
currentPage = 1;
loadBooks(currentPage);
});
loadMoreButton.addEventListener("click", () => {
currentPage++;
loadBooks(currentPage);
});
// POST リクエスト送信用ボタン
const submitBookButton = document.getElementById("booklist-submit-book");
if (submitBookButton) {
submitBookButton.addEventListener("click", () => {
const title = document.getElementById("booklist-book-title").value;
const content = document.getElementById("booklist-book-content").value;
if (!title || !content) {
alert("Both title and content are required.");
return;
}
// POST リクエストの ISBN 入力欄の値
const isbnPost = document.getElementById("booklist-book-isbn").value.trim();
// data を別途定義
const data = {
title: title,
content: content,
status: "publish",
}
// ISBN が入力されていれば data に追加
if (isbnPost) data.isbn = isbnPost;
wp.apiFetch({
path: "/wp/v2/books/",
method: "POST",
data: data, // 別途定義した data を指定
})
.then((result) => {
alert("Book saved!");
console.log("Book created:", result);
})
.catch((error) => {
console.error("Failed to create book:", error);
alert("Failed to save book.");
});
});
}
// PUT リクエスト送信用ボタン
const updateBookButton = document.getElementById("booklist-update-book");
if (updateBookButton) {
updateBookButton.addEventListener("click", function () {
const id = document.getElementById("booklist-update-book-id").value.trim();
const title = document.getElementById("booklist-update-book-title").value.trim();
const content = document.getElementById("booklist-update-book-content").value.trim();
// PUT リクエストの ISBN 入力欄の値
const isbnPut = document.getElementById("booklist-update-book-isbn").value.trim();
// 判定条件に ISBN も追加
if (!id || isNaN(id) || !(title || content || isbnPut)) {
alert("ID は数値であり、タイトルまたはコンテンツ、ISBN のいずれかが必要です。");
return;
}
wp.apiFetch({ path: `/wp/v2/books/${id}`, method: "GET" })
.then((currentBook) => {
return wp.apiFetch({
path: `/wp/v2/books/${id}`,
method: "PUT",
data: {
title: title || currentBook.title.rendered,
content: content || currentBook.content.rendered,
isbn: isbnPut || currentBook.isbn, // ISBN が空なら元の値を保持
status: currentBook.status,
},
});
})
.then((updatedBook) => {
alert("Book updated successfully!");
console.log("Book updated:", updatedBook);
})
.catch((error) => {
console.error("Failed to update book:", error);
alert("Failed to update book: " + (error.message || "Unknown error"));
});
});
}
// DELETE リクエスト送信用ボタン
const deleteBookButton = document.getElementById("booklist-delete-book");
if (deleteBookButton) {
deleteBookButton.addEventListener("click", function () {
const id = document.getElementById("booklist-delete-book-id").value.trim();
const forceDelete = document.getElementById(
"booklist-delete-force"
).checked;
if (!id || isNaN(id)) {
alert("ID は数値ででなければなりません。");
return;
}
wp.apiFetch({
path: `/wp/v2/books/${id}`,
method: "DELETE",
data: forceDelete ? { force: true } : {},
})
.then((result) => {
alert("Book deleted!");
console.log("Book deleted:", result);
})
.catch((error) => {
console.error("Failed to delete book:", error);
alert("Failed to delete book: " + (error.message || "Unknown error"));
});
});
}
外部から投稿を取得して表示
WordPress 外部から JavaScript で WordPress REST API を使って投稿のデータを取得して、タイトルにリンクを付けて一覧表示する例です。
この例では、id が fetch-posts の div 要素があれば、JavaScript でその要素内に各投稿のリンクを ul と li 要素でマークアップして一覧として出力します。
<div id="fetch-posts"></div>
以下の JavaScript を作成します。HTML が記述されているページの script 要素に記述することもできますし、ファイルを作成して記述し、HTML が記述されているページで読み込むこともできます。
リンク文字列を検証する関数 sanitizeUrl と HTML からテキストを取得する関数 sanitizeHtml を定義して、リンクと HTML が含まれる可能性のある文字列を念の為サニタイズします。
そしてエンドポイントの URL と出力先の要素を受取って、投稿のタイトルとリンクを取得してその一覧を出力する関数 renderPostLinks を定義し、DOMContentLoaded イベントで呼び出します。
エンドポイントへのリクエストの送信は、JavaScript(Fetch API)の fetch() メソッドを使用します。
エンドポイントに追加するパラメータは見やすいようにオブジェクト形式で定義し、URLSearchParams とその toString() メソッドを使ってクエリ文字列に変換します。
パラメータには、_fields=title,link を指定することで必要なデータのみ取得し、API のレスポンスサイズを削減してパフォーマンスを最適化しています(41行目)。1ページあたりの取得件数(per_page)や並び順(order)は環境に合わせて適宜変更します。
(() => {
// URL サニタイズ用の関数
const sanitizeUrl = (url) => {
try {
const sanitizedUrl = new URL(url);
if ( sanitizedUrl.protocol === "http:" || sanitizedUrl.protocol === "https:") {
// protocol が 'http:' または 'https:' の場合のみ許可
return sanitizedUrl.href;
}
} catch (e) {
console.warn("Invalid URL:", url);
}
return ""; // 無効な URL は空文字を返す
};
// HTML からテキストを取得する関数
const sanitizeHtml = (htmlRendered) => {
if (!htmlRendered) return "";
const elem = document.createElement("div");
elem.innerHTML = htmlRendered;
return elem.textContent || elem.innerText || "";
};
// 指定されたエンドポイントの URL から投稿のタイトルとリンクを取得して出力する関数
const renderPostLinks = (url, target) => {
const sanitizedUrl = sanitizeUrl(url);
// url が無効な場合は終了
if (!sanitizedUrl) {
console.error("無効な API URL");
return;
}
// 投稿リストを出力する ul 要素を作成し、出力先に追加
const list = document.createElement("ul");
list.setAttribute("id", "post-list");
list.style.setProperty("list-style-type", "none");
target.appendChild(list);
// URLSearchParams でオブジェクト形式のパラメータをクエリ文字列に変換
const params = new URLSearchParams({
_fields: "title,link", // 必要なデータ(タイトルとリンク)のみ取得してレスポンスサイズを削減
per_page: 20, // 20件表示(デフォルト 10)
order: "asc", // 古い記事から表示(デフォルト 'desc')
}).toString();
// fetch() メソッドで GET リクエストを送信
fetch(`${sanitizedUrl}?${params}`)
.then((response) => {
if (!response.ok)
throw new Error(
`リクエスト失敗: ${response.status} ${response.statusText}`
);
return response.json();
})
.then((posts) => {
list.innerHTML = posts
.map((post) => {
const sanitizedLink = sanitizeUrl(post.link);
const sanitizedTitle = sanitizeHtml(post.title?.rendered);
// sanitizedLink が 有効な URL の場合のみ、<li> 要素を作成
return sanitizedLink
? `<li><a href="${sanitizedLink}">${sanitizedTitle}</a></li>`
: "";
})
.join(""); // map()で作成された配列をjoin('')で1つの文字列に結合(空文字列を指定することで、カンマ,が入らないように)
})
.catch((error) => {
console.warn("投稿の取得エラー:", error);
if (list)
list.innerHTML = `<li>投稿の取得に失敗しました:${error.message}</li>`;
});
};
document.addEventListener("DOMContentLoaded", () => {
// エンドポイントの URL (http://example.com の部分は適宜変更)
const url = "http://example.com/wp-json/wp/v2/posts";
// 出力先の要素
const target = document.getElementById("fetch-posts");
// 出力先の要素が存在すれば
if (target) {
// エンドポイントの URL と出力先の要素を指定して、投稿を取得して表示
renderPostLinks(url, target);
}
});
})();
REST API から取得した投稿のデータを HTML のリストに変換する処理(55-66行目)では、map() と join() を使って HTML の <li> 要素を作成し、それをまとめて list.innerHTML にセットしています。
その際、sanitizeUrl() で URL をサニタイズし、sanitizedLink が有効な URL の場合のみ、<li> 要素を作成します(sanitizedLink が空文字の場合はその投稿はリストに追加されません)。
.map() が生成する新しい配列は、["<li>...</li>", "<li>...</li>", ...]
のようになるので、.join() を使って配列を1つの文字列に結合します。.join() には空の文字列を指定することでカンマ(,)が入らないようにします。
また、上記の処理(55-66行目)は、HTML を文字列として一気に作成しているので効率的(高速)ですが、.innerHTML を直接代入する方法は XSS のリスクもあります(この例の場合はリンクもタイトル文字列もサニタイズしているので問題ありません)。
XSS のリスクを避ける方法として、以下のように DocumentFragment を使用することもできます。空の DocumentFragment は document.createDocumentFragment() で作成することができます。
.then((posts) => {
// documentFragment を使用して仮想のコンテナ(フラグメント)を作成
const fragment = document.createDocumentFragment();
posts.forEach((post) => {
const sanitizedLink = sanitizeUrl(post.link);
const sanitizedTitle = sanitizeHtml(post.title?.rendered);
if (sanitizedLink) {
const li = document.createElement("li"); // li 要素を生成
const a = document.createElement("a"); // a 要素を生成
a.href = sanitizedLink;
a.textContent = sanitizedTitle;
li.appendChild(a); // li 要素にリンクを追加
fragment.appendChild(li); // フラグメントに li 要素を追加(この時点では、まだ DOM には追加されない)
}
});
// 最後にまとめてフラグメントを DOM に追加(1回の DOM 更新で済むため効率的)
list.appendChild(fragment);
})
上記のコードは文字列として処理するよりは少し処理速度は落ちますが、documentFragment を使用することで、以下のように DocumentFragment を使わない処理より効率的です。
以下の場合、list.appendChild(li) を直接実行しているので、ループのたびに DOM の再計算・再描画 が発生してしまい、効率的ではありません。
// 効率的でない方法
.then((posts) => {
posts.forEach((post) => {
const sanitizedLink = sanitizeUrl(post.link);
const sanitizedTitle = sanitizeHtml(post.title?.rendered);
if (sanitizedLink) {
const li = document.createElement("li");
const a = document.createElement("a");
a.href = sanitizedLink;
a.textContent = sanitizedTitle;
li.appendChild(a);
list.appendChild(li); // 毎回 DOM を更新してしまう
}
});
})
投稿タイプを指定
HTML 側で以下のようにカスタムデータ属性(data-post-type 属性)を使って投稿タイプの rest_base を指定して、その投稿タイプの投稿を取得して表示する例です。
data-post-type 属性が指定されていない場合やその値が空の場合は、投稿(posts)を取得して表示します。data-post-type 属性に間違った値(例えば page や post)を指定するとエラーになります。
<div id="fetch-posts" data-post-type="pages"></div>
data-post-type には以下を指定できます。
- 投稿:posts
- 固定ページ: pages
- カスタム投稿タイプ:カスタム投稿タイプのスラッグまたは登録時に rest_base に指定した値
JavaScript を以下のように書き換えます。
DOMContentLoaded の中で出力先の要素を取得して、dataset.postType で data-post-type 属性の値を取得し、その値を使って関数 renderPostLinks に指定して実行します。
(() => {
const sanitizeUrl = (url) => {
try {
const sanitizedUrl = new URL(url);
if ( sanitizedUrl.protocol === "http:" || sanitizedUrl.protocol === "https:") {
return sanitizedUrl.href;
}
} catch (e) {
console.warn("Invalid URL:", url);
}
return "";
};
const sanitizeHtml = (htmlRendered) => {
if (!htmlRendered) return "";
const elem = document.createElement("div");
elem.innerHTML = htmlRendered;
return elem.textContent || elem.innerText || "";
};
const renderPostLinks = (url, target) => {
const sanitizedUrl = sanitizeUrl(url);
if (!sanitizedUrl) {
console.error("無効な API URL");
return;
}
const list = document.createElement("ul");
list.setAttribute("id", "post-list");
list.style.setProperty("list-style-type", "none");
target.appendChild(list);
const params = new URLSearchParams({
_fields: "title,link",
per_page: 20,
order: "asc",
}).toString();
fetch(`${sanitizedUrl}?${params}`)
.then((response) => {
if (!response.ok)
throw new Error(
`リクエスト失敗: ${response.status} ${response.statusText}`
);
return response.json();
})
.then((posts) => {
list.innerHTML = posts
.map((post) => {
const sanitizedLink = sanitizeUrl(post.link);
const sanitizedTitle = sanitizeHtml(post.title?.rendered);
return sanitizedLink
? `<li><a href="${sanitizedLink}">${sanitizedTitle}</a></li>`
: "";
})
.join("");
})
.catch((error) => {
console.warn("投稿の取得エラー:", error);
if (list)
list.innerHTML = `<li>投稿の取得に失敗しました:${error.message}</li>`;
});
};
document.addEventListener("DOMContentLoaded", () => {
// URL のプロトコルとホスト名部分(適宜変更)
const host = "http://example.com";
// 出力先の要素
const target = document.getElementById("fetch-posts");
// 出力先の要素が存在すれば
if(target){
// data-post-type 属性を取得(指定されていない場合は posts を適用)
const postTypeBase = target.dataset.postType || "posts";
renderPostLinks(`${host}/wp-json/wp/v2/${postTypeBase}`, target);
}
});
})();
複数表示に対応
これまでの例は、id 属性が指定された1つの要素を対象にしていましたが、class 属性が指定された複数(1つ以上)の要素を対象にできるようにします。
以下は fetch-posts クラスを指定した要素に data-post-type 属性が指定されていれば、指定された投稿タイプの投稿を出力します。 data-post-type 属性を省略した場合は、投稿(posts)を取得して表示します。
必要に応じて見出し(h タグ)を記述しておくこともできます。
<div class="fetch-posts" data-post-type="pages">
<h3>Page</h3>
</div>
<div class="fetch-posts">
<h3>Post</h3>
</div>
<div class="fetch-posts" data-post-type="books">
<h3>Book</h3>
</div>
JavaScript を以下のように書き換えます。
関数 renderPostLinks() では追加で出力先の要素(target)を受け取るように変更します。また、投稿を出力する ul 要素(list)には id 属性ではなく class 属性を設定します。
そして DOMContentLoaded で出力先の要素を getElementsByClassName() で取得します。
getElementsByClassName() は HTMLCollection を返すため、Array.from() で配列に変換してから forEach() でループします。forEach は targets が空でもエラーにならず、単に forEach の中身が実行されないので、targets.length > 0 のチェックなしでも安全です。
(() => {
const sanitizeUrl = (url) => {
try {
const sanitizedUrl = new URL(url);
if ( sanitizedUrl.protocol === "http:" || sanitizedUrl.protocol === "https:") {
return sanitizedUrl.href;
}
} catch (e) {
console.warn("Invalid URL:", url);
}
return "";
};
const sanitizeHtml = (htmlRendered) => {
if (!htmlRendered) return "";
const elem = document.createElement("div");
elem.innerHTML = htmlRendered;
return elem.textContent || elem.innerText || "";
};
// 引数に追加で出力先の要素を受け取る
const renderPostLinks = (url, target) => {
const sanitizedUrl = sanitizeUrl(url);
if (!sanitizedUrl) {
console.error("無効な API URL");
return;
}
const list = document.createElement("ul");
// id ではなく class を設定
list.classList.add("post-list");
list.style.setProperty("list-style-type", "none");
target.appendChild(list);
const params = new URLSearchParams({
_fields: "title,link",
per_page: 5,
order: "desc",
}).toString();
fetch(`${sanitizedUrl}?${params}`)
.then((response) => {
if (!response.ok)
throw new Error(
`リクエスト失敗: ${response.status} ${response.statusText}`
);
return response.json();
})
.then((posts) => {
list.innerHTML = posts
.map((post) => {
const sanitizedLink = sanitizeUrl(post.link);
const sanitizedTitle = sanitizeHtml(post.title?.rendered);
return sanitizedLink
? `<li><a href="${sanitizedLink}">${sanitizedTitle}</a></li>`
: "";
})
.join("");
})
.catch((error) => {
console.warn("投稿の取得エラー:", error);
if (list)
list.innerHTML = `<li>投稿の取得に失敗しました:${error.message}</li>`;
});
};
document.addEventListener("DOMContentLoaded", () => {
// URL(適宜変更)
const host = "http://example.com";
// 出力先の要素(fetch-posts クラスを持つ全ての要素)
const targets = Array.from(document.getElementsByClassName("fetch-posts"));
// 出力先の要素が1つ以上存在すればそこに投稿を表示
targets.forEach((target) => {
// data-post-type 属性が指定されていない場合は posts を適用
const postType = target.dataset.postType || "posts";
renderPostLinks(`${host}/wp-json/wp/v2/${postType}`, target);
});
});
})();
カテゴリーを指定
投稿タイプではなく、カテゴリーを指定する例です。
HTML 側で以下のようにカスタムデータ属性(data-category-ids)を使って、カテゴリーの ID(複数の id 指定可能)を指定すると、そのカテゴリーの投稿を取得して表示します。
data-category-ids 属性を指定しない場合やその値が空または無効な場合は、カテゴリーを指定しないのと同様、全てのカテゴリーを対象にします。見出し(h タグ)はオプションです。
<div class="fetch-posts">
<h3>All Categories</h3>
</div>
<div class="fetch-posts" data-category-ids="11,9">
<h3>Category: Foo & Bar</h3>
</div>
<div class="fetch-posts" data-category-ids="1">
<h3>Category:未分類</h3>
</div>
JavaScript を以下のように書き換えます。
data-category-ids 属性に指定されたカテゴリーIDを取得してその配列を返す関数 getCategoryIds を定義します(22-32行目)。この関数では、dataset.categoryIds で取得した値が数値であることを検証(数値のみをフィルタ)し、値が空であったり、無効な場合は undefined を返します。
投稿を取得して出力する関数 renderPostLinks() では、追加でカテゴリーID を受取り、カテゴリーID が有効な値であれば、クエリパラメータに追加します(62-63行目)。
renderPostLinks() の呼び出しでは、第3引数に getCategoryIds() を指定して getCategoryIds 関数で処理した値を渡します(101,115行目)。
(() => {
const sanitizeUrl = (url) => {
try {
const sanitizedUrl = new URL(url);
if (sanitizedUrl.protocol === "http:" || sanitizedUrl.protocol === "https:") {
return sanitizedUrl.href;
}
} catch (e) {
console.warn("Invalid URL:", url);
}
return "";
};
const sanitizeHtml = (htmlRendered) => {
if (!htmlRendered) return "";
const elem = document.createElement("div");
elem.innerHTML = htmlRendered;
return elem.textContent || elem.innerText || "";
};
// カテゴリーIDを取得してその配列を返す関数
const getCategoryIds = (target) => {
// data-category-ids 属性の値を取得
const categoryData = target.dataset.categoryIds;
if (!categoryData) return undefined;
const ids = categoryData
.split(",") // カンマで分割
.map(id => id.trim()) // 空白を削除
.filter(id => /^\d+$/.test(id)) // 数値のみをフィルタ
.map(id => parseInt(id, 10)); // 数値に変換
return ids.length > 0 ? ids : undefined; // 有効な ID があるなら返す、なければ undefined
};
// 追加で categoryIds を引数に受け取る
const renderPostLinks = (url, target, page = 1, categoryIds = undefined) => {
const sanitizedUrl = sanitizeUrl(url);
if (!sanitizedUrl) {
console.error("無効な API URL");
return;
}
const list = target.querySelector("ul") || document.createElement("ul");
list.classList.add("post-list");
list.style.setProperty("list-style-type", "none");
if (!target.contains(list)) target.appendChild(list);
const loadMoreButton = target.querySelector("button") || document.createElement("button");
if (!target.contains(loadMoreButton)) {
loadMoreButton.textContent = "Load More";
loadMoreButton.style.display = "none";
target.appendChild(loadMoreButton);
}
// カテゴリー以外のクエリパラメータ
const query = {
_fields: "title,link",
page: page,
per_page: 5,
order: "desc",
};
// categoryIds が undefined でなければカテゴリーをクエリパラメータに追加
if (categoryIds !== undefined) query.categories = categoryIds;
const params = new URLSearchParams(query).toString();
fetch(`${sanitizedUrl}?${params}`)
.then((response) => {
if (!response.ok) throw new Error(`リクエスト失敗: ${response.status} ${response.statusText}`);
const totalPagesHeader = response.headers.get("X-WP-TotalPages");
const totalPages = totalPagesHeader ? parseInt(totalPagesHeader, 10) : 1;
target.dataset.totalPages = totalPages;
return response.json();
})
.then((posts) => {
if (page === 1) list.innerHTML = "";
const html = posts.map((post) => {
const sanitizedLink = sanitizeUrl(post.link);
const sanitizedTitle = sanitizeHtml(post.title.rendered);
return `<li><a href="${sanitizedLink}">${sanitizedTitle}</a></li>`;
}).join("");
list.insertAdjacentHTML("beforeend", html);
if (page >= target.dataset.totalPages) {
loadMoreButton.style.display = "none";
} else {
loadMoreButton.style.display = "inline-block";
}
})
.catch((error) => {
console.warn("投稿の取得エラー:", error);
});
// イベントリスナーの登録(重複登録を防止)
if (!loadMoreButton.dataset.listenerAdded) {
loadMoreButton.addEventListener("click", () => {
let currentPage = parseInt(target.dataset.currentPage || "1", 10);
currentPage++;
target.dataset.currentPage = currentPage;
// 第3引数に別途定義した getCategoryIds() 関数でカテゴリーを指定
renderPostLinks(url, target, currentPage, getCategoryIds(target));
});
loadMoreButton.dataset.listenerAdded = "true";
}
};
document.addEventListener("DOMContentLoaded", () => {
// エンドポイントの URL (http://example.com の部分は適宜変更)
const url = "http://example.com/wp-json/wp/v2/posts";
const targets = document.getElementsByClassName("fetch-posts");
Array.from(targets).forEach((target) => {
target.dataset.currentPage = "1"; // 初期ページ設定
// 第3引数に別途定義した getCategoryIds() 関数でカテゴリーを指定
renderPostLinks(url, target, 1, getCategoryIds(target));
});
});
})();
カテゴリーを選択(フィルタリング)
カテゴリーのエンドポイント(Categories)を使ってカテゴリー一覧を取得し、セレクトボックスでユーザーがカテゴリーを選択できるようにする例です。初期状態ではカテゴリー指定なしの一覧を表示します。
class が fetch-posts の div 要素があれば、そこへカテゴリーを選択するプルダウンと投稿一覧を表示し、カテゴリーを選択することができます(1ページに複数配置可能)。
<div class="fetch-posts"></div>
以下が JavaScript です。
カテゴリーのエンドポイントからカテゴリー一覧を取得して select 要素に option 要素を追加する関数 loadCategories を定義します(22-45行目)。
この関数では引数にカテゴリーのエンドポイントの URL とカテゴリー項目を表示する select 要素を受け取ります。この関数の fetch() に指定する URL には、_fields=id,name を指定してカテゴリーの id と name のみを取得し、不要なデータを取得しないようにします。そして取得したカテゴリーのデータ(id と name)を使って option 要素を作成し、select 要素の HTML に設定します。最初の option 要素の value は空文字列を設定します。
関数 renderPostLinks では、初回セレクトボックスのラッパーの div 要素と select 要素を生成する際に、loadCategories を呼び出して select 要素に option 要素を追加します(78行目)。
そして、セレクトボックスで選択されたカテゴリーの ID の値を取得してクエリパラメータに追加して投稿を取得します。初期状態または最初のオプションの All Categories が選択された場合は、セレクトボックスで選択された値は空文字なので、クエリパラメータにカテゴリーは追加されません(97-107行目)。
また、カテゴリーを変更して新しいカテゴリーで「Load More」をクリックした場合に、前のカテゴリーの続きとしてデータを取得しないように、select 要素の change イベントでカテゴリー変更時に currentPage をリセットします(150行目)。
(() => {
const sanitizeUrl = (url) => {
try {
const sanitizedUrl = new URL(url);
if ( sanitizedUrl.protocol === "http:" || sanitizedUrl.protocol === "https:" ) {
return sanitizedUrl.href;
}
} catch (e) {
console.warn("Invalid URL:", url);
}
return "";
};
const sanitizeHtml = (htmlRendered) => {
if (!htmlRendered) return "";
const elem = document.createElement("div");
elem.innerHTML = htmlRendered;
return elem.textContent || elem.innerText || "";
};
// カテゴリーを取得して select 要素に option 要素を追加する関数
const loadCategories = (catUrl, categorySelect) => {
const sanitizedUrl = sanitizeUrl(catUrl);
if (!sanitizedUrl) {
console.error("無効な API URL");
return;
}
// _fields パラメータに id と name を指定(必要なデータだけ取得)
fetch(`${sanitizedUrl}?_fields=id,name`)
.then((response) => {
if (!response.ok)
throw new Error(
`リクエスト失敗: ${response.status} ${response.statusText}`
);
return response.json();
})
.then((categories) => {
// 取得したカテゴリーのタームの id と name プロパティを使って select 要素に option 要素を追加
categorySelect.innerHTML = '<option value="">All Categories</option>' +
categories.map(cat => `<option value="${cat.id}">${sanitizeHtml(cat.name)}</option>`).join('');
})
.catch((error) => {
console.error("Failed to fetch categories:", error);
});
}
// 現在のページ番号を追加で受け取る
const renderPostLinks = (url, target, page = 1) => {
const sanitizedUrl = sanitizeUrl(url);
if (!sanitizedUrl) {
console.error("無効な API URL");
return;
}
// セレクトボックスのラッパーの div 要素
const categorySelectWrapper = target.querySelector(".category-select-wrapper");
// セレクトボックスの select 要素
let categorySelect = target.querySelector(".select-category");
// セレクトボックスのラッパーがまだ存在しなければ div 要素と select 要素、label 要素を生成
if (!categorySelectWrapper) {
const wrapper = document.createElement("div");
wrapper.classList.add('category-select-wrapper');
// select 要素を生成
categorySelect = document.createElement("select");
categorySelect.classList.add("select-category");
categorySelect.name = "select-category";
// label 要素を生成
const categorySelectLabel = document.createElement("label");
categorySelectLabel.htmlFor = "select-category"; // for 属性
categorySelectLabel.textContent = "Category: ";
// ラッパーの div 要素にセレクトボックスとラベルを追加
wrapper.appendChild(categorySelectLabel);
wrapper.appendChild(categorySelect);
// ターゲットにラッパーを追加
target.appendChild(wrapper);
// セレクトボックスを作成したタイミングで loadCategories を呼び出す(http://example.com の部分は適宜変更)
loadCategories("http://example.com/wp-json/wp/v2/categories", categorySelect);
}
// ul 要素を作成して追加 (すでに存在すればその要素を使用)
const list = target.querySelector("ul") || document.createElement("ul");
list.classList.add("post-list");
list.style.setProperty("list-style-type", "none");
if (!target.contains(list)) target.appendChild(list);
// Load More ボタンを作成して追加 (すでに存在すればその要素を使用)
const loadMoreButton = target.querySelector(".fetch-load-more") || document.createElement("button");
if (!target.contains(loadMoreButton)) {
loadMoreButton.classList.add("fetch-load-more");
loadMoreButton.textContent = "Load More";
loadMoreButton.style.display = "none";
target.appendChild(loadMoreButton);
}
// セレクトボックスで選択されたカテゴリーの値
const category = categorySelect.value;
// カテゴリー以外のクエリパラメータ
const query = {
_fields: "title,link",
page: page,
per_page: 5,
order: "desc",
};
// カテゴリーが選択されていればクエリパラメータに追加
if (category) query.categories = category;
const params = new URLSearchParams(query).toString();
fetch(`${sanitizedUrl}?${params}`)
.then((response) => {
if (!response.ok) throw new Error( `リクエスト失敗: ${response.status} ${response.statusText}`);
const totalPagesHeader = response.headers.get("X-WP-TotalPages");
const totalPages = totalPagesHeader ? parseInt(totalPagesHeader, 10) : 1;
target.dataset.totalPages = totalPages;
return response.json();
})
.then((posts) => {
// 初回はリストをクリア
if (page === 1) list.innerHTML = "";
const html = posts.map((post) => {
const sanitizedLink = sanitizeUrl(post.link);
const sanitizedTitle = sanitizeHtml(post.title.rendered);
return `<li><a href="${sanitizedLink}">${sanitizedTitle}</a></li>`;
}).join("");
list.insertAdjacentHTML("beforeend", html);
// 現在のページが最後のページ(総ページ数)未満なら表示
if (page >= target.dataset.totalPages) {
loadMoreButton.style.display = "none";
} else {
loadMoreButton.style.display = "inline-block";
}
})
.catch((error) => {
console.warn("投稿の取得エラー:", error);
});
// 既に click イベントが登録済みかをチェック(イベントリスナーの重複登録を防止)
if (!loadMoreButton.dataset.listenerAdded) {
loadMoreButton.addEventListener("click", () => {
let currentPage = parseInt(target.dataset.currentPage || "1", 10);
currentPage++;
target.dataset.currentPage = currentPage;
renderPostLinks(url, target, currentPage);
});
loadMoreButton.dataset.listenerAdded = "true";
}
// 既に change イベントが登録済みかをチェック(イベントリスナーの重複登録を防止)
if (!categorySelect.dataset.listenerAdded) {
categorySelect.addEventListener("change", () => {
target.dataset.currentPage = "1"; // カテゴリー変更時にページ番号をリセット
renderPostLinks(url, target, 1);
});
categorySelect.dataset.listenerAdded = "true";
}
};
document.addEventListener("DOMContentLoaded", () => {
// URL(http://example.com の部分は適宜変更)
const url = "http://example.com/wp-json/wp/v2/posts";
const targets = document.getElementsByClassName("fetch-posts");
Array.from(targets).forEach((target) => {
target.dataset.currentPage = "1";
renderPostLinks(url, target, 1);
});
});
})();
例えば、以下のようなカテゴリー選択のセレクトボックスが表示されます(別途 CSS を適用しています)。
アイキャッチ画像を取得
認証なしでアイキャッチ画像の URL を取得するには、以下のような方法があります。
- エンドポイントに独自のフィールドを追加する
- 独自のエンドポイントを作成する
エンドポイントに独自のフィールドを追加
以下は register_rest_field() を使って WP REST API のエンドポイントに独自のフィールドを追加して、認証不要でアイキャッチ画像の URL を取得できるようにする例です。
functions.php
functions.php(またはプラグインファイル)で以下のコードを追加します。
以下は register_rest_field() の第1引数に対象の投稿タイプ post(投稿)を、第2引数にフィールド名 featured_image_url を指定して、featured_image_url という独自のフィールドを post エンドポイントに追加し、アイキャッチ画像の URL を直接取得できるようにしています。
第3引数の値を取得する関数では get_post_thumbnail_id() で投稿のアイキャッチ画像の ID を取得し、wp_get_attachment_image_src() でアイキャッチ画像のデータ(配列)を取得しています。
wp_get_attachment_image_src() は画像の ID とイメージサイズを受け取り、画像データの配列を返します。この例ではイメージサイズに large を指定していますが、thumbnail や full なども指定できます。
画像データの配列には、画像ファイルの URL、幅、高さが順に格納されています。
// エンドポイントに独自のフィールドを追加
function custom_register_rest_fields() {
register_rest_field(
'post', // 対象の投稿タイプ(通常の投稿)
'featured_image_url', // 追加するフィールド名
[
// 値を取得する関数
'get_callback' => function ($post) {
$thumbnail_id = get_post_thumbnail_id($post['id']);
// アイキャッチ画像の ID が取得できれば
if ($thumbnail_id) {
// 画像のソースを取得(以下の場合は large サイズ)
$image = wp_get_attachment_image_src($thumbnail_id, 'large');
// $image[0] は画像ファイルの URL
return $image ? $image[0] : null;
}
return null;
},
// スキーマ(フィールドのデータ仕様)デフォルトの null を指定(省略可能)
'schema' => null,
]
);
}
add_action('rest_api_init', 'custom_register_rest_fields');
JavaScript
以下は HTML に div#fetch-posts が存在すれば、そこに各投稿のアイキャッチ画像(もしあれば)とタイトルにリンクを付けて ul と li 要素でマークアップして一覧として出力する例です。
外部から投稿を取得して表示する最初の例とほぼ同じですが、エンドポイントに独自に追加したフィールド featured_image_url を _fields パラメータに指定します(40行目)。
アイキャッチ画像の URL を post.featured_image_url で取得して変数 imageUrl に格納し(59行目)、取得できていれば(imageUrl が undefined でなければ)出力します(63行目)。
(() => {
// URL サニタイズ用の関数
const sanitizeUrl = (url) => {
try {
const sanitizedUrl = new URL(url);
if ( sanitizedUrl.protocol === "http:" || sanitizedUrl.protocol === "https:") {
return sanitizedUrl.href;
}
} catch (e) {
console.warn("Invalid URL:", url);
}
return "";
};
// HTML からテキストを取得する関数
const sanitizeHtml = (htmlRendered) => {
if (!htmlRendered) return "";
const elem = document.createElement("div");
elem.innerHTML = htmlRendered;
return elem.textContent || elem.innerText || "";
};
// 指定されたエンドポイントの URL から投稿のアイキャッチ画像とタイトル、リンクを取得して出力する関数
const renderPostLinks = (url, target) => {
const sanitizedUrl = sanitizeUrl(url);
if (!sanitizedUrl) {
console.error("無効な API URL");
return;
}
// 投稿リストを出力する ul 要素を作成し、出力先に追加
const list = document.createElement("ul");
list.setAttribute("id", "post-list");
list.style.setProperty("list-style-type", "none");
target.appendChild(list);
// URLSearchParams でオブジェクト形式のパラメータをクエリ文字列に変換
const params = new URLSearchParams({
// featured_image_url を取得できるように _fields パラメータに指定。
_fields: "title,link,featured_image_url",
per_page: 10,
order: "desc"
}).toString();
fetch(`${sanitizedUrl}?${params}`)
.then((response) => {
if (!response.ok)
throw new Error(
`リクエスト失敗: ${response.status} ${response.statusText}`
);
return response.json();
})
.then((posts) => {
list.innerHTML = posts
.map((post) => {
const sanitizedLink = sanitizeUrl(post.link);
const sanitizedTitle = sanitizeHtml(post.title?.rendered);
// アイキャッチ画像の URL を post.featured_image_url で取得
const imageUrl = post.featured_image_url ? sanitizeUrl(post.featured_image_url) : undefined;
return sanitizedLink
? `<li style="margin-bottom:20px">
<a href="${sanitizedLink}">
${imageUrl ? `<div><img src="${imageUrl}" alt="${sanitizedTitle}" style="max-width:200px;"></div>`: ""}
<div>${sanitizedTitle}</div>
</a>
</li>`
: "";
})
.join("");
})
.catch((error) => {
console.warn("投稿の取得エラー:", error);
if (list)
list.innerHTML = `<li>投稿の取得に失敗しました:${error.message}</li>`;
});
};
document.addEventListener("DOMContentLoaded", () => {
// 投稿(post)エンドポイントの URL (http://example.com の部分は適宜変更)
const url = "http://example.com/wp-json/wp/v2/posts";
// 出力先の要素
const target = document.getElementById("fetch-posts");
// 出力先の要素が存在すれば
if (target) {
// エンドポイントの URL と出力先の要素を指定して、投稿を取得して表示
renderPostLinks(url, target);
}
});
})();
複数のサイズを取得
前述の例では、1つのサイズの画像 URL を取得していますが、複数のサイズの画像 URL を同時に取得することもできます。
functions.php
functions.php(またはプラグインファイル)に記述したエンドポイントに独自のフィールドを追加する関数を以下のように書き換えます。
追加するフィールド名を featured_image_urls に変更し、full, large, medium, thumbnail の各サイズの URL を配列として返します(画像が存在しない場合は null を返すようにしています)。
function custom_register_rest_fields() {
register_rest_field('post', 'featured_image_urls', [
'get_callback' => function ($post) {
$thumbnail_id = get_post_thumbnail_id($post['id']);
if ($thumbnail_id) {
return [
'full' => wp_get_attachment_image_src($thumbnail_id, 'full')[0] ?? null,
'large' => wp_get_attachment_image_src($thumbnail_id, 'large')[0] ?? null,
'medium' => wp_get_attachment_image_src($thumbnail_id, 'medium')[0] ?? null,
'thumbnail' => wp_get_attachment_image_src($thumbnail_id, 'thumbnail')[0] ?? null,
];
}
return null;
},
'schema' => null,
]);
}
add_action('rest_api_init', 'custom_register_rest_fields');
JavaScript
JavaScript では、エンドポイントから取得した featured_image_urls を利用するように修正します。
const params = new URLSearchParams({
// 独自のフィールドを featured_image_urls に変更。
_fields: "title,link,featured_image_urls",
per_page: 10,
order: "desc"
}).toString();
PHP の配列は REST API のレスポンスの JSON ではオブジェクト形式になります(WordPress の REST API は JSON を返すため、PHPの 連想配列は JavaScript のオブジェクトとして受け取られます)。
以下では medium サイズがあればその値を取得し、なければ large サイズを取得するようにしています。
list.innerHTML = posts
.map((post) => {
const sanitizedLink = sanitizeUrl(post.link);
const sanitizedTitle = sanitizeHtml(post.title?.rendered);
// アイキャッチ画像 URL のオブジェクト
const imageUrls = post.featured_image_urls || {};
// アイキャッチ画像 URL(medium サイズを優先)
const imageUrl = imageUrls.medium || imageUrls.large || "";
return sanitizedLink
? `<li style="margin-bottom:20px">
<a href="${sanitizedLink}">
${imageUrl ? `<div><img src="${imageUrl}" alt="${sanitizedTitle}" style="max-width:200px;"></div>`: ""}
<div>${sanitizedTitle}</div>
</a>
</li>`
: "";
})
.join("");
コード全体は以下のようになります。
(() => {
const sanitizeUrl = (url) => {
try {
const sanitizedUrl = new URL(url);
if ( sanitizedUrl.protocol === "http:" || sanitizedUrl.protocol === "https:") {
return sanitizedUrl.href;
}
} catch (e) {
console.warn("Invalid URL:", url);
}
return "";
};
const sanitizeHtml = (htmlRendered) => {
if (!htmlRendered) return "";
const elem = document.createElement("div");
elem.innerHTML = htmlRendered;
return elem.textContent || elem.innerText || "";
};
const renderPostLinks = (url, target) => {
const sanitizedUrl = sanitizeUrl(url);
if (!sanitizedUrl) {
console.error("無効な API URL");
return;
}
const list = document.createElement("ul");
list.setAttribute("id", "post-list");
list.style.setProperty("list-style-type", "none");
target.appendChild(list);
const params = new URLSearchParams({
// 独自のフィールドを featured_image_urls に変更。
_fields: "title,link,featured_image_urls",
per_page: 10,
order: "desc"
}).toString();
fetch(`${sanitizedUrl}?${params}`)
.then((response) => {
if (!response.ok)
throw new Error(
`リクエスト失敗: ${response.status} ${response.statusText}`
);
return response.json();
})
.then((posts) => {
list.innerHTML = posts
.map((post) => {
const sanitizedLink = sanitizeUrl(post.link);
const sanitizedTitle = sanitizeHtml(post.title?.rendered);
// アイキャッチ画像 URL のオブジェクト
const imageUrls = post.featured_image_urls || {};
// アイキャッチ画像 URL(medium サイズを優先)
const imageUrl = imageUrls.medium || imageUrls.large || "";
return sanitizedLink
? `<li style="margin-bottom:20px">
<a href="${sanitizedLink}">
${imageUrl ? `<div><img src="${imageUrl}" alt="${sanitizedTitle}" style="max-width:200px;"></div>`: ""}
<div>${sanitizedTitle}</div>
</a>
</li>`
: "";
})
.join("");
})
.catch((error) => {
console.warn("投稿の取得エラー:", error);
if (list)
list.innerHTML = `<li>投稿の取得に失敗しました:${error.message}</li>`;
});
};
document.addEventListener("DOMContentLoaded", () => {
// 投稿(post)エンドポイントの URL (http://example.com の部分は適宜変更)
const url = "http://example.com/wp-json/wp/v2/posts";
const target = document.getElementById("fetch-posts");
if (target) {
renderPostLinks(url, target);
}
});
})();
独自のエンドポイントを作成
WordPress の REST API に独自のエンドポイントを作成するには、register_rest_route() 関数を使用します。この関数を使うことで、独自の API エンドポイントを追加し、GET や POST などのリクエストに対応できます。Adding Custom Endpoints
register_rest_route() の書式と引数
引数 | 説明 |
---|---|
$namespace | エンドポイントの名前空間(例: myplugin/v1) |
$route | ルートの URL パス(例: /data → /wp-json/myplugin/v1/data) |
$args | エンドポイントの詳細設定(HTTP メソッド、コールバック関数など。以下参照) |
プロパティ | 説明 |
---|---|
methods | 許可する HTTP メソッド('GET', 'POST', 'PUT', 'DELETE' など) |
callback | リクエストを処理するコールバック関数 |
permission_callback | リクエストの権限をチェックするコールバック |
args | エンドポイントで受け取るパラメータの設定 |
実装例
以下のコードは myplugin/v1/data というエンドポイントを作成し、JSON データを返します。
// register_rest_route() を使って API ルートを作成
function myplugin_register_rest_routes() {
register_rest_route('myplugin/v1', '/data', [
'methods' => 'GET',
'callback' => 'myplugin_get_data',
'permission_callback' => '__return_true', // 認証なしでアクセス可能にする
]);
}
// rest_api_init アクションフックを使用してエンドポイントを登録
add_action('rest_api_init', 'myplugin_register_rest_routes');
// リクエストを処理するコールバック関数
function myplugin_get_data() {
$data = array('message' => 'Hello, REST API!');
return rest_ensure_response($data);
// または return new WP_REST_Response($data, 200); // 200 は HTTP ステータスコード(省略可能)
}
上記のコードを functions.php に追加して、https://xxxxx/wp-json/myplugin/v1/data に GET リクエストを送ると、{ "message": "Hello, REST API!" } が返されます。
コールバック関数の戻り値
コールバックが呼び出されると、戻り値は JSON に変換され、クライアントに返されます。
WordPress の REST API では、カスタムエンドポイントのコールバック関数が配列やオブジェクトを返す場合、自動的に WP_REST_Response にラップして処理してくれます。
前述の実装例の場合、単に return $data でも機能しますが、rest_ensure_response() を使えば、関数の戻り値を適切な WP_REST_Response オブジェクトに変換してくれるので、$data は 自動的に WP_REST_Response に変換され、REST API のレスポンスとして適切に処理されます。
但し、レスポンスのステータスコードを手動で指定したい場合やカスタムヘッダー(キャッシュ制御など)を追加したい場合は new WP_REST_Response() を使います。
$response = new WP_REST_Response($posts, 201); // 201 Created
$response->header('Cache-Control', 'no-cache');
return $response;
permission_callback
register_rest_route() を使って REST API エンドポイントを登録する際、permission_callback を指定しない(省略した)場合、デフォルトで「公開 API(認証不要)」として扱われます。
但し、デバッグモードで、permission_callback を指定しない場合、投稿の管理画面ソースコードに「Notice:関数 register_rest_route が誤って呼び出されました。(中略)public REST API ルートに対してはパーミッションコールバックとして __return_true を使用してください。」という注意が出力されます。
register_rest_route() Changelog Version 5.5.0 には「必須の permission_callback 引数が設定されていない場合に _doing_it_wrong() 通知を追加」とあります。
そのため、認証なしでアクセス可能にする(公開 API とする)場合は、明示的に permission_callback に __return_true を指定したほうが良さそうです。
また、permission_callback を省略すると、未ログイン含む全ユーザーにデータを公開することになるので、認証が必要な API の場合は permission_callback でリクエストの権限をチェックする必要があります。例えば、認証が必要なエンドポイントなら以下のように指定することができます。
'permission_callback' => function() {
return current_user_can('edit_posts'); // 投稿編集権限があるユーザーのみ
}
アイキャッチ画像を取得
独自のエンドポイントを設定すれば、認証なしでもアイキャッチ画像のデータを取得できます。
以下はカスタム REST API エンドポイント(custom/v1/posts)を作成し、必要なデータ(タイトル・リンク・アイキャッチ画像)を含めて公開する例です。
functions.php またはプラグインファイルに以下のコードを追加します。
// 投稿データ(アイキャッチ画像付き)を取得する関数
function custom_get_posts_with_featured_image() {
// WP_Query 用のクエリ引数を定義
$args = array(
'post_type' => 'post', // 投稿タイプ(通常の投稿)
'posts_per_page' => 10, // 取得する投稿数(最大10件)
'orderby' => 'date', // 投稿日順にソート(デフォルトなので省略可能)
'order' => 'DESC', // 新しい投稿から順に取得(デフォルトなので省略可能)
'post_status' => 'publish', // 公開済みの投稿のみ取得(デフォルトなので省略可能)
'no_found_rows' => true, // ページネーション情報を無視して高速化(ページ数や総件数が不要な場合に有効)
'ignore_sticky_posts' => true, // スティッキーポスト(先頭に固定表示された投稿)を無視(必要に応じて)
);
// WP_Query インスタンスを作成し、投稿を取得
$query = new WP_Query($args);
$posts_data = array(); // 投稿データを格納する配列
// 投稿が存在する場合の処理
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post(); // 現在の投稿データをセット
// アイキャッチ画像のURLを取得(サイズは 'medium')
$featured_image = get_the_post_thumbnail_url(get_the_ID(), 'medium');
// 取得した投稿データをエスケープして配列に追加
$posts_data[] = array(
'title' => esc_html(wp_strip_all_tags(get_the_title())), // 投稿タイトルから HTML を除去してエスケープ
'link' => esc_url(get_permalink()), // 投稿URLをエスケープ
'featured_image' => $featured_image ? esc_url($featured_image) : '', // アイキャッチ画像の URL をエスケープ(なければ空)
);
}
wp_reset_postdata(); // グローバルな投稿データをリセット
}
// REST API レスポンスとしてデータを返す
return rest_ensure_response($posts_data);
}
// カスタム REST API エンドポイントを登録する関数
function register_custom_rest_route() {
register_rest_route('custom/v1', '/posts/', array(
'methods' => 'GET', // HTTPメソッド(GETリクエスト)
'callback' => 'custom_get_posts_with_featured_image', // 呼び出す関数
'permission_callback' => '__return_true', // 認証なしでアクセス可能にする(明示的に指定)
));
}
// REST API が初期化されるタイミングでカスタムエンドポイントを登録
add_action('rest_api_init', 'register_custom_rest_route');
JavaScript
以下は HTML に div#fetch-posts が存在すれば、そこに各投稿のアイキャッチ画像(もしあれば)とタイトルにリンクを付けて ul と li 要素でマークアップして一覧として出力する例です。
エンドポイントに独自のフィールドを追加する例の JavaScript とほぼ同じですが、 _fields パラメータに指定するアイキャッチ画像のフィールド名や独自のエンドポイントを指定する部分が異なっています。
この例の場合、独自のエンドポイント custom/v1/posts で返すフィールドは title, link, featured_image の3つだけなので、_fields を省略しても同じです。
クエリパラメータの per_page や order はエンドポイント側で固定なので指定しても効果はありません。
また、独自のエンドポイントでは esc_html() や esc_url() を使ってエスケープ処理した値を返すので、JavaScript 側で再度エスケープ処理を行う必要は基本的にありませんが、innerHTML を使っているため、保守性と将来のリスク対策として JS 側でも sanitizeHtml() を使って再エスケープしています。
(() => {
// HTML からテキストを取得する関数
const sanitizeHtml = (htmlRendered) => {
if (!htmlRendered) return "";
const elem = document.createElement("div");
elem.innerHTML = htmlRendered;
return elem.textContent || elem.innerText || "";
};
// 指定されたエンドポイントの URL から投稿のアイキャッチ画像とタイトル、リンクを取得して出力する関数
const renderPostLinks = (url, target) => {
if (!url) {
console.error("無効な API URL");
return;
}
const list = document.createElement("ul");
list.setAttribute("id", "post-list");
list.style.setProperty("list-style-type", "none");
target.appendChild(list);
const params = new URLSearchParams({
// title, link, featured_image を取得できるように _fields パラメータに指定。
_fields: "title,link,featured_image",
//per_page: 10, // 指定しても意味がない
//order: "desc" // 指定しても意味がない
}).toString();
fetch(`${url}?${params}`)
.then((response) => {
if (!response.ok)
throw new Error(
`リクエスト失敗: ${response.status} ${response.statusText}`
);
return response.json();
})
.then((posts) => {
list.innerHTML = posts
.map((post) => {
// リンク URL は post.link で取得(エスケープ済みのデータをそのまま使用)
const sanitizedLink = post.link;
// タイトルは post.title で取得(エスケープ済みだが念の為サニタイズ)
const sanitizedTitle = sanitizeHtml(post.title);
// アイキャッチ画像の URL を post.featured_image で取得(エスケープ済みのデータをそのまま使用)
const imageUrl = post.featured_image || undefined;
return sanitizedLink
? `<li style="margin-bottom:20px">
<a href="${sanitizedLink}">
${imageUrl ? `<div><img src="${imageUrl}" alt="${sanitizedTitle}" style="max-width:200px;"></div>`: ""}
<h3>${sanitizedTitle}</h3>
</a>
</li>`
: "";
})
.join("");
})
.catch((error) => {
console.warn("投稿の取得エラー:", error);
if (list)
list.innerHTML = `<li>投稿の取得に失敗しました:${error.message}</li>`;
});
};
document.addEventListener("DOMContentLoaded", () => {
// 独自のエンドポイントを指定 (http://example.com の部分は適宜変更)
const url = "http://example.com/wp-json/custom/v1/posts";
const target = document.getElementById("fetch-posts");
if (target) {
renderPostLinks(url, target);
}
});
})();
クエリパラメータを受け取る
以下は投稿の取得件数(per_page)をサポートするカスタム REST API エンドポイントを作成する例です。先の例と同じエンドポイント(custom/v1/posts)を使用するので、functions.php(またはプラグインファイル)に記述した先のコードは削除して以下に書き換えます。
REST API のコールバック関数 custom_get_posts_with_pagination() では、引数として $request を受け取ります。WP_REST_Request は WordPress REST API のリクエストデータを扱うクラスです。
$request オブジェクトを使うことで、$request->get_param('key') のようにクエリパラメータの値を取得したり、HTTP ヘッダーを取得したりすることができます。
以下では、$request->get_param('per_page') でリクエストから per_page パラメータを取得し、isset() で存在チェックし、intval() で整数に変換し、指定がない場合は10をデフォルトにしています。
そしてカスタム REST エンドポイントの登録の args(エンドポイントで受け取るパラメータの設定)の validate_callback で正の整数のみ許可するようにしています。
また、以下ではアイキャッチ画像の各サイズの URL と抜粋を取得して公開しています。
// カスタム REST API のコールバック関数:指定された件数の投稿を取得し、JSON 形式で返す
function custom_get_posts_with_pagination(WP_REST_Request $request) {
// リクエストから per_page パラメータを取得(指定がない場合は10をデフォルトに)
$per_page = $request->get_param('per_page');
$per_page = isset($per_page) ? intval($per_page) : 10;
// 投稿取得のためのクエリ引数を設定
$args = array(
'post_type' => 'post', // 通常の投稿タイプ
'posts_per_page' => $per_page, // 取得件数(リクエストに応じて変動)
'no_found_rows' => true, // ページネーション情報を無視して高速化
'ignore_sticky_posts' => true, // スティッキーポストを無視(必要に応じて)
);
$query = new WP_Query($args); // 投稿クエリを実行
$posts_data = array(); // レスポンス用の配列
// 投稿が存在する場合はループでデータを整形
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$posts_data[] = array(
// タイトルから HTML タグを除去し、エスケープ
'title' => esc_html(wp_strip_all_tags(get_the_title())),
// 抜粋(excerpt)を取得し、HTML タグを除去してエスケープ
'excerpt' => esc_html(wp_strip_all_tags(get_the_excerpt())),
// 投稿のパーマリンクを取得・エスケープ
'link' => esc_url(get_permalink()),
// アイキャッチ画像の各サイズの URL を取得・エスケープ
'featured_image_full' => esc_url(get_the_post_thumbnail_url(get_the_ID(), 'full')),
'featured_image_large' => esc_url(get_the_post_thumbnail_url(get_the_ID(), 'large')),
'featured_image_medium' => esc_url(get_the_post_thumbnail_url(get_the_ID(), 'medium')),
'featured_image_thumbnail' => esc_url(get_the_post_thumbnail_url(get_the_ID(), 'thumbnail')),
);
}
wp_reset_postdata(); // グローバルな投稿データをリセット
}
// REST API レスポンスとして返却(自動で JSON 形式に変換される)
return rest_ensure_response($posts_data);
}
// カスタム REST エンドポイントの登録
function register_custom_posts_endpoint() {
register_rest_route('custom/v1', '/posts/', array(
'methods' => WP_REST_Server::READABLE, // GET / HEAD リクエストを許可
'callback' => 'custom_get_posts_with_pagination', // コールバック関数を指定
'permission_callback' => '__return_true', // 認証なしでアクセス可能にする
'args' => array(
// per_page パラメータのバリデーションと説明
'per_page' => array(
'description' => '取得する投稿数(最大20件)',
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 20,
// バリデーション:正の整数のみ許可
'validate_callback' => function ($param) {
return is_numeric($param) && intval($param) > 0;
}
),
),
));
}
// REST API 初期化時にエンドポイントを登録
add_action('rest_api_init', 'register_custom_posts_endpoint');
上記の register_rest_route() の methods には GET ではなく WP_REST_Server::READABLE を指定しています。GET も WP_REST_Server::READABLE もどちらも GET リクエストを許可 しますが、WP_REST_Server::READABLE を使うと GET および HEAD を許可(サポート)します。この例の場合、GET を指定しても同じです。
JavaScript
JavaScript を以下のように書き換えます。
以下ではアイキャッチ画像は large サイズを取得して表示するように _fields に featured_image_large を指定しています。この例では抜粋は使用しないので指定していません。
取得件数を指定できるので、per_page: 5 を指定して投稿のデータを5件取得するようにしています。
(() => {
// HTML からテキストを取得する関数
const sanitizeHtml = (htmlRendered) => {
if (!htmlRendered) return "";
const elem = document.createElement("div");
elem.innerHTML = htmlRendered;
return elem.textContent || elem.innerText || "";
};
// 指定されたエンドポイントの URL から投稿のアイキャッチ画像とタイトル、リンクを取得して出力する関数
const renderPostLinks = (url, target) => {
if (!url) {
console.error("無効な API URL");
return;
}
const list = document.createElement("ul");
list.setAttribute("id", "post-list");
list.style.setProperty("list-style-type", "none");
target.appendChild(list);
const params = new URLSearchParams({
// 画像 URL は featured_image_large を指定。
_fields: "title,link,featured_image_large",
// 取得件数を指定
per_page: 5,
}).toString();
fetch(`${url}?${params}`)
.then((response) => {
if (!response.ok)
throw new Error(
`リクエスト失敗: ${response.status} ${response.statusText}`
);
return response.json();
})
.then((posts) => {
list.innerHTML = posts
.map((post) => {
const sanitizedLink = post.link;
const sanitizedTitle = sanitizeHtml(post.title);
// アイキャッチ画像の URL を post.featured_image_large で取得
const imageUrl = post.featured_image_large || undefined;
return sanitizedLink
? `<li style="margin-bottom:20px">
<a href="${sanitizedLink}">
${imageUrl ? `<div><img src="${imageUrl}" alt="${sanitizedTitle}" style="max-width:200px;"></div>`: ""}
<h3>${sanitizedTitle}</h3>
</a>
</li>`
: "";
})
.join("");
})
.catch((error) => {
console.warn("投稿の取得エラー:", error);
if (list)
list.innerHTML = `<li>投稿の取得に失敗しました:${error.message}</li>`;
});
};
document.addEventListener("DOMContentLoaded", () => {
// 独自のエンドポイントを指定 (http://example.com の部分は適宜変更)
const url = "http://example.com/wp-json/custom/v1/posts";
const target = document.getElementById("fetch-posts");
if (target) {
renderPostLinks(url, target);
}
});
})();
パラメータに画像サイズを指定
前述の例のエンドポイントでは、アイキャッチ画像の各サイズの URL を取得して返していますが、リクエストパラメータ image_size を追加し、指定されたサイズのアイキャッチ画像のみを返すようにする例です。
対応するサイズは full, large, medium, thumbnail として、それ以外が指定された場合は medium をデフォルトにします。これにより、余計なデータは返さないためレスポンスがコンパクトになります。
functions.php(またはプラグインファイル)に記述したコードを以下に書き換えます。
$request->get_param('image_size') で image_size パラメータを取得し、full, large, medium, thumbnail のみ許可するように image_size のバリデーションを追加します。
また、register_rest_route() の validate_callback でも同様にバリデーションを行います。
基本的には register_rest_route() の validate_callback でバリデーションを行えば良いのですが、custom_get_posts_with_pagination でも isset() や in_array() で デフォルト値をセットし、安全なデータを使用する補助的なバリデーションを行うことで、より安全で予期しないエラーを防ぐことができます。
// カスタム REST API のコールバック関数:指定された件数の投稿を取得し、JSON 形式で返す
function custom_get_posts_with_pagination(WP_REST_Request $request) {
// per_page パラメータを取得(指定がない場合は10)
$per_page = $request->get_param('per_page');
$per_page = isset($per_page) ? intval($per_page) : 10;
// image_size パラメータを取得(指定がない場合は 'medium')
$image_size = $request->get_param('image_size');
$allowed_sizes = array('full', 'large', 'medium', 'thumbnail');
if (!in_array($image_size, $allowed_sizes, true)) {
$image_size = 'medium'; // デフォルトサイズ
}
// 投稿取得のためのクエリ引数を設定
$args = array(
'post_type' => 'post', // 通常の投稿タイプ
'posts_per_page' => $per_page, // 取得件数(リクエストに応じて変動)
'post_status' => 'publish', // 公開済み投稿のみ
'no_found_rows' => true, // ページネーション情報を無視して高速化
'ignore_sticky_posts' => true, // スティッキーポストを無視(必要に応じて)
);
$query = new WP_Query($args); // 投稿クエリを実行
$posts_data = array(); // レスポンス用の配列
// 投稿が存在する場合はループでデータを整形
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$posts_data[] = array(
// タイトルから HTML タグを除去し、エスケープ
'title' => esc_html(wp_strip_all_tags(get_the_title())),
// 抜粋(excerpt)を取得し、HTML タグを除去してエスケープ
'excerpt' => esc_html(wp_strip_all_tags(get_the_excerpt())),
// 投稿のパーマリンクを取得・エスケープ
'link' => esc_url(get_permalink()),
// 指定されたサイズのアイキャッチ画像 URL を取得・エスケープ
'featured_image' => esc_url(get_the_post_thumbnail_url(get_the_ID(), $image_size)),
);
}
wp_reset_postdata(); // グローバルな投稿データをリセット
}
// REST API レスポンスとして返却(自動で JSON 形式に変換される)
return rest_ensure_response($posts_data);
}
// カスタム REST エンドポイントの登録
function register_custom_posts_endpoint() {
register_rest_route('custom/v1', '/posts/', array(
'methods' => WP_REST_Server::READABLE, // GET / HEAD リクエストを許可
'callback' => 'custom_get_posts_with_pagination', // コールバック関数を指定
'permission_callback' => '__return_true', // 認証なしでアクセス可能にする
'args' => array(
// per_page パラメータのバリデーションと説明
'per_page' => array(
'description' => '取得する投稿数(最大20件)',
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 20,
// バリデーション:正の整数のみ許可
'validate_callback' => function ($param) {
return is_numeric($param) && intval($param) > 0;
}
),
// image_size パラメータのバリデーションと説明
'image_size' => array(
'description' => '取得するアイキャッチ画像のサイズ(full, large, medium, thumbnail)',
'type' => 'string',
'default' => 'medium',
'validate_callback' => function ($param) {
return in_array($param, array('full', 'large', 'medium', 'thumbnail'), true);
}
),
),
));
}
// REST API 初期化時にエンドポイントを登録
add_action('rest_api_init', 'register_custom_posts_endpoint');
JavaScript
JavaScript を以下のように書き換えます。
以下ではクエリパラメータ image_size に large を指定して large サイズの画像 URL を取得しています。クエリパラメータ image_size を省略するとデフォルトの medium サイズの画像 URL が適用されます。
また、この例では抜粋(excerpt)も取得して表示しています。カスタムエンドポイントが提供する全てのフィールドを取得するので、この場合、_fields は指定しなくても(省略しても)同じです。
(() => {
// HTML からテキストを取得する関数
const sanitizeHtml = (htmlRendered) => {
if (!htmlRendered) return "";
const elem = document.createElement("div");
elem.innerHTML = htmlRendered;
return elem.textContent || elem.innerText || "";
};
// 指定されたエンドポイントの URL から投稿のアイキャッチ画像とタイトル、リンクを取得して出力する関数
const renderPostLinks = (url, target) => {
if (!url) {
console.error("無効な API URL");
return;
}
const list = document.createElement("ul");
list.setAttribute("id", "post-list");
list.style.setProperty("list-style-type", "none");
target.appendChild(list);
const params = new URLSearchParams({
// 画像 URL は featured_image を指定。excerpt を追加(全てのフィールドを取得するので以下は省略可能)
_fields: "title,link,featured_image,excerpt",
// 取得件数を指定
per_page: 5,
// 取得する画像サイズを指定(省略するとデフォルトの medium)
image_size: 'large',
}).toString();
fetch(`${url}?${params}`)
.then((response) => {
if (!response.ok)
throw new Error(
`リクエスト失敗: ${response.status} ${response.statusText}`
);
return response.json();
})
.then((posts) => {
list.innerHTML = posts
.map((post) => {
const sanitizedLink = post.link;
const sanitizedTitle = sanitizeHtml(post.title);
const sanitizedExcerpt = sanitizeHtml(post.excerpt);
// アイキャッチ画像の URL を post.featured_image で取得
const imageUrl = post.featured_image || undefined;
return sanitizedLink
? `<li style="margin-bottom:20px">
<a href="${sanitizedLink}">
${imageUrl ? `<div><img src="${imageUrl}" alt="${sanitizedTitle}" style="max-width:200px;"></div>`: ""}
<h3>${sanitizedTitle}</h3>
</a>
<div>${sanitizedExcerpt}</div>
</li>`
: "";
})
.join("");
})
.catch((error) => {
console.warn("投稿の取得エラー:", error);
if (list)
list.innerHTML = `<li>投稿の取得に失敗しました:${error.message}</li>`;
});
};
document.addEventListener("DOMContentLoaded", () => {
// 独自のエンドポイントを指定 (http://example.com の部分は適宜変更)
const url = "http://example.com/wp-json/custom/v1/posts";
const target = document.getElementById("fetch-posts");
if (target) {
renderPostLinks(url, target);
}
});
})();
ブラウザでカスタムエンドポイントを確認
ブラウザで設定したカスタムエンドポイントの名前空間(custom/v1)にアクセスして内容を確認することができます。