[WordPress] REST APIで静的サイトに更新可能なコンテンツを作成する

クライアントが更新できるようにして〜という雑な要望に応えるべく、我々は都度立ち上がってきた。
これまで様々なツールを用いてきたが「とりあえずWordpress使っとけばよくね?」という結論に至って幾星霜。
長い月日のなかで思い知らされたのはWordPressテーマのフルカスタムは面倒臭いという現実であった。

救世主REST API降臨

Version4.7くらいだったろうか、それまでプラグインだったREST APIが標準搭載になったのは。
これで「Wordpressでコンテンツを更新できるサイトを作るならカスタムテーマを作成しなければならない」という呪縛を断ち切れる、と人々は喜んだ。

さて実際にREST APIを利用して更新できるコンテンツを作ってみようとした時に、一体何をすればいいのか?
この記事は静的に作られたサイトで更新が必要な部分だけをWordpressに依存させるべく、ページを作るのに必要なJSONをREST APIで出力するまでの手順についてまとめたものです。

要件

  1. デフォルトの投稿をサイトのお知らせとして使う。(post_type=post
    →Newsとしてサイトで表示
  2. カスタム投稿タイプで動画を追加する。(post_type=movie
    →Movieとしてサイトで表示

WordPressテーマの新設

※MAMPやVCCWなどローカル環境のWordpressで試してください。

wordpressのインストールフォルダにあるwp-content/thems以下に適当な名前のフォルダを作成する。
そこに中身が空のindex.phpと、定型のコメントを記入したstyle.cssを投入すればテーマとして有効化できるようになります。

以下がテーマフォルダに必須のstyle.css冒頭に入れるコメント。

/*
Theme Name: Rest API Sample
Author: tenderfeel@gmail.com
Author URI: https://webtecnote.com/
Description: 静的サイトに更新コンテンツを仕込むテスト
Version: 1.0
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html

This theme, like WordPress, is licensed under the GPL.
Use it to make something cool, have fun, and share what you've learned with others.
*/

プレビューがなくて寂しかったらscreenshot.pngを追加する。

テーマの機能設定

テーマフォルダにfunctions.phpを作成する。以降のソースはこのファイル内に書きます。

add_action( 'after_setup_theme', function() {
  // 投稿サムネイルサポート
  add_theme_support( 'post-thumbnails' );

  // 画像サイズ設定
  update_option( 'thumbnail_size_w', 520 );
  update_option( 'thumbnail_size_h', 520 );

  update_option( 'medium_size_w', 820 );
  update_option( 'medium_size_h', 820 );

  update_option( 'large_size_w', 1640 );
  update_option( 'large_size_h', 1640 );
} );

投稿をNewsに見せかける編集

機能はそのままに、管理画面上の見た目だけ変える。

管理画面メニューのラベル変更

add_action('admin_menu', function() {
  global $menu;

  $menu[5][0] = 'News';
});

投稿画面のタイトル変更

add_action( 'init', function () {
  global $wp_post_types;
  $labels = &$wp_post_types['post']->labels;
  $labels->name = 'News';
  $labels->singular_name = 'News';
  $labels->edit_item = 'Newsの編集';
});

カスタム投稿タイプMovieの追加

WordPressの記事分類は投稿タイプ(post_type)で無限に増やすことができます。
デフォルトの投稿タイプはpostなので、ラベルを変えただけのNewsはpost_type=postということになります。
動画についての投稿はNewsと別にしたいのでpost_type=movieを追加します。
カテゴリーも専用に用意するためregister_taxonomymovie-categoryを設定。

各設定についての詳細はregister_post_typeのCodexをみてください。

add_action( 'init', function () {
  register_post_type( 'movie', array(
    'labels'             => array(
      'name'               => _x( 'Movie', 'post type general name', 'rest' ),
      'singular_name'      => _x( 'Movie', 'post type singular name', 'rest' ),
      'menu_name'          => _x( 'Movie', 'admin menu', 'rest' ),
      'name_admin_bar'     => _x( 'Movie', 'add new on admin bar', 'rest' ),
      'add_new'            => _x( '新規追加', 'rest' ),
      'add_new_item'       => __( '新規追加', 'rest' ),
      'new_item'           => __( '新規', 'rest' ),
      'edit_item'          => __( '編集', 'rest' ),
      'view_item'          => __( '表示', 'rest' ),
      'all_items'          => __( '動画一覧', 'rest' ),
      'search_items'       => __( '動画を探す', 'rest' ),
      'parent_item_colon'  => __( '親の動画:', 'rest' ),
      'not_found'          => __( '動画が見つかりませんでした', 'rest' ),
      'not_found_in_trash' => __( '動画がゴミ箱でも見つかりませんでした', 'rest' )
    ),
    'public'             => true,
    'publicly_queryable' => true,
    'show_ui'            => true,
    'show_in_menu'       => true,
    'show_in_rest'       => true,
    'query_var'          => true,
    'rewrite'            => array( 'slug' => 'movie' ),
    'capability_type'    => 'post',
    'has_archive'        => true,
    'hierarchical'       => false,
    'menu_position'      => 5,
    'menu_icon'          => 'dashicons-video-alt3',
    'rest_base'          => 'movies',
    'rest_controller_class' => 'WP_REST_Posts_Controller',
    'supports'           => array( 'title', 'editor', 'thumbnail' )
  ) );

  // 動画用カテゴリー
  register_taxonomy( 'movie-category', array('movie'), array(
     'hierarchical'          => true,
     'labels'                => array(
      'name'                       => _x( '動画カテゴリー', 'taxonomy general name' ),
      'singular_name'              => _x( '動画カテゴリー', 'taxonomy singular name' ),
      'search_items'               => __( 'カテゴリーを検索' ),
      'popular_items'              => __( '人気のカテゴリー' ),
      'all_items'                  => __( '全てのカテゴリー' ),
      'parent_item'                => null,
      'parent_item_colon'          => null,
      'edit_item'                  => __( 'カテゴリーを編集' ),
      'update_item'                => __( 'カテゴリーを更新' ),
      'add_new_item'               => __( 'カテゴリーを追加' ),
      'new_item_name'              => __( '新規カテゴリー名' ),
      'separate_items_with_commas' => __( 'カテゴリーが複数ある場合はコンマで区切ってください' ),
      'add_or_remove_items'        => __( 'カテゴリーを追加または削除' ),
      'choose_from_most_used'      => __( '既存のカテゴリーから選ぶ' ),
      'not_found'                  => __( 'カテゴリーが見つかりませんでした' ),
      'menu_name'                  => __( '動画カテゴリー' ),
    ),
     'show_ui'               => true,
     'show_admin_column'     => true,
     'update_count_callback' => '_update_post_term_count',
     'query_var'             => true,
     'rewrite'               => array( 'slug' => 'movie-category' ),
  ) );
});

動画投稿フィールドの設定

カスタムフィールドでYouTubeのURLを設定するように仕込むこととします。
Advanced Custom Fields プラグインをインストール、有効化したのち、カスタムフィールド設定画面でフィールドグループMovieを追加します。

  • 表示条件
    投稿タイプ・等しい・Movie
  • スタイル
    シームレス
  • 位置
    高(タイトルの下)
  • 画面に非表示
    投稿サムネイル以外全部チェック

フィールドはフィールド名をyoutube、フィールドタイプをoEmbedにして設定する。

そうすると投稿時UIがこのようになります。

…が、ブロックエディタだと本文追加が普通にできてしまうので注意。

REST APIの有効化

最新バージョンだと標準搭載&実行状態なので特にやることはないです。
Newsに適当な記事を数件追加してから、別タブで /wp-json/wp/v2/posts にアクセスしてみてください。
JSONが表示されたらおk。

JSONが返ってこなかった場合、パーマリンク設定を確認する。
パーマリンク設定がデフォルトになっているとmod_rewriteが書き換わってないので/wp-json/は使えません。
変更できるならデフォルト以外に変更、変更できない場合はURLを?rest_route=/wp/v2/postsにします。

REST APIが返すJSONの構造

タイトルや本文などはオブジェクトで、renderedというキーに設定されてます。

以下は記事1件分の抜粋:

{  
      "id":13,
      "date":"2018-10-18T16:31:55",
      "date_gmt":"2018-10-18T07:31:55",
      "guid":{  
         "rendered":"http:\/\/localhost:8888\/?p=13"
      },
      "modified":"2018-12-13T19:29:38",
      "modified_gmt":"2018-12-13T10:29:38",
      "slug":"%e3%82%a4%e3%83%99%e3%83%b3%e3%83%88%e3%81%ae%e3%81%8a%e7%9f%a5%e3%82%89%e3%81%9b",
      "status":"publish",
      "type":"post",
      "link":"http:\/\/localhost:8888\/archives\/13",
      "title":{  
         "rendered":"\u30a4\u30d9\u30f3\u30c8\u306e\u304a\u77e5\u3089\u305b"
      },
      "content":{  
         "rendered":"<p><img class=\"alignnone size-full wp-image-11\" src=\"http:\/\/localhost:8888\/wp-content\/uploads\/2018\/10\/DXb0tpVVMAAEo-i.jpg\" alt=\"\" width=\"624\" height=\"415\" \/><\/p>\n<p>\u30cd\u30b3\u3068\u548c\u8ae7\u305b\u3088<\/p>\n",
         "protected":false
      },
      "excerpt":{  
         "rendered":"<p>\u30cd\u30b3\u3068\u548c\u8ae7\u305b\u3088<\/p>\n",
         "protected":false
      },
      "author":1,
      "featured_media":0,
      "comment_status":"open",
      "ping_status":"open",
      "sticky":false,
      "template":"",
      "format":"standard",
      "meta":[  

      ],
      "categories":[  
         6
      ],
      "tags":[  

      ],
      "_links":{  
         "self":[  
            {  
               "href":"http:\/\/localhost:8888\/wp-json\/wp\/v2\/posts\/13"
            }
         ],
         "collection":[  
            {  
               "href":"http:\/\/localhost:8888\/wp-json\/wp\/v2\/posts"
            }
         ],
         "about":[  
            {  
               "href":"http:\/\/localhost:8888\/wp-json\/wp\/v2\/types\/post"
            }
         ],
         "author":[  
            {  
               "embeddable":true,
               "href":"http:\/\/localhost:8888\/wp-json\/wp\/v2\/users\/1"
            }
         ],
         "replies":[  
            {  
               "embeddable":true,
               "href":"http:\/\/localhost:8888\/wp-json\/wp\/v2\/comments?post=13"
            }
         ],
         "version-history":[  
            {  
               "count":1,
               "href":"http:\/\/localhost:8888\/wp-json\/wp\/v2\/posts\/13\/revisions"
            }
         ],
         "predecessor-version":[  
            {  
               "id":14,
               "href":"http:\/\/localhost:8888\/wp-json\/wp\/v2\/posts\/13\/revisions\/14"
            }
         ],
         "wp:attachment":[  
            {  
               "href":"http:\/\/localhost:8888\/wp-json\/wp\/v2\/media?parent=13"
            }
         ],
         "wp:term":[  
            {  
               "taxonomy":"category",
               "embeddable":true,
               "href":"http:\/\/localhost:8888\/wp-json\/wp\/v2\/categories?post=13"
            },
            {  
               "taxonomy":"post_tag",
               "embeddable":true,
               "href":"http:\/\/localhost:8888\/wp-json\/wp\/v2\/tags?post=13"
            }
         ],
         "curies":[  
            {  
               "name":"wp",
               "href":"https:\/\/api.w.org\/{rel}",
               "templated":true
            }
         ]
      }
   }

REST APIのレスポンスにカスタムフィールドの値を含める

カスタム投稿タイプのMovieは/wp-json/wp/v2/moviesでJSONが表示されるけど、
そのままだとカスタムフィールドyoutubeで設定したYouTubeのURLは出ないためregister_rest_fieldで追加する。

/**
 * "youtube"フィールドをMovieのRST APIレスポンスに追加
 */
add_action( 'rest_api_init', function () {
  register_rest_field(
    'movie',
    'youtube',
    array(
        'get_callback'    => 'get_field',
        'schema'          => null,
    )
  );
});

/**
 * カスタムフィールドのデータ取得するためのハンドラ
 *
 * @param array $object The レスポンス・オブジェクト
 * @param string $field_name フィールド名
 * @param WP_REST_Request $request 現在のリクエスト
 *
 * @return mixed
 */
function get_field( $object, $field_name, $request ) {
    return get_post_meta( $object[ 'id' ], $field_name, true );
}

結果のJSON

{  
      "id":153,
      "date":"2019-07-31T17:37:44",
      "date_gmt":"2019-07-31T08:37:44",
      "guid":{  
         "rendered":"http:\/\/localhost:8888\/?post_type=movie&#038;p=153"
      },
      "modified":"2019-07-31T17:38:26",
      "modified_gmt":"2019-07-31T08:38:26",
      "slug":"153",
      "status":"publish",
      "type":"movie",
      "link":"http:\/\/localhost:8888\/archives\/movie\/153",
      "title":{  
         "rendered":"\u52d5\u753b1"
      },
      "content":{  
         "rendered":"",
         "protected":false
      },
      "featured_media":0,
      "template":"",
      "youtube":"https:\/\/www.youtube.com\/watch?v=LJwR4iHxKV0",
      "_links":{  
         "self":[  
            {  
               "href":"http:\/\/localhost:8888\/wp-json\/wp\/v2\/movies\/153"
            }
         ],
         "collection":[  
            {  
               "href":"http:\/\/localhost:8888\/wp-json\/wp\/v2\/movies"
            }
         ],
         "about":[  
            {  
               "href":"http:\/\/localhost:8888\/wp-json\/wp\/v2\/types\/movie"
            }
         ],
         "wp:attachment":[  
            {  
               "href":"http:\/\/localhost:8888\/wp-json\/wp\/v2\/media?parent=153"
            }
         ],
         "curies":[  
            {  
               "name":"wp",
               "href":"https:\/\/api.w.org\/{rel}",
               "templated":true
            }
         ]
      }
   }

カスタムエンドポイントの作成

レスポンスの内容使わん情報の方が多くて無駄だな…と思ったら、カスタムエンドポイント作って返す内容全部自分で作ることもできる。

以下はMoviesのリストを返すカスタムエンドポイントのサンプル

/**
 * カスタムエンドポイントの追加
 */
class Custom_Route extends WP_REST_Controller {

  /**
   * Register the routes for the objects of the controller.
   */
  public function register_routes() {
    $version = '1';
    $namespace = 'custom/v' . $version;

    register_rest_route( $namespace, '/movies', array(
      array(
        'methods'         => WP_REST_Server::READABLE,
        'callback'        => array( $this, 'get_movies' ),
        'args'            => array(
          'page' => array(
            'validate_callback' => function($param, $request, $key) {
              return is_numeric( $param );
            }
          ),
          'per_page' => array(
            'validate_callback' => function($param, $request, $key) {
              return is_numeric( $param ) || is_null( $param );
            }
          )
        ),
      )
    ) );
  }

  /**
   * Get a collection of movies
   *
   * @param WP_REST_Request $request Full data about the request.
   * @return WP_Error|WP_REST_Response
   */
  public function get_movies( $request ) {
    $page = !empty($request['page']) ? (int) $request['page'] : 1;
    $per_page = !empty($request['per_page']) ? (int) $request['per_page'] : (int) get_option('posts_per_page');

    $args = array(
      'posts_per_page' => $per_page,
      'post_type' => 'movie',
      'post_status' => 'publish',
      'update_post_meta_cache' => false,
    );

    //ページ送り設定
    if( $page > 1 ) {
      $args['paged'] = $page;
    }

    $posts_query = new \WP_Query( $args );
    $posts_array = array();



    while( $posts_query->have_posts()){
      $posts_query->the_post();

      $posts_array[] = array(
        'id' => $posts_query->post->ID,
        'date' => date( 'Y年m月d日', strtotime( $posts_query->post->post_date )),
        'datetime' => $posts_query->post->post_date,
        'title' => $posts_query->post->post_title,
        'youtube' => get_post_meta($posts_query->post->ID, 'youtube', true),
        'category' => $this->parseCategory(get_the_terms($posts_query->post->ID, 'movie-category'))
      );
    }

    return new WP_REST_Response( array(
      'posts' => $posts_array,
      'page' => $page,
      'perPage' => $per_page
    ), 200 );
  }

  /**
   * 投稿カテゴリーのtermを必要な情報に限定する
   * @param Array.<WP_Term> $terms WP_Term
   */
  public function parseCategory($terms) {
    $result = array();
    $sort = array();
    $parent = null;

    if (!is_array($terms)) return array();

    foreach ($terms as $t) {
      $r = array(
        'id' => $t->term_id,
        'name' => $t->name,
        'slug' => $t->slug
      );

      if ($t->parent !== 0 ) { //親カテゴリがある
        if (is_null($parent) || $parent->term_id !== $t->parent) {
          $parent = get_category($t->parent);
        }

        $r['parent'] = $parent->slug;
      }

      $result[] = $r;

    }

    foreach ((array) $result as $key => $value) {
      $sort[$key] = $value['id'];
    }

    array_multisort($sort, SORT_ASC, $result);

    return $result;
  }

}

add_action( 'rest_api_init', function () {
  $custom_route = new Custom_Route();
  $custom_route->register_routes();
});

URLは http://localhost:8888/wp-json/custom/v1/movies になります。

レスポンスサンプル

{
   "posts":[
      {
         "id":153,
         "date":"2019\u5e7407\u670831\u65e5",
         "datetime":"2019-07-31 17:37:44",
         "title":"\u52d5\u753b1",
         "youtube":"https:\/\/www.youtube.com\/watch?v=LJwR4iHxKV0",
         "category":[
            {
               "id":3,
               "name":"\u97f3\u697d",
               "slug":"music"
            }
         ]
      }
   ],
   "page":1,
   "perPage":10
}

サイト側の対応について

あとはREST APIエンドポイント叩いて帰ってくるJSONをよしなに処理すればおk。
詳しいことはやる気が出たら別記事に載せますが、APIヘのリクエストは

fetchを使うならこう:

fetch('http://localhost:8888/wp-json/wp/v2/posts', { page: 1 })
  .then(res => {
    return res.json();
  })
  .then(json => {
    console.log(json) // response
  })
  .catch(error => {
    console.error(error);
  });

jQueryならこんな感じで:

$.get'http://localhost:8888/wp-json/wp/v2/posts', { page: 1 })
  .done((res) => {
    console.log(res)
  })
  .fail(error => {
    console.log(error)
  });

WordPress置いてるサーバー側には他からAPI使われないように制限をかけておくといいです。

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください