Nuxt.js + Netlify + WP REST APIでブログサイトをつくってみる①

Nuxt.js + Netlify + WP REST APIでブログサイトをつくってみる①

はじめに

前回、「Nuxt.js + Netlifyで問い合わせフォームを作る方法【自動返信あり】」という記事でNuxt.jsとNetlifyでお問い合わせフォームを作りましたが、今回はWP REST APIを組み合わせてブログサイトを作ってみました。

ブログサイトは、このサイトのWP REST APIをカスタマイズして使います。

ひとまず完成したサイトはこちら。
https://notes-sharesl.netlify.app/

このブログ自体がまだ30記事程度しかないので、ページネーションとか細かい機能は作ってませんが、新着・ピックアップ・カテゴリー・タグ・執筆者・フリーワード検索のいずれかで記事を探して見ることが可能になっています。

足りない機能ももちろんありますが、とりあえずここまでの作成で学べたことをメモしておきたいと思います。

その中でも今回は、

  • WP REST APIとの連携
  • Nuxt.jsでの静的生成の調整

この2点についてをメモしておきます。

WP REST APIとの連携

静的サイトジェネレーター(SSG)としてNuxtを使います。ただブログ記事をコード内に直書きなんてしてられないのでCMSと連携させることが多いと思います。

今回はWordPressと連携します。

なんでWordPress?

最近は手軽に扱えるContentfulやNetlify CMSなどがよく紹介されていますが、エンジニアが自分で扱うなら正直どんなCMSだろうが、極端な話テキストファイルにMarkdownで直書きだろうが、何でも良いのですが、クライアントを想定した場合、管理画面はWordPressの方が断然使い勝手が良いですし、説明しやすいです。要望に応じて管理画面自体のカスタマイズも容易ですしね。

なのでWordPressを使います。

WP REST APIのエンドポイント調整

まずググるとよく出てくるのが、素のままのエンドポイントを使う方法です。

/wp-json/wp/v2/postsで投稿一覧、/wp-json/wp/v2/categoriesでカテゴリー情報、みたいな。

でもこれ無駄な情報が多く含まれてるしすごい扱いにくいんですよね。アクセスしてみると分かりますが、記事のping_statusとか使う人いるの?アイキャッチもカテゴリーも作成者もIDしか含まれてないから画像や名前ひっぱりたくてもデフォルトじゃ付いてこないからそのまま使えないし。記事もカテゴリーも素のままじゃ最大10件しか取得できないし、件数指定のパラメータ付けても最大100件までの取得制限あるし。もちろんカスタムフィールドも含まれないし。。。。

…こんなの使えないよ。笑

以前書いた記事「WP REST APIで独自エンドポイントの作り方」と同じ方法で、必要なAPIのエンドポイントを自分で生成します。すでにあるエンドポイントにカスタムフィールドの情報を追加することもできますが、自分で新しく作った方が調整が簡単です。WordPressで普通に投稿ページ作るのとやることあんまし変わらんので、WPのテンプレ作成に慣れている方は独自エンドポイントの作成を強くおすすめします。その方が必要な情報を必要な分だけ好きなように取得できます。

  • /wp-json/custom/v0/posts=>すべての記事一覧
  • /wp-json/custom/v0/categories => カテゴリー一覧
  • /wp-json/custom/v0/tags =>タグ一覧
  • /wp-json/custom/v0/authors=> 作成者一覧
  • /wp-json/custom/v0/pages =>固定ページ一覧

今回はこの5つのカスタムエンドポイントを作成して情報を取得します。
これらの実装内容を掲載してると先に進まないので割愛させていただきます。
REST APIの作り方については過去記事を参照ください。

Nuxt.jsの静的サイト生成の調整

NuxtでREST APIと連携して記事を作る方法は、

  1. ajaxでWPのエンドポイントにアクセスして情報取得
  2. 取得したデータをテンプレートに流し込む

ざっくり言えばこれだけです。
あとはCSSで好きなデザインにするだけなのでフロントはめちゃくちゃ開発しやすいです。

ただこれだけでは静的サイト生成ができません。
ここでは静的サイト生成の具体的な方法をメモしていきます。

APIからのデータ取得方法

pageコンポーネント内で、fetchもしくはasyncDataを使ってAPIにアクセスします。
※fetchフックはNuxt 2.12 以降新しくなっていますのでご注意。

データの取得 - NuxtJS

https://ja.nuxtjs.org/docs/2.x/features/data-fetching/

まずaxiosでajaxの設定。@nuxtjs/axiosを使います。インストールしていない場合はまずインストール。

$ yarn add -D @nuxtjs/axios

続いてnuxt.config.jsに追記

export default {
  //...省略
  axios: {
    baseURL: 'https://example.com/wp-json'
  },
  //...省略
}

このようにaxiosにWP REST APIエンドポイントのベースとなるURLを設定しておくと便利です。例えば記事一覧を取得したい場合、

this.$axios.$get('https://example.com/wp-json/custom/v0/posts')

と書くところを、ベースURLは省略して

this.$axios.$get('/custom/v0/posts')

という形で書くことができるようになります。

環境変数を使うとローカルと本番環境を分けられてさらに便利。

dotenvをインストール

$ yarn add -D dotenv

.envファイルをルートディレクトリに作成

BASE_API_URL=http://localhost/notes/wp-json

nuxt.config.js

require("dotenv").config()
export default {
  //...省略
  axios: {
    baseURL: process.env.BASE_API_URL
  },
  //...省略
}

Netlify上の環境変数の設定は、
Setting -> Build & deploy -> Environment -> Environment variables から追加できます。

少し脱線しましたが戻ります。
pageコンポーネントでfetchもしくはasyncDataを使います。

<script>
  export default {
    //fetchは取得したデータをstoreに保存する時に使う
    async fetch() {
      const res = await $axios.$get('/custom/v0/posts')
      .catch((err) => {
        console.error(err)
      });
      //storeにデータ保存してどこからでも扱えるようにする
      await this.$nuxt.context.store.commit('saveAllPosts', res);
    },
    //asyncDataは返却地がそのコンポーネントのdataにマージされる。
    async asyncData({ params, error, payload, store, $axios }) {
      const res = await $axios.$get('/custom/v0/posts')
      .catch((err) => {
        console.error(err)
      });
      //this.postsでアクセス可能になる
      return {
        posts : res
      }
    }
  }
</script>

コード内のコメントにも書いていますが、Vuex storeにデータを保存して同じデータを複数コンポーネントで扱う場合はfetch、それ以外で現在のコンポーネントのみで必要なデータを取得して扱う場合はasyncData、と使い分けるみたいです。

静的サイト生成する場合、記事一覧、カテゴリー一覧といったAPIで取得するデータを一度にすべて取得して複数箇所で使いまわしたいのでfetchを使ってstoreで状態管理させます。

まずはstore/index.jsに状態管理するためのコードを書きます。

export const state = () => ({
  //すべての記事
  allPosts : null
});

export const getters = {
  //すべての記事
  allPosts(state){
    return state.allPosts;
  }
}

export const mutations = {
  //すべての記事を保存
  saveAllPosts(state, allPosts){
    state.allPosts = allPosts;
  }
}

export const actions = {
  /*
  * 全記事取得
  */
  async fetchAllPost({ state, commit }){
    const res = await this.$axios.$get('/custom/v0/all')
    .catch((err) => {
      console.error(err)
    });
    await commit('saveAllPosts', res);
    return res;
  }
}

続いてpageコンポーネントにfetchを使ってstoreにデータを保存する処理を書きます。

※fetchフックの使い方がNuxt 2.12 以降変更されています。
こちらの記事が参考になりますので気になる方はご確認ください。

Nuxt2.12.0で新しくなったfetchについて

https://qiita.com/miyaoka/items/11fdc03ff591d34a585c

尚、この記事では以降のコード紹介で当初fetch(context)として引数でcontextを受け取る形式で書いていましたが、記事を読んでくださった有識者の方からご指摘いただき、fetchasyncDataに変更しております。fetchとasyncDataの使い分けの話と矛盾が生じてしまいますが、動作としては問題ありませんのでご了承ください。

export default {
  async fetch() {
    if(!this.$nuxt.context.store.getters.allPosts){
      await this.$nuxt.contextstore.dispatch('fetchAllPost');
    }
  }
}

これでAPIから取得したデータはすべてVuex storeで状態管理できるようになりました。storeに情報を保存できればあとは煮るなり焼くなりって感じです。カテゴリーやタグなど必要な情報をAPIから取得する処理をstoreに追記して、データをフィルターしたり検索したりうまく利用して、記事詳細ページ、カテゴリーページ、タグページなどを実装します。

nuxt generateコマンドで静的ファイル生成

フロントページが実装できたら、nuxt generateコマンドを使って静的ファイルを生成します。ただし、何もせずそのままコマンドを実行しても失敗します。理由はnuxt.config.jsファイルで静的に生成するファイルを指定してやらないとファイルが生成されないためです。

Nuxt.jsの公式ドキュメントを参考にして実際に書いてみます。

require("dotenv").config()
import axios from 'axios';

export default {
  mode: 'universal',

  //...省略

  generate: {
    fallback : true,
    interval : 100,
    routes (callback) {
      Promise.all([
        axios.get(`${process.env.BASE_API_URL}/custom/v0/posts`),
        axios.get(`${process.env.BASE_API_URL}/custom/v0/categories`),
        axios.get(`${process.env.BASE_API_URL}/custom/v0/tags`),
        axios.get(`${process.env.BASE_API_URL}/custom/v0/authors`),
        axios.get(`${process.env.BASE_API_URL}/custom/v0/pages`)
        ])
      .then (axios.spread( (posts, categories, tags, authors, pages) => {

        //ここに静的ページ生成の処理を書く

        callback()
      }))
    }

  },

  //...省略

}

何をしてるか簡単に説明。
nuxt genetrateコマンドを実行したときに、generateプロパティに設定した内容が実行されます。静的サイト生成する時のオプションですね。

  • fallback : true => エラーページに404.htmlを設定(layouts/error.vueの内容が404ページとして使われる)
  • interval : 100 => デフォルトではAPIアクセスをほぼ同時に行います。非力なサーバーにWordPressを積んでる場合、503エラーが頻発してうまく情報が取得できない場合があるので、アクセスの間隔をここで設定して回避します。
  • routes => デフォルトでは/しか静的ファイル生成しないため、ここに動的に追加されたルートを配列で設定します。APIで取得したページをベースにするので、APIにaxiosでアクセスした結果を配列に反映します。

かなり省略した説明になっていますので、詳しくは公式ドキュメントでもご確認ください。

API: generate プロパティ

https://ja.nuxtjs.org/api/configuration-generate/

ここで1番大事なのがroutesの設定なので詳しく中身を見ていきます。

routes (callback) {
  Promise.all([
    axios.get(`${process.env.BASE_API_URL}/custom/v0/posts`),
    axios.get(`${process.env.BASE_API_URL}/custom/v0/categories`),
    axios.get(`${process.env.BASE_API_URL}/custom/v0/tags`),
    axios.get(`${process.env.BASE_API_URL}/custom/v0/authors`),
    axios.get(`${process.env.BASE_API_URL}/custom/v0/pages`)
    ])
  .then (axios.spread( (posts, categories, tags, authors, pages) => {
    //記事詳細ページ
    const route_post = posts.data.map((post) => {
      return {
        route   : `/articles/${post.id}`,
        payload : {
          posts       : posts.data,
          currentPost : post,
          categories  : categories.data
        }
      }
    })

    //カテゴリーページ
    const route_category = categories.data.map((category) => {
      return {
        route   : `/${category.slug}`,
        payload : {
          posts           : posts.data,
          categories      : categories.data,
          currentCategory : category,
        }
      }
    })

    //配列を結合してcallbackに
    callback(null, route_post.concat(route_category))
  }))
}

ここでは一気にすべてのAPIから情報取得しましたが、コードが長くなるので説明のために記事詳細ページとカテゴリー一覧ページだけの静的ファイル生成を設定しました。

記事詳細ページはaxiosで取得した情報をmap()メソッドで回してrouteに設定します。

//posts.dataに取得したデータが入っているのでそれをmapメソッドで回す
const route_post = posts.data.map((post) => {
  return {
    //記事詳細ページのルートを設定する。routeには実際に生成するパスを設定
    route   : `/articles/${post.id}`
  }
})

また、payloadを使うことで一度取得したデータをコンテキストから使いまわせるので、何度も同じAPIにアクセスせずに済みます。これは静的ファイル生成の時間を短縮でき、フロント側で発生するAPIへのアクセスも省略できますのでほぼ必須の設定になると思います。

const route_post = posts.data.map((post) => {
  return {
    route   : `/articles/${post.id}`,
    //payloadを使うとasyncDataやfetchの引数から取得できる
    payload : {
      //すべての記事の情報
      posts       : posts.data,
      //現在の記事の情報
      currentPost : post,
      //すべてのカテゴリー情報
      categories  : categories.data
    }
  }
})

pages/article/_id.vueでpayloadを使ってデータの効率化

export default {
  async asyncData({ params, error, payload, store, $axios }) {
    //コンテキストからpayloadを取得して、payloadがある場合はそれを使います。
    if (payload){
      //全ての記事をstoreに保存
      await store.commit('saveAllPosts', payload.posts);
      //現在表示している記事をstoreに保存
      await store.commit('saveCurrentPost', payload.currentPost);
      //カテゴリー一覧をstoreに保存
      await store.commit('saveAllCategories', payload.categories);
      return;
    }
    //payloadがない時はAPIから取得する処理
    else{
      //パラメータがない場合は404
      if(!params.id){
        await error({ statusCode: 404, message: 'Post not found' });
        return;
      }
      //記事一覧がstoreにある場合はidから現在の記事を抜き出して保存
      if(store.getters.allPosts){
        await store.commit('saveCurrentPostById', params.id);
      }
      //記事一覧も現在の記事一覧もstoreにない場合はAPIから取得
      if(!store.getters.allPosts && !store.getters.currentPost){
        await store.dispatch('fetchAllPost', params.id);
      }
      //カテゴリー情報がstoreにない場合はAPIから取得
      if(!store.getters.allCategories){
        await store.dispatch('fetchCategories');
      }
    }
  },

  //...省略

}

store/index.jsにstore内の処理を追記

export const state = () => ({
  //すべての記事
  allPosts        : null,
  //現在の記事
  currentPost     : null,
  //カテゴリーページ すべて
  allCategories   : null
});

export const getters = {
  //すべての記事
  allPosts(state){
    return state.allPosts;
  },
  //現在の記事
  currentPost(state){
    return state.currentPost;
  },
  //カテゴリーページ すべて
  allCategories(state){
    return state.allCategories;
  }
}

export const mutations = {
  //すべての記事を保存
  saveAllPosts(state, allPosts){
    state.allPosts = allPosts;
  },
  //現在の記事をすべての記事からIDで検索して現在の記事を保存
  saveCurrentPostById(state, postId){
    if(!state.allPosts){
      return;
    }
    state.currentPost = state.allPosts.find( (post) => {
      return Number(post.id) === Number(postId)
    });
  },
  //現在の記事を保存
  saveCurrentPost(state, currentPost){
    state.currentPost = currentPost;
  },
  //すべてのカテゴリーを保存
  saveAllCategories(state, allCategories){
    state.allCategories = allCategories;
  }
}

export const actions = {
  /*
  * 全記事取得
  */
  async fetchAllPost({ state, commit }, postId = null){
    const res = await this.$axios.$get('/custom/v0/all')
    .catch((err) => {
      console.error(err)
    });
    await commit('saveAllPosts', res);
    if(postId){
      await commit('saveCurrentPostById', postId);
    }
    return res;
  },
  /*
  * 全カテゴリー取得
  */
  async fetchCategories({ state, commit }){
    const res = await this.$axios.$get('/wp/v2/categories')
    .catch((err) => {
      console.error(err)
    });
    await commit('saveAllCategories', res);
    return res;
  }
}

個人的にVuex storeを使う時は、API取得周りの処理はactionsにまとめておくことにしています。コンポーネント内でaxiosを使わないで良くなるので処理を追うのが簡単になるし、コードの見通しがよくなるのでこうしてます。

カテゴリーページについてはコードを省略しますが、ほぼまったく同じ仕組みでAPIから情報を取得し、同じようにfetchからstoreに保存するように作ります。

そうしてページができ上がったら、ターミナルからコマンドを実行します。

$ nuxt generate

//または

$ npm run generate

//または

$yarn generate

これでコマンドが実行されて静的ファイルが生成されます。
少し時間がかかりますが、エラーなく終了すればOKです。特別なにも設定していなければdistというフォルダがルートディレクトリにできあがりますが、これが静的に生成されたサイトになります。

静的サイトのローカルでの確認方法

ローカルで作った場合はそのままアクセスしても相対パスがずれて見れません。http-serverというnpmパッケージを使って見れるようにしましょう。

まずインストール

$ yarn add -D http-server

package.jsonに追記

{
  //省略...

  "scripts": {
    //省略...
    "generate": "nuxt generate",
    "webserver": "http-server ./dist -o -p 8888" //追記
  },

  //省略...
}

コマンドを実行

$ yarn webserver

サーバーが立ち上がって、生成された静的サイトを確認できます。

「http-server」についてはこちらのサイトを参考にさせていただきました。

Nuxt.jsでgenerateで出力された静的ファイルは「http-server」を使ってローカルでさくっと確認しちゃおう

https://papadays.com/post/6gozzcufrbylieiwjyox9r/

Netlifyへのデプロイ

最終的に生成した静的サイトはNetlifyから配信させたいので、デプロイの設定をします。といってもやることはかなり簡単。

  • Netlifyにアカウント登録
  • GithubやBitbucketなどと連携
  • デプロイするサイトのGitリポジトリ・ブランチ(masterとか)を設定
  • Build commandにnpm run generateと設定
  • Publish directoryにdistと設定

これでデプロイまでの設定は完了!
あとはNetlify上でデプロイを手動でトリガーするか、該当のgitリポジトリにpushすることでデプロイが始まります。

一応、参考サイトを置いておきます。

NetlifyでWebサイトを公開する方法

https://qiita.com/shozzy/items/dadea4181d6219d2d326

デプロイ時の注意点

WordPressやサーバー側でセキュリティ対策をしている場合、NetlifyからのアクセスをWordPress側で弾いてしまってAPIを取得できずデプロイが失敗する場合があります。

よくあると思われるのは、

  • サーバーによって、海外IPからのアクセス制限が自動で設定されている
  • IP Geo Blockなどのプラグインで海外からの不正アクセス対策をしている

このようなアクセス制限です。

海外IPからのアクセス制限は各サーバーのコントロールパネルなどで解除する方法が違いますので調べてみてください。

例に出しましたIP Geo Blockの場合は、プラグインの設定画面を開き、「国コードに優先して検証するIPアドレスのホワイトリスト」の欄にNetlifyのIPアドレスを入力してあげればOKです。

NetlifyのIPアドレスは、独自ドメインを設定する時に表示されるAレコードが使えます。特に独自ドメイン自体を持っていなくても、Netlifyに適当なドメインを入力して追加しDNSの確認をすると、これをAレコードに設定してねって感じでAレコードが表示されます。そこに表示されているIPアドレスからのアクセスを許可すれば良い、ということです。

もしデプロイ時にnuxt generateで404・403エラーで失敗してしまうっていう時は、一度WordPressのサーバー側でアクセス制限していないか確認してみてください。

記事の公開に合わせてデプロイする

WordPressで記事更新する場合、いちいち更新するたびにNetlify開いてデプロイしたり、git pushしたりはしたくないですよね。できれば記事を更新・公開するタイミングで自動でデプロイしたいです。

そこはさすがWordPress。プラグインであります。

これをインストールしてWordPress管理画面の「設定」=>「Deployments」に進みます。

JAMstack Deployments設定画面
JAMstack Deployments設定画面

Netlifyの設定画面を開き、Build&deploy > Build hooksの下にあるAdd build hookボタンを押します。すると「ビルドフック名」と「ビルドするブランチ名」を指定する画面が出てきます。

Netlify Build hooksの設定画面
Add build hookボタンを押したら上記のような画面が出てくる

フック名はなんでもOKですのでwp-deployとかなんかわかりやすい名前をつけると良いかと思います。ブランチはデプロイしたいgitブランチですので適宜セレクターから設定してください。そしてSaveボタンで保存します。

するとhttps://api.netlify.com/build_hooks/~~というURLが表示されますので、これを先ほどのプラグインの画面の「Build Hook URL」に入れます。

あとは「Hook Method」はPOST、どの記事更新のタイミングでデプロイさせるかは「Post Types」や「Taxonomies」にチェックを入れればOKです。

「Badge Image URL」「Badge Link」はなくても動作しますが、設定したい場合は、Netlify管理画面のGeneral > Status badgesに進みます。

[![Netlify Status](https://api.netlify.com/api/v1/badges/~~~~~~~/deploy-status)](https://app.netlify.com/sites/~~~~~~~~/deploys)

こんなコードが表示されており、コード内に2種類のURLがありますがそれを設定します。

「Badge Image URL」にhttps://api.netlify.com/api/v1/badges/~~~/deploy-status

「Badge Link」にhttps://app.netlify.com/sites/~~~~/deploys

それぞれ設定すると管理画面の右上にバッジが表示されるようになります。

JAMstack Deployments Badge
JAMstack Deployments設定画面の右上

これで設定完了です!記事更新でデプロイされるようになりました。

※Netlifyにはビルド時間の上限があります。こまめに記事修正・更新する場合は手動でデプロイした方が良い場合もありますのでご注意ください。

「JAMstack Deployments」についてはこちらの記事を参考にさせていただきました。

NetlifyにデプロイしてくれるWPプラグインJAMstack Deploymentsの設定方法

https://renaca.jp/blog/wp-plugin-jamstack-deployments/

さいごに

今回はNuxt + Netlify + WP REST APIで静的なブログサイトを生成する方法の中で個人的に重要なポイントだけをまとめて記事にしました。実際のページの作り方やOGPの設定についてはまた別の記事で詳しく書こうと思います。

Nuxtで静的ファイルを生成してNetlifyで配信することで、元のサイトよりかなり爆速表示されるようになりました。静的なのでセキュリティについても安心できそう。

ただWordPressと連携するに当たって、いくつか解決できていない問題が残っています。

  • よく作るデモページのようなNuxtに依存しないページの置き場所 => Github Pages + Gistで対応
  • WPのプレビュー機能 => 管理画面にCSSを適用する?プレビュー用のAPI作る?
  • AMPページの生成方法 => 爆速になったらいらない?
  • PWA対応 => ブログレベルのサイトだとキャッシュもややこしいし普通に必要ない?
  • ドメインをどうするか => WPのドメインとNetlifyのドメインで別々のものを使う必要があるので運用面でめんどくささがある

開発者ブログとして今のこのサイトと同じような状態で運用するにはまだこの辺りの調査・調整に時間がかかるためこのサイトと差し替えるまでには至っていません。率直な感想としては、運用のことを考えるとWordPress単体の方が自由度効くし慣れているので楽ですね。個人的な問題ですが、実運用するにはちょっとまだ知識的に足りないかなーというところです。爆速とセキュリティは非常に魅力的なんですが・・・。

とにかくもっと調べて使ってみないとわからないですね!
次回はこの続きで、「OGPの設定方法」を書こうと思います!

ひとつひとつ問題をクリアして使えるサイトにしていきたいと思います。