Help us understand the problem. What is going on with this article?

【実録】WordPressサイトをAWS+Laravel+Nuxtにフルリプレイスした話

概要

創業2期目のスタートアップ株式会社NoSchoolでCTOをしている僕が、WordPressで開発された自社サービスを、副業メンバーと共に2ヶ月掛けてAWS+Laravel+Nuxt.jsにフルリプレイスした際の技術選定について書きます。

対象読者

  • Laravelを使ってみたい/使えるライブラリを一通り知りたい
  • AWS構築の全体感を知りたい
  • Nuxt.jsやVuetifyの使用感を知りたい
  • WordPressを脱却したい

技術選定の背景

技術選定と言っても好きな技術を選べばいいというわけではありません。自社が持っている技術力、事業の状況によるところが大きいため、まずは背景としてそのあたりを説明していきます。
先に技術が気になる方はここは読み飛ばして、あとで戻ってきてください:bow:

①自社の技術力

CTO @mejileben

NoSchoolは創業2期目で2019年6月現在、フルタイムメンバーが僕と社長しかいません。
そして社長は営業出身のため、僕が唯一のフルタイムエンジニアです。

新卒で入社した株式会社LIFULLを2019年3月に退職してNoSchoolのCTOを始めたのですが、その時点で経験していたのはざっくり下記のとおりです。

  • プログラミング:PHP、Ruby、Python、JavaScript、HTML、CSS
  • インフラ:EC2、CloudFront、EMR、S3、IAM、GCP、Firebase、Nginx、Apache
  • データベース:MySQL、Oracle Database
  • フレームワーク:Vue.js、Symfony、Rails(少し)、Sinatra

一番好きなのがフロントエンドですが、他も満遍なく捌けるので、このフェーズのスタートアップのCTOとしてはうってつけなんじゃないかと自負しています笑

副業メンバー @tyamahori@hanachandev1

毎週土曜日に副業で参加していただいているメンバーもいます。当時は@tyamahori@hanachandev1の2名でしたが、2人とも本業でLaravelを利用しています。

②自社の事業状況

来年にはなくなっているかも知れない会社

NoSchoolはプロの家庭教師や塾講師が無料で回答する勉強Q&Aサイトを事業として行っています。
スタートアップ企業としてはシードラウンドの資金調達を終えたばかりで、売上もほぼ立っていないので、投資家の方々から調達した資金だけで会社も僕も社長も生きています。

とりあえず新しい技術とか、使いたい技術を使うというわけではなく、事業展開のスピードに追いつけるような拡張性と柔軟性を持てるかどうか。採用で困ることはないか。といった観点で判断しています。

社長がWordPressで起業し、そのまま資金調達までこぎつけた

営業出身の社長は2017年に独学でドメインを取得、WordPressでQ&Aサイト用のテーマを購入しNoSchoolを起業しました。
そのままユーザー数を順調に増やし続けたことで2018年末に資金調達を成功、調達したお金で僕を雇用することができるようになったので晴れてフルタイムCTOとして転職という過去があります。
2018年の6月頃から僕を含め数名の副業エンジニアを採用したことで、一部のページは既にLaravelで構築していますが、メインの機能はWordPressのままでした。
しかもインフラはさくらのVPSでサーバー1台にソースコードもDBも画像も全部突っ込んでいるという有り様です。
CTOになった僕の最初のミッションがWordPress脱却(兼AWS移行)となりました。

ということで

副業メンバーの生産性を考えるとアプリケーションはLaravelで構築するのが吉と判断しました。2人とも週1日の稼働とはいえ、1秒でも速く移行するためには副業メンバーの稼働も大切です。スタートアップなので1時間の時給も、1日の工数も無駄にできません。
また、正社員が自分1人という状況なので、普段は誰にも質問せず1人で黙々とやります。となるとオープンソースかつエコシステムが完成しており、既存文献も多くあるフレームワークであるLaravelは問題解決の速度面で考えても魅力的でした。
インフラおよびフロントエンドに関しては、基本的に自分の独壇場になることが確定しましたが、こちらも文献が多くベターと判断できるものを選定していこうと思いました。

技術選定の内容

前述の状況を踏まえて、WordPress脱却にあたって選択した技術を順番に説明していきます。
WordPressを脱却した今、ここの技術を抑えたのがキーだったなと思う部分と、AWS/Laravel/Nuxtそれぞれ入門してみたい人に全体感が伝わる部分を中心に書いてみました。

  • AWS
  • Laravel
  • Nuxt.js

の順で書いていくので、気になるところからお読みください!

AWS

AWSの個々のサービスについて、概要や選定理由などを中心で説明します。詳細はまた別の記事で書ければと思うので、要望あればTwitterとかで連絡ください笑

