※このエントリは、自分のブログに書いていたものを転載したものです(元記事は削除しました)

このブログはWordPressで構築していて、Vue.js + WP REST APIで投稿を表示している。投稿のプレビューを表示するのが少し(だいぶ)大変だったので、どうやって実装したかを記しておく。

プレビュー表示時のURL

まず、WordPressのプレビューの表示はちょっとクセがあって、投稿を公開しているか下書きのままかで、管理画面の「プレビュー」ボタンを押したときに開くURLが違う。パーマリンクの設定が/post/%post_id%のとき、以下のようになる。

  • 投稿を公開しているとき: /post/id/?preview=true
  • 投稿が下書きのままのとき: /?p=id&preview=true

下書きの時は、パーマリンクの設定に関わらず、/?p=id&preview=trueというクエリのついたURLになる。公開されている投稿と下書きの投稿の両方をプレビューしたい場合は、この2つのURLを判別して処理を分岐する必要がある。

プレビューを表示するためにはリビジョンを取得する

「プレビュー」ボタンを押したときに、その時の投稿の内容がリビジョンとして保存される。/wp-json/wp/v2/posts/id/revisionsというURLにリクエストを送ると、すべてのリビジョンが配列で取得できる。配列の1番目の内容がプレビューボタンを押した時の内容。

APIを叩くときに認証する

単純に/wp-json/wp/v2/posts/id/revisionsにアクセスしても、rest_cannot_readというエラーが出てリビジョンは取得できない。APIのリクエスト時に認証をおこなう必要がある。

認証 | WP REST API v2 Documentation

WP REST APIのページに詳しく書いてありますが、

  • APIのリクエスト時にX-WP-Nonceというヘッダーをセットしておく
    • クエリの_wpnonceパラメータでもいい
  • このnonceの値が管理画面にログインしているユーザーのものと一致したら、認証が通ってリビジョンが取得できる

という感じ。nonceの生成はテーマファイルのPHPでおこない、その値をJSで読み取るのがいいっぽい。

index.php

index.php
<!DOCTYPE html>
<html lang="ja">
<head>
  <title><?php echo get_bloginfo('name'); ?></title>
  <?php wp_head(); ?>

  <?php
    $wp_api_settings = json_encode(array(
      nonce => wp_create_nonce('wp_rest'),
      is_logged_in => is_user_logged_in(),
      is_preview => is_preview()
    ));
  ?>
  <script>
    var wpApiSettings = <?php echo $wp_api_settings; ?>;
  </script>
</head>
<body>
  <div id="app"></div>
  <script src="<?php echo get_template_directory_uri(); ?>/index.js"></script>
  <?php wp_footer(); ?>
</body>
</html>

JSのグローバル変数wpApiSettingsに、PHPで出力したJSON$wp_api_settingsを格納して、どこからでもアクセスできるようにしておく。nonceの他に、ログインしているかどうかを判別するis_logged_inと、プレビューかどうかを判別するis_previewも追加。

普通に投稿を表示する

さて、前置きが長くなったけど、ここからVue.jsでプレビューを表示して行く。まずは普通に投稿を表示してみる。vue-routerで、URLから投稿のidを取得できるようにしておく。

router.js

router.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import index from '@/pages/index.vue'
import single from '@/pages/single.vue'

Vue.use(VueRouter)

const routes = [
  {path: '/', component: index},
  {path: '/post/:id', component: single}
]

const router = new VueRouter({
  mode: 'history',
  routes
})

export default router

single.vue

single.vue
<template>
  <article>
    <h1 v-html="post.title.rendered"></h1>
    <div v-html="post.content.rendered"></div>
  </article>
</template>

<script>
  import axios from 'axios'

  export default {
    data () {
      return {
        post: {}
      }
    },

    mounted () {
      const postId = this.$route.params.id

      axios.get('/wp-json/wp/v2/posts/' + postId)
        .then((res) => {
          this.post = res.data
        })
    }
  }
</script>

以上で投稿は表示できるけど、投稿が公開されているときしか表示できない。

公開済みの投稿のプレビューを表示する

プレビューの時だけリビジョンの1番目をpostに入れる。まずは公開済みの投稿のプレビューを表示した時、つまり/post/id/?preview=trueにアクセスした時。

single.vue

single.vue
<template>
  ...
</template>

<script>
  import axios from 'axios'

  export default {
    data () {
      return {
        post: {}
      }
    },

    mounted () {
      const client = axios.create({
        baseURL: '/wp-json/wp/v2',
        headers: {'X-WP-Nonce': window.wpApiSettings.nonce}
      })
      const postId = this.$route.params.id

      if (window.wpApiSettings.is_preview) {
        client.get('/posts/' + postId + '/revisions')
          .then((res) => {
            this.post = res.data[0]
          })
      } else {
        client.get('/posts/' + postId)
          .then((res) => {
            this.post = res.data
          })
      }
    }
  }
</script>

axiosは、パラメータは同じだけどURLが違うだけみたいな複数のリクエストで、作ったインスタンスを使い回せて便利。axios.create()の引数の中でX-WP-Nonceというヘッダーを指定している。

/posts/idを叩いた時はオブジェクトで投稿内容が取得できるけど、リビジョンの場合は配列なので、res.data[0]をpostに格納する。

下書きのプレビューを表示する

PHPのis_preview()trueで、投稿のidが分かっていればリビジョンは取得できる。
ただ、vue-routerの設定で、/post/idにアクセスした場合はidが取得できるようにしているわけだけど、下書きのプレビューを表示する時、つまり/?p=id&preview=trueにアクセスする場合は、ルーティングのルールに合致しないのでidが取得できない。現状のrouter.jsの設定だとindexコンポーネントを表示するようになっている。

vue-routerはクエリに応じてルーティングすることができない仕様なので(たぶん)、/?p=id&preview=trueにアクセスした場合はsingleコンポーネントを表示する、とかはできない。なので、indexコンポーネントの中で頑張る必要がある。

ということで、以下のような感じで実装すると、下書きでもプレビューを表示できる。

  • is_preview()true、かつURLにクエリがある
  • ppreviewというパラメータがある
  • このときだけ、singleコンポーネントに遷移する
  • 遷移時にidを渡す

今回は、beforeRouteEnterというコンポーネント内ガードをindex.vueに追加することにした。

ナビゲーションガード · vue-router

index.vue

index.vue
<template>
  ...
</template>

<script>
  export default {
    beforeRouteEnter (to, from, next) {
      next((vm) => {
        const query = vm.$route.query

        if (window.wpApiSettings.is_preview && Object.keys(query).length > 0) {
          if (Object.keys(query.p).length > 0 && query.preview) {
            vm.$router.replace({path: '/post/' + query.p})
          }
        }
      })
    }
  }
</script>

もっといい条件分岐の書き方があるかも知れませんが。router.push()ではなくrouter.replace()で遷移させると、ブラウザの履歴に遷移が追加されない。

これで、管理画面のプレビューボタンを押してindexに遷移してしまっても、singleに自動で遷移し、投稿を取得・表示することができる。


最後に

だいぶ長くなってしまったけど、Vue.jsとWP REST APIで投稿のプレビューを表示するためにはこれだけ大変という話でした。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.