Blog

ブログ

Nuxt.js + microCMS + VercelでJamstackな個人サイトを作りました

「プロフェッショナルWebプログラミング Vue.js」を読了したので、最終章のおさらいとしてNuxt.jsベースのJamstackサイトを立ち上げました。

MinecraftのYouTube動画倉庫 - マインクラフトムービーズ | minecraft-movies

コンテンツ管理はヘッドレスCMSのmicroCMS、デプロイしているホスティングサービスはVercelです。どのように構築を進めたのか、現時点でどこで詰まっているのかなどを書いておきます。

データ設計

ほぼ毎日、お昼休みにYouTubeでMinecraft関係の動画を見ています。ですがYouTubeはプレイリストと高評価以外に動画をアーカイブする方法がないので、過去に観た面白かった動画を見つけられなくなってきました。

コンテンツを消費してばかりの現状も嫌なので、動画をタグ付けして紹介する軽いブログでも書こうかと考えていましたが、「あれ?これ、今の勉強の土台に使えないかな?」と、Jamstackで(冗長にもほどがありますが)構築することにしました。

設計としては、動画紹介用のデータ群と、動画を紹介し簡単な説明をつけるタグ用のデータ群が必要になります。microCMSで作ったAPIのスキーマは以下のスクリーンショットのとおりです。microCMSはフリープランでも1対多、かつ順番指定が可能な関連付けをすることができます。

動画紹介APIのスキーマ

タグAPIのスキーマ

Nuxt.jsでテンプレート・コンポーネント作り

続いて、Nuxt.jsで静的なサイトをマークアップしていきました。CSSフレームワークは自作の「echo.css」を使いました。Nuxt.jsに使うのははじめてでしたが、特に直しが必要になることもなく、かっちりはまってくれたのでホッとしています。

echo.cssのコンポーネントが、そのままNuxt.jsのコンポーネントになります。パーツをcomponentsディレクトリに切り離し、配列を渡すと各コンポーネントで勝手に表示してくれるよう、本を読み返しながら作っていきました。作成したコンポーネントは以下のとおりです。

  • ヘッダ
  • フッタ
  • サイト紹介
  • ページヘッダ
  • 動画一覧
  • 本文
  • タグリスト

動画一覧のコンポーネント「PostsListCard」のコードは以下のとおりです。コンポーネント別に追加CSSを書けるのをいいことにけっこう雑なコーディングをしています。

PostsListCardの呼び出し部分は以下のとおりです。配列moviesを渡すと各動画へのリンクを繰り返し表示します。

基本的にサムネイルは動画のアイキャッチ画像ですが、数年前の動画だと大きなサムネイルがないことがあります。その場合は自分で切り抜いて登録できるように、アイキャッチ画像の有無判定を入れました。

<div class="echo-cards">
    <posts-list-card
        v-for="(movie, index) in movies"
        :id="movie.id"
        :key="index"
        :eyecatch-sm="movie.eyecatchSm ? movie.eyecatchSm : null"
        :eyecatch-sm-url="movie.eyecatchSm ? movie.eyecatchSm.url : null"
        :youtube-id="movie.youtubeId"
        :title="movie.title"
        :date="movie.createdAt"
        :time="movie.time"
    />
</div><!-- /.echo-cards -->

microCMSの画像がないときの仕様

microCMSの画像フィールドは、内容が空だったときは空のオブジェクト(null)を返すのではなく、オブジェクトそのものを定義しません。一般の有無判定である :eyecatch-sm=”movie.eyecatchSm” だと、undefindだった場合に正しく判定できないので、式を厳密にしました。
movie.eyecatchSm !== undefined ? movie.eyecatchSm.url : null の方が確実なのかもしれません。まだよくわかりません。

アーカイブマッピング

ここでmicroCMSに何件か記事を投稿し、APIを読み込んでサイトとして表示できるようにしてみました。

アーカイブマッピングについては、動画紹介の詳細ページは /post/{動画紹介のID}/ なので難しいことはありませんでした。