ACM

HTTPS通信の証明書を発行したり、期限の管理ができるサービスです。
今回は既存サイトの移行で、社長がXServerでドメインnoschool.asiaを取得していたので、そのドメインの向き先をAWSに変えるにあたってはAWS側でも同ドメインのHTTPS証明書を取得しておく必要がありました。
後述するCloudFrontやALBに証明書を紐付けて利用するのですが、注意点として2019年6月現在、CloudFrontにはバージニア北部のリージョンで取得した証明書じゃないと紐付けることができないので注意してください。

スクリーンショット 2019-06-10 11.57.41.png

CloudFront

WAF(IP制限など)や画像のキャッシュ機構などを乗せることができるサービスです。
自社のドメインをCloudFrontに紐付けて、そのバックエンドにサーバーやデータベースを置くことで、手軽に独自ドメインのサービスをインターネット上に公開、キャッシュ等も乗せることが可能です。CSSやJavaScriptのgzip配信も対応しているので、ページの読み込み速度向上も簡単に実現できます。

後述するS3やALBなどを後段に置き、URLのパスごとに振り分けることができるのも特長です。
XServer側のDNSレコード設定でCNAMEにCloudFrontのデフォルトURLを指定し、先程発行したACMの証明書をCloudFrontに紐付けると、「noschool.asia」をHTTPSで利用できました。

スクリーンショット 2019-06-10 11.52.41.png

S3

今回はNoSchoolに日々投稿されている画像を全部置く場所として利用しました。
NoSchoolは勉強Q&Aサイトなので、日々ノートの写真などが投稿されており、その量は馬鹿になりません。旧構成ではさくらのVPSに1台のサーバーを立ててその中にソースコードも画像も全部置いていました。そのためサーバー自体がパンクする事件が勃発した過去があり、S3の選定は必須でした。

また、非常に難度が高かったのが、WordPress時代に投稿された画像のパスは例えばhttps://noschool.asia/wp-content/uploads/cd93a2eb47ba85d48717e8d2a7d62f4HOGEHOGE.jpgのように/wp-content/uploads配下にあるため、単にS3に置いてCloudFrontから向き先をS3に向けるだけでは、移行前の画像のパスが全部死んでしまうことです。

これはS3上で同じ構成のディレクトリを作りその中に画像を移行して、CloudFront側でも後段に置くOriginの指定時に同じパスを指定しておくことで解決しました。
スクリーンショット 2019-06-10 11.49.48.png

ALB

CloudFrontの後段に置いたALBはロードバランサではありますが、URLのパスベースでアクセスを振り分けることも可能な「ちょっと賢い」ロードバランサです。Nginxでやるようなことを代替するイメージです。

ALBの後段にEC2をぶら下げるのですが、EC2をターゲットグループというグループに配属させ、そのグループに対してロードバランシングを行います。

後述しますがNoSchoolではAWS移行後もページ単位でのNuxt移行を粛々と進めており、ページ単位でNuxtに移行するためにパスベースでアクセス先のインスタンスを振り分けています
ALBをロードバランサではなくリバースプロキシとして利用することでこれが実現できます。
noteさんがページ単位でNuxt移行をやっているという記事を読んで真似していますw

