headlessCMSのstrapi をnuxt.jsで静的書き出しを行う。その1:strapiとnuxtの連携
技術スタック JavascriptNuxt.jsHeadlessCMS

headlessCMSのstrapi をnuxt.jsで静的書き出しを行う。その1:strapiとnuxtの連携

2020.11.03

こんにちはjunです。新しいCMSを採用してみようという動きが社内であり、その過程で私がstrapiというheadlessCMSとnuxt.jsを用いて静的サイトを作成するとこまでやりました。

JAMstack構成でCMSを用いたwebサイトを作成できるということもあり、今までのCMSに変わっていく予感があったので調べてみました。シリーズ記事にして以下

  • headlessCMSとは何か、なぜ使うのか
  • strapiの使い方
  • strapiとnuxtの連携方法
  • strapiでブログライクなデータ構築
  • nuxtでの構築
  • 静的書き出し

の6つを中心に説明していきます。また完成したフロントとバックのソースはgithubに上げる予定です。今回の記事では上3つを行います。

なお解説で登場するアプリケーションのバージョンは以下の通りです。

  • node.js 13.12.0
  • strapi 3.2.5
  • nuxt 2.14.6

headlessCMSって何?

headlessCMSのheadというのはビューのことを言います。ビューは今あなたが見ているこのサイトの見た目そのもの、HTMLのことです。そしてその ビューの構築がCMSから切り離されているのが、ビューの生成処理が存在しないのがheadlessCMSです。

wordpressなどの一般的なCMSはビューの生成をwordpressという1つのシステムで行っています。リクエストに応じて対応するデータを引っ張ってきて、HTMLを生成してレスポンスとして返します。

しかしheadlessCMSのビューは 独立したフロントエンド プロジェクトを立てて、そのプロジェクトからAPIを呼び出してデータを取得して、主にjsを用いてHTMLをレンダーします。

どうしてヘッドをレスするの?

例えばwordpressの場合はHTML構造とデータの出力をwordpressというPHPシステム1つで行います。つまりフロントエンド の構築をPHPで行います。しかしその場合VueやReactを用いたフロントの構築は難しいですし、フロントはwordpressのシステムに従った構築を行う必要があります。最近のUIが優れたサイトや、生産的にフロントを作る場合は大変です。

できるだけフロントはJSで構築したい!でもデータもバックから呼び出したい!そんな時にheadlessCMSを用います。 フロントがCMSから分離することでバックエンドの都合や制限を受けにくくなります。(いわゆる疎結合な状態)

例えばwordpressのテーマ構築にはindex.php、page.php、header/footer.php、single.phpと言った決まりきったビューファイルがあります。フロント構築はこの制限されたファイルを上手く使用する必要があります。デザインの都合や仕様によっては開発が困難になったりします。

さらに Nuxt.jsやNext.jsを用いることで静的書き出しを行うことができます。 静的に書き出すことでCMSの課題であった不正ログインや攻撃のリスク、そしてメモリ消費による処理能力ダウンを防ぐことができます。(ただし構成と運用によります)

  • jsフレームワークを用いてリッチで生産性高くフロントエンド を開発できる。
  • 静的書き出しを行うことでセキュリティやパフォーマンスをあげることが可能
  • バックエンドへAPIを送ることでJSONでデータを受け取れる。JSONならば様々な媒体へデータを配信できるので、再利用性が高い(ブログの内容をスマホアプリに使用するなど)

上記の様な構成を JAMStack、javaScript API Markup Stackといいます。

strapiとは

headlessCMSはまだ登場してからまだ日が浅く、また種類も多いです。その中でも特に有名なのはContentful、strapiそして日本ではmicroCMSが人気です。

strapiはheadlessCMSの中でもオープンソースでありバックエンドを自由にカスタマイズ可能です。 ContentfulとmicroCMSはそれらの会社がバックエンドをホスティングしており、指定のAPIキーとエンドポイントへAIPを送ります。その分お金はかかりますが、アーキテクチャによっては完全なサーバレス構成が可能になります。

今回の記事では カスタマイズ性に優れたstrapiを用いて説明を行います。

アプリの構成

今回の説明では以下の図の様に バックエンドにstrapi、フロントエンド にnuxt.jsを用いて構築した上で静的書き出しを行います。 ローカルのプロジェクトでバックとフロントはポートを分けて実質別のサーバの様にします。そしてデータベースは用意が面倒だったのでMAMPのmysqlを使用しています。

そしてお知らせの様な定型の項目に沿って入力するコンテンツだけでなく、今のCMSの様に自由に項目を入力できる様なページも作ろうと思います。

strapiとnuxtのインストール

バックエンドのstrapiをいれる

まずは適当にフロントとバックをいれるheadlessCMSディレクトリを作成

~ % mkdir headless && cd headless

(公式サイト)[https://strapi.io/documentation/developer-docs/latest/setup-deployment-guides/installation/cli.html#step-1-make-sure-requirements-are-met]にもある様にnpxを用いてstrapiをインストール。カスタムモードで行います。

strapinpx create-strapi-app backend
? Choose your installation type Custom (manual settings)
? Choose your default database client mysql
? Database name: strapi
? Host: 127.0.0.1 (DBのホスト先)
? Port: 8889 (DBのポート)
? Username: (DBのユーザー)
? Password: (DBのパスワード)
? Enable SSL connection: No(開発環境だから)

Creating a project with custom database options.
Creating files.
Dependencies installed successfully.

headless % cd backend && npm run develop

strapiユーザーを作成する

(http://localhost:1337/admin)[http://localhost:1337/admin ]ビルドをすると1337のポートが開くので指示されたURLにアクセスします。すると以下の様なユーザー作成画面が表示されます。最初に作成されるユーザーはスーパーユーザーになります。

ここで任意の名前、アドレス、パスワードを設定します。「READY TO START」を押してログインします。

最近になって日本語が当てられる様になったらしいですが、ほとんどが英語のままです。日本語表示はあまり期待しない方がいいです。とりあえずバックエンドはこれでインストール完了です。

nuxt.js のインストール

ではフロントを構築するnuxt.jsを入れましょう。

headless % npx create-nuxt-app frontend

静的に書き出すのでUniversalモードでデプロイターゲットは Static を選びましょう。UIは作るのが面倒なのでbootstrap使います。あしからず。

? Project name: frontend
? Programming language: JavaScript
? Package manager: Npm
? UI framework: Bootstrap Vue
? Testing framework: None
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Static (Static/JAMStack hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? What is your GitHub username? jun
? Version control system: Git

Successfully created project frontend

headless % cd frontend && npm run dev

npm run dev で開発サーバーを立ち上げます。localhost:3000 にアクセスするとお馴染みのnuxt.jsの画面がみれます。

コンテンツタイプを作成する

フロントにデータを表示しようにも、バックにデータがなければ何も始まりません。まずはstrapiで表示するデータを作成します。そこでstrapiで「コンテンツタイプ」というものでコンテンツを定義してデータの型を作成します。

とりあえず以下の様なカラム を持つコンテンツ「お知らせ」を作ります。

  • タイトル
  • 内容(リッチテキスト)
  • サムネイル

headlessCMSではまず最初に「どんなデータ構成を持つコンテンツを作るか?」ということを考えます。コンテンツを構成する要素を洗い出し、ユーザーが登録する項目を決定します。strapiでの操作はまず画面左の「Contents-Type Builder」をクリックします。画面が切り替わり、「コンテンツタイプ」と書かれているとこの「Create collection type」をクリックして追加します。

するとコンテンツタイプの名前をいれる様に言われますので、入力します。「お知らせ」なので「newsItem」としておきます。(なぜかタイトル名を「news」にすると400エラーになってしまいます。予約されている可能性あり。)

「続ける」をクリックすると次は項目とそのデータ型を登録します。

タイトル→文字(text)、内容→リッチテキスト(Rich Text : Long Text)、サムネイル→画像(Media)とします。例えば「タイトル」は以下の通りです。

Nameに入力された値はカラム名となるので日本語入力できません。引き続き入力する場合は「+Add another filed」で続け、項目の設定を終了する場合は「終了」を押します。ひとまず以下の様に設定します。

そして「保存」を押すとこのコンテンツタイプが登録されます。ターミナルをみてみると

[2020-11-02T16:43:29.201Z] info File created: /Users/jun/headless/backend/api/news-items/config/routes.json
[2020-11-02T16:43:29.201Z] info File created: /Users/jun/headless/backend/api/news-items/controllers/news-items.js
[2020-11-02T16:43:29.201Z] info File created: /Users/jun/headless/backend/api/news-items/models/news-items.js
[2020-11-02T16:43:29.201Z] info File created: /Users/jun/headless/backend/api/news-items/models/news-items.settings.json
[2020-11-02T16:43:29.201Z] info File created: /Users/jun/headless/backend/api/news-items/services/news-items.js
[2020-11-02T16:43:29.588Z] debug POST /content-type-builder/content-types (650 ms) 201
[2020-11-02T16:43:29.614Z] info The server is restarting

content-typeを作成するためのAPIが POSTされていることがわかります。実はstrapi内でもこの様にREST APIで呼び合ってコンテンツの編集を行っています。

感覚的にはテーブルを作成

コンテンツタイプ作成の手順を一通りみてみるとDBでテーブルを作成したり、MVCフレームワークの「モデル」部分の実装と操作が似ています。というか同じです。コンテンツタイプでデータ項目と型を決定します。さらにstrapiではこのコンテンツタイプに応じたエンドポイントも作成してくれます。

headlessCMSはこの様に

  • データテーブルとそのカラム・データ型の作成
  • エンドポイントの自動作成
  • エンドポイントの権限管理
  • 自動マイグレーション
  • コンテンツタイプ(モデル)に応じたレコードの挿入、削除

これらを提供してくれます。

お知らせを1件登録する

では早速レコードを1つ登録してみましょう。コレクションタイプ(画面左上)に「newsItem」が登録されているので、それをクリックします。すると以下の様な一覧画面が表示されます。

レコードを登録するために右上の「newsItemを追加」をクリックします。すると先程コンテンツタイプ作成で定義した項目の入力欄が出現します。そこに値を入力します。

テキストエリア はプレーンテキスト、リッチテキストはマークダウンで入力します。画像を定義した箇所はファイルアップローダーが起動するので、ファイルをアップロードできますし、アップロードしたものを選択することもできます。

内容が入力し終わったら画面右上の「保存」をクリックします。そして保存終了後に隣の「Publish」をクリックしてこの登録したレコードが外部に公開できる様になります。

このPublishをクリックしないと、このデータをAPIで呼び出しても404が帰ってきますので注意。

公開するためにもう一歩

現段階ではまだ先程登録したnewsItemは外部から呼び出せません。「設定」から「権限とロール」をクリックします。権限とロールでは公開するコンテンツタイプやPOST、 PUT、DELETEの権限設定を行えます。

初期では「Authenticated(認証ユーザー)」「Public(匿名・全てのリクエスト)」のロールがあります。Publicをまずクリックすると、それぞれの権限設定が表示されます。

登録したnewsItemの設定があります。ここで findとcount、findoneにチェックを入れます。そして「保存」を押します。 チェックした3つは読み取り専用です。ここでcreateやdeleteにチェックをいれると、APIを通じて誰もがデータを操作できてしまうので気をつけてください。

逆にstrapiで登録したユーザーが外部からデータを操作する場合は Authenticatedでチェックを入れます。私は面倒なのでAuthenticatedは Select all にして、Publicは読み取りのみにしています。

ちなみにロールは3つまで無料です。3ロール以上はなぜか課金が必要です。

APIドキュメントプラグインをインストール

フロントの構築に移る前にAPIドキュメントプラグインを入れておきます。このプラグインは最初から入っておらず、「マーケットプレイス」から無料インストール可能です。このプラグインをいれるとコンテンツタイプに応じたAPIのエンドポイント一覧ドキュメントを自動で作成してくれます。

メニューにDocumentationが現れ、その画面から「Open the Documentation」でドキュメントが開きます。登録したnewsItemのAPIもあります。

フロントから呼び出してみる

ドキュメントをみてみると

  • /news-items で一覧
  • /news-items/count で総数
  • /news-items/{id} でidで紐づいたデータ

を取得することができます。idはstrapiのnewsItemの一覧画面で見れます。数字で表されます。さっき例で作ったのは最初なので /news-items/1 で取得できます。

Talend API Tester でテスト

とりあえずAPIがきちんと呼び出せるか、内容が取れるかが確かめたい場合はTalend API Testerなどを使用してAPIをテストすることができます。以下の様に http://localhost:1337/news-items/1 に対してAPIを投げてみると先程の登録した内容がレスポンスにJSONで戻ってきました。

Nuxtでの構築もこのJSONを元に作成します。

strapi nuxt moduleをインストール

nuxtでAPIベースの呼び出しを行う場合、呼び出し用のプラグインを自作することがあります。例えば以下の様な感じです。

plugins/axios.js
export default function(context,inject){
    // axios インスタンス
    const api = context.$axios.create({
        timeout:5000,
        headers:{
            'Content-Type': 'application/json',
            'X-Requested-With': 'XMLHttpRequest',
        }
    })

    // APIの呼び出し先を設定
    api.setBaseURL(context.env.apiBaseURL);

 
    api.onRequest(config=>{
        // リクエスト時の処理
    })


    api.onResponseError(err=>{
        switch(err.response.status){
            // ステータスごとに異なる処理
        }
    })

    // こうすると $API で上記の設定をしたaxiosインスタンスを呼び出せる。つまり通信処理を共通化できる。
    inject('API',api);
}

こうすることでAPIの呼び出し処理を共通化できます。しかしstrapiには上記の様な strapi nuxt module というものがあります。そのモジュールを用いると以下の様に呼び出しが可能です。

async asyncData(){
    await this.$strapi.findOne('newsItem',1)
        .then(async res=>{
            //成功時の処理 res にstrapi からのデータが入っている
        })
        .catch(err=>{
       //エラー時の処理
            console.log(error);
        })
}

$strapi というコンテキストが自動で追加され findOne('contens-type-name',id)で呼び出すことができます。loginや認証ユーザーのAPIもあるのでnuxt strapiモジュールを入れておくと構築がしやすくなります。公式サイト

npm でインストールしておきます。

npm install @nuxtjs/strapi

nuxt.config.jsで以下の様に追記します。

modules: [
  '@nuxtjs/strapi',
],
strapi: {
  url:'http://localhost:1337'
},

こうするとモジュールが有効になり、$strapiのコンテキストが使用可能になります。strapi:{}でオプションが指定できます。strapiがホストされているurlを書いておきます。開発版なのでhttp://localhost:1337にしていますが実際の運用ではenvファイルに書いておくなりしておきましょう。

とりあえず 初期画面に表示してみる

nuxt.jsをインストールした時に既に存在している pages/index.vue に登録したニュースをboostrap のcardを用いて表示させてみましょう。以下の様に記述

pages/index.vue
<template>
  <div class="container">
    <div>
      <b-card
        :title="title"
        :img-src="img"
        img-top
        tag="article"
        style="max-width: 20rem;"
        class="my-5"
      >
        <b-card-text>
          {{content}}
        </b-card-text>
      </b-card>
    </div>
  </div>
</template>

<script>
export default {
  data(){
    return{

    }
  },
  async asyncData(context){
    return await context.$strapi.findOne('news-items',1)
      .then(res=>{
        return {
          title:res.title,
          img:context.env.envSet.IMG_BASE_URL+res.thumbnail.url,
          content:res.conctent
        }
      })
      .catch(err=>{
        console.error(err)
      })
  }
}
</script>

いらないものはとりあえず省きましました。コードの解説をします。

asyncDataでAPIを呼ぶ

asyncDataというのはssr・ssgモードのpages配下で利用できます。これはcreatedよりも前、コンポーネントインスタンスが完成する際に呼び出せます。このasycData内の処理が終わってからコンポーネントが作成されます。

静的出力・ssrの場合はasycDataに処理を書くことでサーバーサイドでAPIの呼び出し、そしてコンポーネントのdataへのマージを行ってくれます。createdでやると、静的出力した際になんと静的htmlからAPIを呼び出してしまいます。

詳しくはこちら

画像のURLはちょっと注意

APIで呼び出した際に、サムネイルのURLは以下の様になっていました。

thumbnail:{
...
url": "/uploads/nuxt_strapi_8a86d72c14.png",
...
}

ローカルの環境では画像にアクセスできません。(実物がhttp://localhost:1337/配下にいるため)そのためenvを用いて

img:context.env.envSet.IMG_BASE_URL+res.thumbnail.url,

としてURLの前にenv.envSet.IMG_BASE_URL = http://localhost:1337が出力される様にしています。そして表示されたページが以下の感じです。

きちんとデータ・画像がとってこれていますし、bootstrapにはめ込まれていい感じです。

データ取得できない場合

データが取得できない場合以下のことを確認

  • レコードは「Publish」になっているか
  • 権限とロールでPublicの読み取り権限にチェックを入れているか
  • エンドポイントを間違っていないか
  • strapiのサーバを落としていないか

次回は…

今回の記事ではstrapiとNuxtのインストール・連携まで行いました。次回の記事からはブログ構成を構築するためのstrapiで「ページ」データの構築、そしてnuxtでの出力方法を書いていきます。

コメント

コメント読み込み中..

Copyright © 2021 jun. All rights reserved.