タグ一覧についてはIDではなく、/tag/{スラッグ=タグの英語名}/ としようと思っていたのですが…このようなURLにするには3段階の処理をしなければなりません。

  1. 今表示しているページのパスを拾って、APIに「スラッグがパスと同じタグの情報」をリクエストする
  2. 取得した情報からタグのIDを拾って、APIに「タグにこのIDを指定している動画紹介一覧」をリクエストする
  3. 表示する

1が終わってから2の処理をする方法がわからなかったので、今回は妥協してタグ一覧のURLは /tag/{タグのID}/ としました。

タグリストを作る

トップページにタグリストを表示してみて、はたと気付きました。動画をひとつも関連付けていないタグは除外しなければなりません。microCMSのAPIでは関連付け情報を持っていないのでフィルタできません。WordPressやa-blog cmsだと勝手にやってくれているのですが。

仕方がないので、動画一覧とタグ一覧の配列を渡すと、比較して使用しているタグだけを返すフィルタを作りました。動画が数百件とかになったらどうしようという問題は残りました。

マークダウン

本文にHTMLをそのまま出力する v-html を使用すると、XSSで攻撃されるよ!とESLintに怒られます。私しか投稿しないのであれば無視してもいいのですが、いずれにしても本文にリッチテキストは使いたくなかったので、markdown記法で書いてフィルタでHTMLに変換するようにしました。変換ライブラリはmarkedを使用しています。

markedjs/marked: A markdown parser and compiler. Built for speed.

本文のところは以下のように書きました。やっぱりv-htmlにしなければなりませんが、かなり安全です。

<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="$options.filters.markdown(body)"></div>

動画一覧のランダム表示

最後に、トップページと詳細ページ本文下の動画一覧を、ランダム表示にしようとしました。50件くらい取得して、computedで配列をシャッフルすればいいんでしょ?…と思ったのですが、サムネイルの順番はシャッフル前のままで、動画一覧だけ更新されてしまいます。説明が難しいのですが画像のとおりです。

おそらくアイキャッチ画像のsrc属性のv-bindも関数にして監視させなければならないのかと思いますが、時間がかかりそうだったので、APIから読み込んでdataに渡す前にシャッフルすることにしました。

なお、dataに渡す前にシャッフルしてしまうと、ローカル環境ではブラウザを更新するたびにシャッフルされますが、Vercel上では静的になっているのでデプロイ(=GitのpushかCMSの更新)をしない限りシャッフルされません。これに気付いたのはサイトができてからでした。

デプロイ

サイトができたので、VercelにGitHubアカウントとmicroCMSを連携してデプロイしました。本で紹介されていたNetlifyの表示が遅いとぼやいていたところ、著者のぐっちーさんに「最近はVercelの方が早いようだ」と教えてもらった経緯があります。

びっくりするくらいわかりやすくて簡単でした。せいぜい、「BUILD COMAND」欄を npm run build ではなく npm run generate に書き換える必要があるくらいです。サブドメインもcnameで簡単に設定できました。echo.cssのサイトもNuxtに書き換えてVercelに移行してしまえばいいのではと思いましたが、Next.jsでの構築も勉強したいのでぐっとこらえました。

何もしてないのに壊れました

こうして「マインクラフトムービーズ」は、とりあえず公開できました。当初は10時間くらいでの立ち上げを目指しましたが、タグやシャッフルの件で苦戦して最終的に20時間くらいかかってしまいました。

実務レベルにはほど遠いです。「何もしてないのに壊れました」的な課題が山ほどあります。そもそもデプロイとは何なのかまだよくわかりません。ですがこのサイトの手直しは保留して、勉強を先に進めようと思います。Next.jsやES2016以降の仕様の理解の方が重要ですし、スキルアップしたことで、すんなり問題が解決するかもしれないからです。

改めて感じるのは、WordPressなどのブログ系CMSの便利さです。開発者の皆さんには、足を向けて寝られないですね。