既にいくつかのページをNuxtに移行しているので、例としてALBのパスルールを一部貼っておきます(開発環境の設定です)。ポイントはNuxt移行したページのパスと、/_nuxt/*および/_loading/*をNuxt側のインスタンス(図内target-group-nuxt)に振るところです。
スクリーンショット 2019-06-10 12.00.10.png

EC2

月並みにセキュリティグループやAMIの運用をしているので、特段この記事で解説することは少ないなと思います。
セキュリティグループではHTTP/HTTPS通信をALBからのもののみ許可するとか、後述するCodeDeployやAutoScalingのことを考えながらAMIを作成するところに特に骨が折れました。

あと、NoSchoolのPV数ならサーバーのスペックに特にこだわらなくてもよかったのですが、Nuxt.jsを動かしているサーバーに関してはCPUのコア数/スペックに注意しておかないと、CodeDeployのnpm run build時にCPU利用率が100%に貼り付いて何度も落ちてハマったことがあるので注意です。

RDS

AWSでデータベースを構築するならまずRDSを選ぶと思います。EC2内にMySQLを入れるのとは違って、一度設定してしまえば基本的にあとはすることが無いです。フルマネージドのメリットを存分に受けることができます。

WordPress時代のDBがMySQLだったため、引き続きRDS for MySQLを選択しました。Auroraは明らかにオーバースペックなので選びませんでした。

Laravelとの組み合わせとしては、RDSのホスト名やユーザー名、パスワードを.envファイルに記載する(=環境変数に設定する)ことが必要です。またネットワークの観点ではRDSのセキュリティグループでEC2からのアクセスを許可する必要があります。
.envに秘匿情報を書いてGitHubにPushするのは悪手なので、後述するパラメータストアで秘匿情報を管理し、デプロイ時にそれらの情報を環境変数に設定することでアクセスする手法をNoSchoolでは取っています。

ElastiCache

ElastiCacheとLaravelの組み合わせについて書いている記事があんまり見当たらなかったのですが、本番で複数台構成のEC2を構築したとき、ログインユーザーのセッションを管理するのをそれぞれのサーバーでやってしまうと不都合があります。

具体的には、ログインはサーバーAで処理が行われたが、別日にアクセスしたときALBがサーバーBに振り分けてしまうと、ログアウトした状態になるということです。AWSを触りたての4月上旬頃にこの事象に遭遇して1人で相当焦った記憶がありますw

これを解消するために複数台のEC2でセッション情報を始めとする各種キャッシュを共通化する必要があり、キャッシュ版のRDSとも言えるElastiCacheの選定に至りました。
同じく.envに必要な情報を設定することで利用できますが、パッケージとしてpredis/predisを入れておく必要があります。

CodeDeploy / CodePipeline

概要

その名の通りソースコードをデプロイできるサービスです。今回は指定したEC2インスタンスにソースコードをリリースする用途で使っています。
CodePipelineを利用することで、GitHubとの連携が可能です。GitHubの特定のブランチにPushしたのをトリガーに指定したCodeDeploy設定を動かすことができるので、

  1. 本番用のEC2インスタンスを用意し、CodeDeployのデプロイ先に指定する
  2. CodePipelineからGitHubのリポジトリと連携し、releaseブランチへのPushをトリガーに1.で作成したCodeDeployを動作するように設定する

この2ステップでGitHubとデプロイを連携させることができます。
今となってはレビューOKとなったブランチをdevelopにマージすることで開発環境のAWSにソースがデプロイされ、masterにマージすると本番環境のAWSにソースが持っていかれます(ブランチの運用ルールがまだ適当なので見ないふりしてください)。
スタートアップのスピード感にとっては夢のような環境を作ることができます。

CodeDeployの設定方法

CodeDeployの設定方法をざっくりお話しますと、下記のとおりです。

  1. appspec.ymlというファイルを作成し、リポジトリ内のルートディレクトリに配置する
  2. appspec.ymlにリリース時に実行してほしいスクリプトを書く(例えばnpm installcomposer install

また、CodeDeploy実行時には、例えば下記の挙動をAWSが勝手にやってくれます。

  1. リリース対象のインスタンスを設定内容から特定する(インスタンスのタグを利用したり、Auto Scalingグループを指定することができます)。
  2. そのインスタンスがHealthyであれば、それをALBのトラフィックから外す(=ターゲットグループから抜く)
  3. appspec.ymlで【デプロイに実行する】と指定したスクリプトを実行する。例えばservice nginx stopなど。
  4. ソースコードをGitHubから持ってきて指定したディレクトリに置く
  5. appspec.ymlで【デプロイに実行する】と指定したスクリプトを実行する。例えばnpm installなど。
  6. ここまでノーエラーで終われば、該当インスタンスをALBのトラフィックに戻す(=ターゲットグループに戻す)

ALBと連携することで、ALBが自動でリリース中のインスタンスをトラフィックから外してくれるので、エラーが出ることはありません。

コンソールにはこんな感じで、各リリースのフェーズに名前がついていて経過時間や結果を観察することができます。
慣れないうちはこれを見ながら設定しましょう。
スクリーンショット 2019-06-10 17.27.25.png

厄介なところ

ここまで話すと、便利で至れり尽くせりなサービスだと思うことでしょう。。。しかし実際は関連するリソースが多すぎて、設定をミスすることが多いのです。まあ、僕が悪いのですがw

4月頭の自分がこんなツイートをしていましたw
https://twitter.com/Meijin_garden/status/1115178360508801024
スクリーンショット 2019-06-12 23.43.46.png

ツイートしたパターンを始め、もう記憶に残っていないほどハマりまくりました。社長が僕の沈んだ顔を見て「今日もAWSの設定で手間取ってたの?」「はい」という会話が定番化した時期がありましたw
ALBなどと連携していたり、シェルスクリプトも自前で書いたりすることで、予想外のミスをいつの間にかしていてハマることが多いので、実際に触る際は辛抱強く進めていただければと思います。

AutoScaling

EC2インスタンスを複数台まとめて登録しておくと、台数を指定した範囲内で自動で上下させてくれるサービスです。
AutoScalingの重宝できる部分は、今のNoSchoolのフェーズだと下記2点だと感じています。

  • 常に台数を一定に保ってくれるので、不調になったインスタンスを削除すると勝手に補充してくれる
  • CodeDeployと連携することで、補充したインスタンスが起動すると自動でソースコードをインストールできる

特に後者が感動した部分です。先日NoSchoolの本番インスタンスが突然反応しなくなり504を返していたのですが、とりあえず新しいインスタンスを作って差し替えると、自動でソースコードのインストールまでやってくれたため、基本的に眺めているだけで復旧作業が終わったことがありました。

Systems Manager(Parameter Store)

RDSのパスワードやIAMで発行したアクセスキーなど、サービスで利用することはあるけどAWS外には出したくない。Git管理などもしたくないというときに使えるのがパラメータストアです。

パラメータストアに保存した情報はAWS CLIのコマンドなどを利用して簡単に取り出せるので、CodeDeploy時のappspec.ymlに書き込んだコマンドによって、各インスタンスがAWS CLIを通してパラメータストアに保存したDBへの接続情報などを取得するように構成してみました。

全体構成

draw.ioでAWSの全体構成図を軽く描いてみました。
この他にもCloudWatchでError LogをLambdaを通してSlackに通知するなどもやっています。

Untitled Diagram.png

アプリケーション

Laravel

データベース内容の移行

WordPressのデータベースは全データを縦持ちしているという衝撃の構成だったので、全てのデータとその目的を洗い出し、ゼロベースでテーブル定義、リレーションを考え直しました。そして、LaravelのCommand機能を使ってDBの内容を一気に移行するコマンドを組み上げました。
Artisanコンソール

こちらがそのソースコードの抜粋です。

MigrationCommand.php
<?php

namespace App\Console\Commands;

use App\Models\Answer;
中略
use App\Models\User;
use App\Models\Wp\Taxonomy;
中略WordPressのDBからの組み換えなのでWordPress用のModelも作成)〜
use App\Models\Wp\Wtermrelation;
use DB;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Schema;

class MigrationCommand extends Command
{
中略
    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $this->info('Start');
        ini_set('memory_limit', '600M');

        $this->comment('followtable');
        $this->userFollowsData();
        $this->info('ok');

        $this->comment('migrate from wp_prefectures to prefectures table');
        $this->migratePrefectures();
        $this->info('ok');

        $this->comment('migrate from wp_users to users and insert metadata, prefectures');
        $this->createUsersTable();
中略
    private function migratePrefectures()
    {
        $data = new Wprefecture;
        $bar  = $this->output->createProgressBar(count($data->all()));
        $bar->start();
        foreach ($data->all() as $data) {
            $pref       = new MasterPrefecture;
            中略
            $bar->advance();
        }
        $bar->finish();
    }

NoSchoolの当時全てのデータを新しい構成に組み替える、合計1200行以上にも及ぶコマンドを副業メンバーの@tyamahoriさんが組み上げてくれました。

全体的な設計

Middleware

LaravelのMiddlewareは、NoSchoolでは例えば下記のようにエンドポイントへのアクセスを制限(認可)するために使っています。

  • ログインしているユーザーしか利用できないエンドポイント(勉強Q&AサイトであるNoSchoolの場合だと、質問を作成するエンドポイントなど)
  • 特定のユーザーしか利用できないエンドポイント(質問を編集、削除するエンドポイント)

LaravelのMiddlewareは、タイミングとしてはリクエストがControllerに到達する前に動作し、「そもそもそのControllerが動作するべきか」というチェックのために利用しています。
「1ユーザーが1分間に60回しか利用できない」ように制限したりログインセッションをチェックする機能があるので、app/Http/Kernel.phpを見ていろいろと調べてみてください。

例えば下記のコードを見れば、APIは1分間に60回までしか同一ユーザーが利用できないことがわかります。

app/Http/Kernel.php
        'api' => [
            'throttle:60,1',
            'bindings',
        ],

Nuxt.jsなどのSPAで利用するときは、APIを多く叩いてしまいがちのため、こちらの上限値は60より多めに引き上げています。

FormRequest

フォームからユーザーがPOSTした情報をバリデーションしたいときは、リクエストにFormRequestを使うのがオススメです。
フォームリクエスト

詳しくはドキュメントを読んでいただければと思いますが、どういったリクエストを許可するのか?をControllerから切り離せるのは再利用性や可読性の面で非常に便利です。Controller以降のServiceやModel層で、どんなリクエストが来ているのかを保証した状態でコーディングできるので見通しも良くなります。

ちなみに同様の処理はMiddlewareでもできそう、と思う方もいるかもしれませんが、そもそもMiddlewareはHTTPの話(CookieやSession)を扱うのがメインのレイヤーなので、リクエストの内容がアプリケーションの仕様に沿っているかどうかといったアプリケーション層の話になると別の仕組みを使うのがいいです。

API Resource

APIリソースはちょっとドキュメントがわかりにくいのですがSPAなどでAPIを大量に作るケースにおいてとても有用な機能です。
出来の良さに感心しました。

APIの仕様って、このキーにこの値を埋めて、このキーの子要素に関連するこの値を埋めて・・・といった感じで結構ややこしくなりがちだと思います。それをPHPで配列を都度都度mergeして返すみたいな処理を自力で書くとロジックがごちゃごちゃになりますよね。
APIリソースを利用すれば、見通しの良いコードベースでAPIの仕様を記述できて、あとはそこにDBから返ってきたCollectionなりLengthAwarePaginatorなり突っ込めばよしなに整形して仕様通りにしてくれるというものです。

気に入りすぎてLaravelの本体のソースコードまで見に行って、元クラスを好きに拡張して自社仕様にどんどん特化させていきました笑

全体感

全体的にLaravelを触って思ったのは「ControllerをFatにしないための工夫が尽くされている」ということです。
上記で紹介した3つの設計手法は、いずれもControllerに詰め込もうと思えば詰め込めるものの、そこからよく書きがちなロジックを別のスキームとして切り出してくれています。

LaravelでControllerが膨らみがちなとき、基本的には自力でどうにかするのではなく、ドキュメントを読んで代替手段を調べる。さらに代替手段を深く知るために必要に応じて生のソースコードを読む。これが重要だと感じています。

利用したライブラリ(抜粋)

実際に弊社で使っているライブラリをざっと書いていきます。ぜひ参考にしてください。

artesaos/seotools

TDKやOGP画像のための各種タグをテンプレートとして共通化しながら簡単に埋め込めるライブラリです。
NoSchoolは勉強Q&Aサイトと、そこに回答する家庭教師の先生方の検索サイトの両面の顔を持っているのですが、特に家庭教師の先生方のページは都道府県別に作られていたりして、SEOのmeta情報の管理が煩雑になってきていました。
当ライブラリの導入によって、ページごとのmeta情報が見通し良くなりました。

davejamesmiller/laravel-breadcrumbs

こちらもSEO関連なのですが、パンくず情報の管理ができるライブラリです。
routes.phpに近い感覚で、ルートごとにパンくずの内容を指定の書式で書くだけでパンくずを作ってくれます。json-ldも対応しているので助かりました。あれ生成するの面倒なんですよね・・・w

laravel/passport

PassportはLaravelで構築したサービスにOAuthの機能を統合できるライブラリです。

今iOSアプリ用のAPIを作っているので、主にそこでトークンの発行などに使っています。まあPassportをその手の目的のためだけに使うのはちょっと大げさではあると思いますが・・・

laravel/socialite

NoSchoolにはTwitterログインの機能があるので、このライブラリを使っています。

本当はFirebaseログインに移行したかったのですが、諸事情で諦めました。いつか移行したいです。

intervention/image

画像処理のライブラリなのですが、このライブラリにあるorientateという関数を利用することで、iPhoneから投稿された画像が横向きになる問題を解消できました。
経験のある人はすぐ思い当たると思うのですが、iPhoneから投稿した画像って、投稿後にWebで表示すると謎に横向きになったりするんですよね・・・
それを当ライブラリのorientateを利用することで防ぐことが出来ました。この関数を見つけて利用したのは完全に勘ですw
ImageMagickとか使わなくてよかったので助かったなと思いました。

league/flysystem-aws-s3-v3

AWS S3へのファイルの読み書きを行うライブラリです。ユーザーが投稿した画像をS3にアップロードする際に利用しました。

predis/predis

前述したAWS ElastiCacheを利用するためのライブラリです。

s-ichikawa/laravel-sendgrid-driver

勉強の質問に対して回答があったときなど、ユーザーにメール通知を行っているのですが、その際Sendgridを利用しています。

jenssegers/agent

リクエストのUser-Agent情報を読み取ってisMobile()などのメソッドを利用してリクエスト元のデバイス判定ができるライブラリです。
ただ、フロントエンドで僕がレスポンシブにCSSを組んでいたのに、いつの間にかサーバーサイドで当ライブラリを使ってPHP側でテンプレートを出し分けるように違うメンバーに実装されてしまった事件が起きたことがあり、Agentはレスポンシブとは違うというのはフロントエンドに特化していない人にはちゃんと伝えないとなと思いました笑

Nuxt.js

最後に、フロントエンド大好き人間である僕が意地で続けているNuxt.js移行についてお話します。

Nuxt.js移行の概要

AWS ALBの章でもお話しましたが、NoSchoolのViewは全てNuxtになったわけではなく、ページ単位でNuxtに移行しています。つまりあるページはLaravelでHTMLを生成して返す旧来の方法、あるページはNuxt.jsでレンダリングしLaravelで実装したAPIでデータを取ってくるSPA/SSRな方法で生成するというように分けています。

NoSchoolをWordPressから脱却しAWSに全て載せ替えるという話になった当初、インフラをAWSに載せ替え、アプリケーションをLaravelに載せ替えるのならば、フロントはjQuery+Bootstrapから脱却してNuxt.jsに移行したいものだと思いました。
しかしスタートアップとして事業展開を1秒でも遅らせるわけにはいかないという背景もあり、あとからAWSに載せ替えたりあとからDBを移行したりあとからアプリケーションをLaravelに移行するのはほぼ不可能だが、フロントだけあとからNuxtに移行するのはできるのではないかと考えて、Nuxt.jsへの移行はWordPress脱却とは別枠で、AWS移行後に部分部分で移行させていく方針にしようと判断しました。

また、NoSchoolは事業特性としてSEOで集客をしているので、下手に移行してHTML構造が大幅に変わることで、SEOおよび事業展開に悪影響を及ぼすリスクがあること、また、SSRするとしても僕自身に本番Node.jsサーバーの運用経験が無かったことなども加味して、AWS+Laravelに移行しながら、ページ単位でNuxtに移行していき様子を見ながら進められたら理想だなと考えました。
そこでnoteさんがページ単位でNuxt移行をやっているという記事を発見し、
記事の内容から自社でできそうなことを真似しながら進めているところです。

Nuxt.jsへの移行状況

2019年6月17日現在、NoSchoolサイト内で最もNuxt移行が進んでいるのは、家庭教師の先生方の検索機能です。
https://noschool.asia/teacher/home

このページから都道府県や先生がQ&Aで回答経験のある科目などで検索ができるのですが、それらの検索結果のページや、個々の先生の詳細ページは全てNuxtで構成されています。

SEO対策も意外と簡単にこだわれる

SEO対策として基本的なものである、<head>タグのmeta情報をページごとに変えることや、パンくずリストの作成、ページネーションの作成、json-ld情報の作成もそこまで難しくはなかったです。
json-ldの作成にはnuxt-jsonldを使わせていただいています。

ただ、どうやって複数ページで共通の仕組みを組み込むかというのが自分の中であまり固まっていなくて、今はhead情報を扱うmixinやパンくずリスト(と対応するjson-ld)を作成するpluginを適宜作成して、必要なページでそれらを読み込むという運用にしています。
実際問題、component側は細かく切り分けることでコード数を削減できますが、ページごとにmeta情報が異なることでその部分のコードが相対的に多くなりがちで、これでいいのかなとは思っています。ページごとにjsonで管理するような専用のpluginを別途作成し、ページのroute名をキーにしてmeta情報を引っ張ってこれたりする仕組みを作るといいのかなぁとか、いろいろ考えてます。誰か教えてください笑

ページネーションについては工夫が必要で、watchQueryプロパティを設定し、URLのクエリパラメータで変更が起きるところを指定しておく必要があります。これをやっておけば、ページネーションをユーザーがしたときにクエリパラメータに?page=3などを追加するだけでasyncDataから再実行してくれます。

ちなみにLaravelでAPIを作るとき、APIのクエリパラメータにpage=X形式でパラメータを渡し、最終的にLengthAwarePaginatorを返すように実装すれば、自動でページネーションされたデータを返してくれるので便利です。

下記はNoSchoolで勉強Q&Aの回答一覧を返すメソッドですが、最後に->paginate()しておりLengthAwarePaginatorを返していることがわかります。

    public function searchAnswer(array $params, Int $take): LengthAwarePaginator
    {
        $query = $this->answer->with([中略]);
        中略ここにEloquentクエリビルダでいろいろと)〜
        return $query->paginate($take);
    }

LengthAwarePaginatorのインスタンスを返すようにAPIを作成していれば、フロントエンド側ではpageを含むクエリパラメータを投げるだけでページネーションされたレスポンスを返すので非常に便利です。

      app.$axios.get("/answer/" + params.id, {
        params: {
          ...query // asyncDataの中だとクエリパラメータをqueryという変数で利用できる
        }
      })

ページネーションされたレスポンスなので、page=Xで指定した範囲のデータだけ返ってくるだけではなく、レスポンスのmetaキーの中にトータルのページ数なども返ってきています。

トータルのページ数を利用して、ページネーションのコンポーネントにその値を渡して、レンダリングに使うことが出来ます。

Pug

PugはHTMLを書きやすくする記法で、PugをHTMLに変換できるライブラリをnpm installしてあげればすぐに使うことが出来ます。
ページ単位でPugで書いたりHTMLで書いたり分けられるので、導入障壁も低いです(これはVue自体の特色ですね)。

たぶん、まずイメージを掴むために下記のサイトを見てみればいいと思います。どう書きやすくなるかざっくりわかるかと。
https://html2jade.org/

閉じタグを書かずに全てインデントで表現したり、classやidの記法がjQueryに近かったり、慣れると離れられなくなる書き方です。
エンジニアがPugで書いても、Nuxtでビルドするときに素のHTMLにちゃんと変換されるので、サイト上の速度とかには全く影響しません。

SCSS

SCSSはCSSを書きやすくする記法で、CSS上で変数が使えたりネストして書けたり分岐や繰り返しもできる優れものです。
しかし後述するVuetifyを使っていることで、SCSSを書くことはほぼなくなりました。

Vuetify

VuetifyはUIフレームワークです。UIフレームワークというのは、デフォルトである程度常識的なデザインを当てたパーツ(ボタンやヘッダーやテキスト入力など)を用意してくれているフレームワークです。

古くはBootstrapというものが有名でしたが、VuetifyはVue.jsでの利用に特化しており、下記のように独自のHTML要素かのように利用することができるのが特長です。

<v-btn color="success">Success</v-btn>

例えばButtonコンポーネントであるv-btnでいうと、デフォルトでホバーしたときにカーソルが変わるとか、余白とか、押したときの色が変わるとか、そういった常識的なボタンだとそのCSS当たっているよね、というものが既に当たっている状況で提供されているので、デザインに特別なこだわりがなければかなりの速度で開発を進めることが出来ます。

特に今のNoSchoolのようなフェーズだと、ホバーの色の当たり具合がをRGBで絶妙に調整すると言うよりは、そもそもページ内の情報設計がこれでいいんだっけ、というところから常に見直していかないといけないため、Vuetifyでディテールを常識的なものに持っていってくれるのは大変ありがたいです。

とはいえまだメジャーバージョン2が出たあたりで、Vuetify本体のアップデートが頻繁に行われていますので、細かいバグやドキュメントの内容がおかしいところなどが散見されるので、随時チェックしていくことが必要です。

https://twitter.com/Meijin_garden/status/1132910380391358466
スクリーンショット 2019-06-17 11.13.55.png

NoSchoolの先生検索機能(https://noschool.asia/teacher/home )は数行しか自力でSCSSを書いておらず、ほぼ全てVuetifyの機能で賄っています。
Vuetifyだけで戦った事例として、これくらいのページなら作れるんだという確認にお使いいただければと思います笑

自分がVuetifyで書くときの流れとか、気がついたことをざっと箇条書きで書いておきます。

  • 基本はv-layoutv-flexで構成する。縦並びはv-layout(column)
  • paddingはpa-2pa-5で原則統一する(数字は何でも良いのですが、全体として統一感を持たせることが大事。marginも統一してます)
  • 仕切り線はv-divider
  • 画像は原則v-imgだが、画像によってはデコードに失敗して警告を吐くので、単なるimgタグを使うこともある(誰かこの解決策教えてほしい)
  • v-chipv-cardはかなり使える。v-cardはカードとしての見た目だけではなく影を消すこともできるため、単にテキストを囲った見た目を作るときにも便利
  • ページネーションはv-pagination、パンくずリストはv-breadcrumbを使う。パンくずはそのまま使うのではなく内部のslotを拡張して独自デザインを当てることになると思う
  • Vuetify自体を拡張したり、機能を限定したコンポーネントを別途componentsディレクトリ内に一通り作っておくといい。じゃないと人によって使い方や書き方が分かれてしまいがちになる
  • コンポーネントを作るときは粒度を意識して、最低限の機能のものはpartsディレクトリへ、partsの組み合わせで構築したものはmodulesへ。といった粒度別の切り分けをやっておくと再利用性を担保しながらコンポーネントを増やせる

例えば序盤で、『「アイコンとテキストが書かれたボタン」を今後たくさん使うな』と思ったら、下記のようなコンポーネントを作っておくと便利かと思います。どういった粒度が良いのかはまだ模索中ですが。

client/components/parts/button/outlined.vue
<template lang="pug">
v-btn(outlined large :color="color" :to="to" @click="clicked" :block="block").py-2
  v-icon(v-if="icon" :class="iconColor + '--text'").subtitle-2 mdi-{{ icon }}
  span {{ text }}
</template>

<script>
export default {
  props: {
    to: Object,
    block: Boolean,
    color: {
      type: String,
      default: 'primary'
    },
    icon: String,
    iconColor: {
      type: String,
      default: 'primary',
    },
    text: {
      type: String,
      required: true,
    },
  },
  methods: {
    clicked() {
      this.$emit('clicked')
    }
  },
}
</script>

SSRの本番運用

SSRの本番運用は現在EC2上でnpm run startして利用しています。本当にシンプルです。
いずれはLambdaに移行して、APIのエンドポイントをAPI Gatewayで作るなどしてLaravel側を完全なAPIサーバーにしたいなと野望を抱いています。

その他

開発環境はDockerを利用しています。
docker-composeを利用して、HTTPS通信を使えるように設定したり、Nginxのコンテナで本番AWS環境のALBと同じ挙動を実現させたり、バックエンドにPHPのコンテナとNuxt.jsのコンテナを別立てしてそれぞれ動作させたり、更にそのバックエンドにMySQLのコンテナを立てることで、かなり高い精度でAWS環境に近づける工夫をしています。

特にNginxにALBの機能を真似させることで、ページ単位でNuxt.js移行しているときにローカルの環境をAWS上の状況と一致させるのにはかなり苦労しました。しかしCTOの僕がフロントエンド大好き人間なので、こういった設定を乗り切ることで思う存分Nuxt移行を進められているので嬉しい限りです笑

また、Docker for Macが遅いとよく噂になっていますが、実際遅いのですが余分なvendorなどのディレクトリをマウント対象から外したり、:cachedを使うことでギリギリ使うのに耐えうる遅さになっているので、今のところはまあこのままでいいかなと思っています(docker-syncはうまく動かなくて心が折れた記憶がある)。

移行当日

なんと移行当日、たった2時間程度のメンテナンス時間でNoSchoolのサイトはWordPressからAWS環境に移行できました
やったことはデータベースの内容を全てmigrationしてRDSに突っ込み、画像を全てS3にコピーし、noschool.asiaの向き先をCloudFrontに変えることで予め構成していた本番用のAWSに向けたという大きな引越しなのですが、アプリケーション面で致命的なエラーも起きませんでしたし、例えばGoogle Analyticsもしっかり動いていました。
ドメインのCNAMEレコードをさくらのVPSからCloudFrontに変えたものの、数時間で各メンバーのデバイスにまで新しい方のページが表示されたので、意外とあっさり終わりました。
とはいえ裏では.envに指定するドメイン名などもStaging用のものからnoschool.asiaに変えるといった細かいところまで手順書にまとめて実行したので、偶然ではなく緻密に計算した上で成功したなと思っています。
社長もいつも本気でデバッグして大量のバグを発見してくれましたし、副業メンバーも気づいたことを随時Slackに書き込んでくれたおかげで致命的なミスを防げたので、小さなベンチャーが総力戦で技術的な自由を勝ち取ることが出来た日でした

総括

ここまでお読みいただきありがとうございました。

WordPressを脱却した今、ここの技術を抑えたのがキーだったなと思う部分と、AWS/Laravel/Nuxtそれぞれ入門してみたい人に全体感が伝わる部分を中心に書いてみましたが、ちょっと書きすぎましたかねw

移行中はスタートアップなのに移行中のせいで施策が打てないことが本当に辛く、Qiitaを書くどころではなかったため、移行が終わってこうやってまとめて知識を発散できて嬉しいです(?)

好評だったら続編として「マネジメント編」「Nuxt.js移行ドタバタ編」など書きます:muscle:

お願い

NoSchoolは勉強Q&Aサイトと家庭教師検索サイトの両方の顔を持つ捻った事業モデルで、賭けの部分が大きいですがかなり練られた戦略を以って資金調達しています。副業メンバーも状況によりますが随時募集しているので、ぜひ興味があれば僕のTwitterWantedly経由ででもご連絡ください!

あと、僕がいつもフルタイム1人でやっていて寂しいし、技術力の伸びが限られてくるので、一緒に勉強していただける方、技術の相談ができるSlackグループに入れてくれる方などいらっしゃればぜひTwitterに連絡いただければ大変嬉しいです!ぜひよろしくお願いいたします。

mejileben
NoSchoolという教育ベンチャーでCTOを務めています。好きなプログラミング言語はTypeScript。好きなCSSプロパティはtransform。好きなAWSのサービスはLambdaです。 経歴は奈良高専卒→LIFULLでHOME'SのWebエンジニアを3年→2019年NoSchoolのCTOに転職。 技術スキルはWebフルスタックで、SEOやUIデザインもよしなにカバーします。
https://meijin.me
noschool
中高生向けのオンライン家庭教師サービス”NoSchool”を開発・運営しています。2018年創立のベンチャー企業です。
https://noschool.asia
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした