Nuxt.js+Wordpressでポートフォリオサイトを制作した際の備忘録

# はじめに
Nuxt.jsとWordpressを使用して私のポートフォリオサイトをリニューアルした際の備忘録です。

- [成果物](https://afterworks.jp)
- [ソース](https://github.com/InumberX/AfterWorks.ver.4.0.0)

もともとWordpressを使用して作成したポートフォリオサイトがありましたが、表示速度の向上や自身の学習を目的にNuxt.jsで作り直しました。
ただ、サイト内のコンテンツの管理はそのまま行いたかったので、WordpressをHeadlessCMSとして利用しました。
この記事では、Nuxt.jsの使い方やWordpressとの連携方法などをご紹介します。

# 環境
## リニューアル前
リニューアル前は下図のような環境構成となっていました。
よくあるWordpressのみで作成したサイトです。
コンテンツ管理が楽な反面、表示速度が遅いなどの問題がありました。
![img_before.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191403/4b96b05f-2d7b-b7b7-c931-573d55a5545b.jpeg)

## リニューアル後
リニューアル後は下図のような環境構成になりました。
Wordpressで固定ページを更新した際にJSONデータが出力されます。
更新時にCircle CIを利用して自動的にデプロイを行わせるようにしました。
この仕組みにより、つねに最新のデータがページに反映されるようになります。
![img_after.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191403/ca672fda-51aa-f6d9-9c6e-60f05ba12fa7.jpeg)

# Wordpress
WordpressをHeadlessCMSとして利用する場合、デフォルトで用意されているWP REST APIを利用するのが楽です。
ただ、今回は以下のような理由からJSONデータを出力する仕組みを自作しました。

- ブログ機能を利用しない
- コンテンツの更新頻度が低い
- WP REST APIを利用した場合に表示速度の低下が懸念される

## テーマファイル
今回、作成したWordpressのテーマファイルのディレクリ構成は下記のような形になります。

```text
aw_rest
 ├ functions.php
 ├ index.php
 ├ screenshot.png
 ├ style.css
 └ sh
    └ deploy.sh
```

Wordpress側はJSONを出力するだけなので、必要最低限のファイルしか用意していません。
JSONを出力する処理はfunctions.phpで行います。

```php:functions.php
<?php

// 固定ページ投稿時の処理
function createJsonPage($post_id) {
 // ドキュメントルート
 $DOCUMENT_ROOT = $_SERVER['DOCUMENT_ROOT'];
 // JSON格納ディレクトリ 
 $JSON_DIR = $DOCUMENT_ROOT . 'json/page/';

 // JSONを出力するページのタイトル
 $PAGE_TITLE_ARRAY = array(
  'INDEX',
  'ABOUT',
  'WORKS'
 );
 $PAGE_TITLE_ARRAY_LENGTH = count($PAGE_TITLE_ARRAY);

 // JSON出力ディレクトリ
 $outputDir = '';
 // JSON出力判定
 $isOutput = false;

 // 配列データ
 $dataArray = '';
 // JSONデータ
 $dataJson = '';

 // データを取得
 $post = get_post($post_id);
 // タイトル
 $title = $post->post_title;

 for($i = 0; $i < $PAGE_TITLE_ARRAY_LENGTH; $i++) {
  // JSON出力を行うページの場合
  if($PAGE_TITLE_ARRAY[$i] == $title) {
   // カスタムフィールドを取得
   $smartCustomFields = getCustomFields($_POST['smart-custom-fields']);

   $dataArray = array(
    'id' => $post_id,
    'title' => $title,
    'createDate' => mysql2date('Y/m/d H:i:s', $post->post_date),
    'updateDate' => mysql2date('Y/m/d H:i:s', $post->post_modified),
    'contents' => $smartCustomFields
   );

   // JSON出力ディレクトリを設定
   $outputUrl = $JSON_DIR . mb_strtolower($title) . '.json';
   $isOutput = true;

   break;
  }
 }
 
 // JSON出力を行う場合
 if($isOutput) {
  // JSON生成
  $dataJson = json_encode($dataArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
  // JSON出力
  file_put_contents($outputUrl, $dataJson);

  // ビルド実行
  while(true){
   if(is_readable($outputUrl)) {
    shell_exec('nohup sh /xxx/xxx/xxx/deploy.sh > /xxx/xxx/xxx/deploy.log 2>&1 &');
    break;
   }
  }
 }

}

// カスタムフィールドを加工する処理
function getCustomFields($customFields) {

 foreach($customFields as $key => $value) {
  // 末尾が「Img」の場合
  if(mb_substr($key, -3) == 'Img') {
   // 画像IDをパスに変換
   foreach($customFields[$key] as $subKey => $subValue) {
    $imageId = $customFields[$key][$subKey];
    $imageSrc = wp_get_attachment_image_src($imageId, 'full');
    if($imageSrc) {
     $customFields[$key][$subKey] = $imageSrc[0];
    }
   }
  } else {
   // 末尾が「Img」以外の場合
   // ダブルクォーテーションをエスケープ
   foreach($customFields[$key] as $subKey => $subValue) {
    $value = $customFields[$key][$subKey];
    $value = str_replace('"', "&quot;", $value);
    $value = str_replace('\\', "", $value);
    $customFields[$key][$subKey] = $value;
   }
  }
 }

 return $customFields;

}

// 固定ページ更新時
add_action('publish_page', 'createJsonPage');

?>
```

固定ページ更新時に何か処理を行いたい場合は、アクションフックのpublish_pageを利用することで実現可能です。

```php
add_action('publish_page', 'createJsonPage');
```

上記のように記述することで、固定ページ更新時にcreateJsonPageというメソッドが実行されます。

createJsonPageでは、ページのタイトルが「INDEX」、「ABOUT」、「WORKS」のいずれかだった場合にJSONを出力する処理が実行されます。
出力されるJSONには以下のような情報を含めました。

- ページID
- ページタイトル
- 作成日
- 更新日
- コンテンツ情報（カスタムフィールド）

カスタムフィールドは[Smart Custom Fields](https://2inc.org/blog/2014/10/09/4426/)を利用し、カスタマイズしました。
カスタムフィールドの繰り返し機能が無料枠で用意されているので、カスタムフィールドを拡張したい時には重宝します。

また、カスタムフィールドに画像を登録すると画像のIDが保存されるので、そのままではNuxtで利用することはできません。
そこで、画像の場合はカスタムフィールド名の末尾に「Img」を付けることで、JSON生成時に画像IDからパスを取得する処理を実装しています。

```php
// カスタムフィールドを加工する処理
function getCustomFields($customFields) {

 foreach($customFields as $key => $value) {
  // 末尾が「Img」の場合
  if(mb_substr($key, -3) == 'Img') {
   // 画像IDをパスに変換
   foreach($customFields[$key] as $subKey => $subValue) {
    $imageId = $customFields[$key][$subKey];
    $imageSrc = wp_get_attachment_image_src($imageId, 'full');
    if($imageSrc) {
     $customFields[$key][$subKey] = $imageSrc[0];
    }
   }
  } else {
   // 末尾が「Img」以外の場合
   // ダブルクォーテーションをエスケープ
   foreach($customFields[$key] as $subKey => $subValue) {
    $value = $customFields[$key][$subKey];
    $value = str_replace('"', "&quot;", $value);
    $value = str_replace('\\', "", $value);
    $customFields[$key][$subKey] = $value;
   }
  }
 }

 return $customFields;

}
```

Wordpressで固定ページを更新した後は、Circle CIを動作させページを新たに生成する必要があります。
Circle CIはGitHubに何かしらファイルをコミットすれば動作するので、シェルスクリプトを使いGitHubに空ファイルをコミットすることで実現しました。

```php
// ビルド実行
while(true){
 if(is_readable($outputUrl)) {
  shell_exec('nohup sh /xxx/xxx/xxx/deploy.sh > /xxx/xxx/xxx/deploy.log 2>&1 &');
  break;
 }
}
```

固定ページ更新時に上記処理でシェルスクプリトを実行します。
実行された下記シェルスクリプトで空ファイル（deploy.txt）をGitHubにコミットしています。

```shell:deploy.sh
#!/usr/local/bin/bash

CLIENT_ID="XXX"
CLIENT_SECRET="XXX"
CODE="XXX"
TOKEN="XXX"
AUTH_HDR="Authorization: bearer $TOKEN"
CONTENT=`cat /xxx/xxx/xxx/deploy.txt`
REPO="api.github.com/repos/xxx/xxx"

BLOB_SHA=`curl -H "$AUTH_HDR" -X POST \
         -d "{\"content\":\"$CONTENT\",\"encoding\":\"utf-8\"}" \
         https://$REPO/git/blobs | /xxx/xxx/xxx/local/rbenv/versions/2.6.3/bin/ruby -r 'json' -e 'puts JSON.parse(STDIN.read)["sha"]'`

BASE_TREE=`curl -H "$AUTH_HDR" https://$REPO/git/trees/master?recursive=1 | /xxx/xxx/xxx/local/rbenv/versions/2.6.3/bin/ruby -r 'json' -e 'puts JSON.parse(STDIN.read)["sha"]'`

TREE_SHA=`curl -H "$AUTH_HDR" -X POST \
         -d "{\"base_tree\": \"$BASE_TREE\",
         \"tree\":[
           {\"path\":\"deploy.txt\",\"mode\":\"100644\",\"type\":\"blob\",\"sha\":\"$BLOB_SHA\"}
          ]}" \
         https://$REPO/git/trees | /xxx/xxx/xxx/local/rbenv/versions/2.6.3/bin/ruby -r 'json' -e 'puts JSON.parse(STDIN.read)["sha"]'`

PARENT_SHA=`curl -H "$AUTH_HDR" https://$REPO/branches/master | /xxx/xxx/xxx/local/rbenv/versions/2.6.3/bin/ruby -r 'json' -e 'puts JSON.parse(STDIN.read)["commit"]["sha"]'`

COMMIT_SHA=`curl -H "$AUTH_HDR" -X POST \
           -d "{\"message\":\"deploy\", \"tree\":\"$TREE_SHA\", \"parents\":[\"$PARENT_SHA\"] }" \
           https://$REPO/git/commits | /xxx/xxx/xxx/local/rbenv/versions/2.6.3/bin/ruby -r 'json' -e 'puts JSON.parse(STDIN.read)["sha"]'`

curl -H "$AUTH_HDR" -X PATCH \
     -d "{\"force\":false, \"sha\":\"$COMMIT_SHA\"}" \
     https://$REPO/git/refs/heads/master
```

# Nuxt.js

## プロジェクト作成

まず、下記手順でNuxtのプロジェクトを作成します。
※Node.jsをインストールしている必要があります。

### プロジェクトを作成したいディレクトリに移動する

```console
cd xxx/xxxx/xxxx
```

### Nextプロジェクトの作成

```console
npx create-nuxt-app 【プロジェクト名】
```

### プロジェクト名の設定

```console
? Project name (【プロジェクト名】) 
```

### プロジェクトの説明

```console
? Project description (My shining Nuxt.js project) 
```

### サーバフレームワークの設定

```console
? Use a custom server framework (Use arrow keys)
❯ none
  express
  koa
  adonis
  hapi
  feathers
  micro
```

※今回は「none」を選択

### モジュールの選択

```console
? Choose features to install (Press <space> to select, <a> to toggle all, <i> to
 invert selection)
❯◯ Progressive Web App (PWA) Support
 ◯ Linter / Formatter
 ◯ Prettier
 ◯ Axios
```

※今回は「Axios」を選択

### UIフレームワークの選択

```console
? Use a custom UI framework (Use arrow keys)
❯ none
  bootstrap
  vuetify
  bulma
  tailwind
  element-ui
  buefy
```

※今回は「none」を選択

### テストモジュールの選択

```console
? Use a custom test framework (Use arrow keys)
❯ none
  jest
  ava
```

※今回は「none」を選択

### レンダリングモードの選択

```console
? Choose rendering mode (Use arrow keys)
❯ Universal
  Single Page App
```

※今回は「Universal」を選択

### 作者名の入力

```console
? Author name (【作者名】) 
```

### パッケージ管理ツールの選択

```console
? Choose a package manager (Use arrow keys)
❯ npm
  yarn
```

※今回は「npm」を選択

### node-sass および sass-loader パッケージをインストール

```console
npm install --save-dev node-sass sass-loader
```

### 開発モードで実行

ここまでくればNuxtのプロジェクトが作成されているハズなので、下記コマンドでNuxtを実行します。

```console
npm run dev
```

確認用のURLがコンソール上に表示されるので、ブラウザからアクセスし画面が表示されていればプロジェクトの作成は完了です。

## ディレクトリ構成
今回、作成したNuxtのディレクトリ構成は下記のような形になります。
全てのファイルを記述すると量が多くなるので、詳細は[GitHub](https://github.com/InumberX/AfterWorks.ver.4.0.0/tree/master/nuxt/afterWorks_v.4.0.0)をご覧ください。

```text
afterWorks_v.4.0.0
 ├ assets
 │  └ css
 ├ components
 │  └ common
 ├ layouts
 ├ pages
 │  └ contact
 ├ plugins
 ├ static
 │  ├ api
 │  ├ img
 │  └ js
 ├ store
 ├ .editorconfig
 ├ .gitignore
 ├ app.html
 ├ nuxt.config.js
 ├ package-lock.json
 └ package.json
```

## 生成ファイル名を固定
Nuxt.jsで生成されたJSなどは基本的にファイル名が乱数で設定されます。
再生成した場合は別の乱数がファイル名として設定されるので、リリースした時にキャッシュが残ってしまうなどの問題を回避できるのだと思います。
ただ、再生成するたびに新規ファイルを作るとリリース時に古いファイルを削除したりしないといけないので、今回はファイル名を固定でつけるようにしました。
nuxt.config.jsのbuildプロパティに下記のように記述することで、生成されるファイル名を固定させることができます。
※下記例の変数cashBusterについては後述します。

```javascript:nuxt.config.js
build: {
 filenames: {
  app: () => 'js/[name].js' + cashBuster,
  chunk: () => 'js/[name].js' + cashBuster,
  css: () => 'css/[name].css' + cashBuster,
  img: () => 'img/[name].[ext]' + cashBuster,
  font: () => 'font/[name].[ext]' + cashBuster,
  video: () => 'video/[name].[ext]' + cashBuster
 }
}
```

## キャッシュバスターを導入
生成するファイルの名前を固定化しましたが、このままではリリース時にキャッシュが残ってしまい古いファイルが参照される恐れがあります。
そこでnuxt.config.js内に下記のように記述し、キャッシュバスターを導入しました。

```javascript:nuxt.config.js
const now = new Date()
let year = now.getFullYear()
let month = now.getMonth() + 1
month = ('0' + month).slice(-2)
let day = now.getDate()
day = ('0' + day).slice(-2)
let hour = now.getHours()
hour = ('0' + hour).slice(-2)
let minute = now.getMinutes()
minute = ('0' + minute).slice(-2)
let second = now.getSeconds()
second = ('0' + second).slice(-2)
const cashBuster = '?ver=' + year + month + day + hour + minute + second

export default {
 mode: 'universal',
 env: {
  cashBuster: cashBuster
 },

〜 中略 〜

}
```

nuxt.config.js内でキャッシュバスターを利用する場合は、そのまま変数cashBusterを使用できます。
単一ファイルコンポーネントなどで使用したい場合は、`process.env.cashBuster`と記述することで利用できます。

# リリース後

## Lighthouse

リリース前後にLighthouseでサイトの測定を実施しました。
まだまだ課題はありますが、点数を向上させることができました。

### Before
<img width="743" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191403/f462ca1e-6d01-226c-9d7b-a76a8148e4aa.png">

### After
<img width="744" alt="スクリーンショット 2019-07-30 22.16.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191403/dd2217b6-db7b-133d-4749-647e4b6813ab.png">

## 今後やりたいこと

1. ページ遷移時のアニメーションをもっとCoolにする
2. ページ遷移時に取得した情報をStoreに保持しておくようにする

# 参考
サイトの制作にあたり、下記情報を参考にさせていただきました。

- [静的ファイルをCircle CIでビルドしてSSHでデプロイする](https://qiita.com/Hoishin/items/0ea130dfc630131a72c6)
- [さくらレンタルサーバーにRubyをインストールする](http://shinsuke789.hatenablog.jp/entry/2015/10/24/123000)
- [シェルスクリプトでGitHub API](https://qiita.com/ueokande/items/f72973b74898aca3440c)
- [GitHub API v3 でリポジトリを作成して、ファイルをコミットする](https://qiita.com/ngs/items/34e51186a485c705ffdb)
- [さくらのレンタルサーバでRubyを使う](https://qiita.com/takemaro/items/7e2b4b7770aff4e0aa93)