Nuxt.jsとmicroCMSでJamstack構成のサイトを作ってみる

Nuxt.jsとmicroCMSを使って、Jamstack構成のサイトを作成する流れを試してみます。
サーバにはNetlifyを使用します。

サイト構成

今回は新着情報を投稿できるサイトの想定で作成してみます。
サイト構成は以下の通りです。

  • トップ(/)
  • 新着情報一覧(/info/)
  • 新着情報詳細(/info/記事ID/)
  • 会社概要(/company/)

トップには新着情報の最新記事3件を表示させます。

microCMSの準備

まずはmicroCMSで今回使用するコンテンツ(新着情報)を作成します。
入力フィールドは以下のようにタイトル・本文・アイキャッチを用意します。

コンテンツ作成後、ダミーの記事をいくつか登録しておきます。

最後にAPI利用時に使用するエンドポイントとAPIキーを取得しておきます。
エンドポイントは右上のAPI設定 > 基本情報 > エンドポイント から確認できます。

APIキーは左上の歯車アイコン > API-KEY > X-API-KEYから確認できます。

それぞれ後ほど使用するのでコピペしておいてください。

Nuxt.jsの準備

次にNuxt.jsの環境を準備します。
今回はcreate-nuxt-appを使ってインストールしますが、インストールに関しては以前に記事を投稿しているのでそちらを参照ください。
今回はNuxt.js modulesでAxios、Rendering modeでUniversal、Deployment targetでStaticを選択しています。

インストール完了後にプロジェクトディレクトリに移動して、下記コマンドで開発環境を起動します。

npm run dev

共通部分の作成

Nuxt.jsの準備ができたので、まずは共通部分を作成してみます。
components/Header.vueを追加して、下記内容でヘッダーを作成します。

<template>
  <header class="l-header">
    <p class="l-header-logo">
      <NuxtLink class="l-header-logo__link" to="/">サイト名</NuxtLink>
    </p>
    <nav class="l-header-nav">
      <ul class="l-header-nav__list">
        <li class="l-header-nav__item">
          <NuxtLink class="l-header-nav__link" to="/company/">会社概要</NuxtLink>
        </li>
        <li class="l-header-nav__item">
          <NuxtLink class="l-header-nav__link" to="/info/">新着情報</NuxtLink>
        </li>
      </ul>
    </nav>
  </header>
</template>

<style>
.l-header {
  display: flex;
  padding: 15px;
  background: #333333;
}
.l-header-logo {
  margin: 0;
}
.l-header-logo__link {
  color: #ffffff;
  text-decoration: none;
}

.l-header-nav {
  margin-left: auto;
}
.l-header-nav__list {
  display: flex;
  margin: 0;
  padding: 0;
}
.l-header-nav__item {
  list-style: none;
  margin-left: 20px;
}
.l-header-nav__link {
  color: #ffffff;
  text-decoration: none;
}
</style>

これでヘッダー部分が作成できました。
次にlayout/default.vueを作成して、先ほど作成したヘッダーを読み込みます。

<template>
  <div class="l-wrapper">
    <Header />
    <main class="l-main">
      <Nuxt />
    </main>
  </div>
</template>

<style>
body {
  margin: 0;
}
.l-main {
  max-width: 1000px;
  margin: auto;
  padding: 20px;
}
</style>

これで共通部分が作成できました。

トップの作成

次にトップを作成しますが、トップではAPIを使って新着情報を表示する想定なので、まずはAPIを使用する準備を行います。
.envファイルを作成して、microCMSの準備時にコピペしたエンドポイントとAPIキーを追記します。

NODE_ENV='development'
API_URL=エンドポイントをここに入れる
API_KEY=APIキーをここに入れる

おそらくデフォルトで.gitignoreに.envファイルが設定されていると思いますが、もし設定されていない場合は.gitignoreに設定を追加しておいてください。
nuxt.config.jsに下記を追加して、.envの内容を使用できるようにします。
RuntimeConfigについて詳しくは以前に記事を投稿していますので、そちらを参照ください。

export default {
  privateRuntimeConfig: {
    apiURL: process.env.API_URL,
    apiKey: process.env.API_KEY
  },
  publicRuntimeConfig: {
    apiURL: process.env.NODE_ENV !== 'production' ? process.env.API_URL : '',
    apiKey: process.env.NODE_ENV !== 'production' ? process.env.API_KEY : ''
  }
}

.envのNODE_ENVがproduction(本番)の場合はフロントでエンドポイントやAPIキーを使用しないようにしています。

これでAPIが使用できるようになったので、トップを作成します。
pages/index.vue を作成して、以下のようにします。

<template>
  <div class="p-top">
    <div class="p-mv">メインビジュアル的なブロック</div>

    <div class="p-article-list">
      <h2>新着情報</h2>
      <article v-for="article of articles" class="p-article-item" :key="article.id">
        <NuxtLink class="p-article-item__link" v-bind:to="'/info/' + article.id + '/'">
          <div class="p-article-item__date">
            <time>{{ dayFormat(article.publishedAt) }}</time>
          </div>
          <p class="p-article-item__ttl">{{ article.title }}</p>
        </NuxtLink>
      </article>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      articles: []
    }
  },
  async asyncData({ $axios, $config }) {
    const res = await $axios.$get(
      $config.apiURL + '?limit=3',
      {
        headers: {
          'X-API-KEY': $config.apiKey
        }
      }
    );
    return {
      articles: res.contents
    }
  },
  computed: {
    dayFormat: function () {
      return function(date) {
        const dateStr = new Date(date);
        return dateStr.getFullYear() + '/' + (dateStr.getMonth() + 1) + '/' + dateStr.getDate();
      }
    }
  }
}
</script>

<style>
.p-article-item {
  border-bottom: #cccccc 1px solid;
  padding: 15px;
}
.p-article-item__link {
  color: #333333;
  text-decoration: none;
}
.p-article-item__date {
  font-size: 14px;
}
.p-article-item__ttl {
  margin: 0;
  font-size: 16px;
}
</style>

先ほど準備したエンドポイントやAPIキーは、$config.apiURLや$config.apiKeyの形で使用できます。
これで以下のようなトップページを作成することができました。

新着情報一覧の作成

次に新着情報一覧を作成しますが、基本的にはトップの流用で作成できます。
pages/info/index.vue を作成して、以下のように記述します。

<template>
  <div class="p-info">
    <div class="p-article-list">
      <h1>新着情報</h1>
      <article v-for="article of articles" class="p-article-item" :key="article.id">
        <NuxtLink class="p-article-item__link" v-bind:to="'/info/' + article.id + '/'">
          <div class="p-article-item__date">
            <time>{{ dayFormat(article.publishedAt) }}</time>
          </div>
          <p class="p-article-item__ttl">{{ article.title }}</p>
        </NuxtLink>
      </article>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      articles: []
    }
  },
  async asyncData({ $axios, $config }) {
    const res = await $axios.$get(
      $config.apiURL,
      {
        headers: {
          'X-API-KEY': $config.apiKey
        }
      }
    );
    return {
      articles: res.contents
    }
  },
  computed: {
    dayFormat: function () {
      return function(date) {
        const dateStr = new Date(date);
        return dateStr.getFullYear() + '/' + (dateStr.getMonth() + 1) + '/' + dateStr.getDate();
      }
    }
  }
}
</script>

トップでは最新3件のみ取得したかったので「?limit=3」を付与していましたが、新着情報一覧では全件表示にしたいので外しています。

これで新着情報一覧も作成できましたが、トップと新着情報一覧で記事部分のコードが同じなのでコンポーネント化してみます。
components/ArticleItem.vue を作成して、トップから記事部分のパーツを持ってきます。

<template>
  <article class="p-article-item">
    <NuxtLink class="p-article-item__link" v-bind:to="'/info/' + article.id + '/'">
      <div class="p-article-item__date">
        <time>{{ dayFormat(article.publishedAt) }}</time>
      </div>
      <p class="p-article-item__ttl">{{ article.title }}</p>
    </NuxtLink>
  </article>
</template>

<script>
export default {
  props: ['article'],
  computed: {
    dayFormat: function () {
      return function(date) {
        const dateStr = new Date(date);
        return dateStr.getFullYear() + '/' + (dateStr.getMonth() + 1) + '/' + dateStr.getDate();
      }
    }
  }
}
</script>

<style>
.p-article-item {
  border-bottom: #cccccc 1px solid;
  padding: 15px;
}
.p-article-item__link {
  color: #333333;
  text-decoration: none;
}
.p-article-item__date {
  font-size: 14px;
}
.p-article-item__ttl {
  margin: 0;
  font-size: 16px;
}
</style>

親からの記事データを引き継げるようにpropsを設定しています。

トップを今作成したコンポーネントを読み込む形に変更してみます。

<template>
  <div class="p-top">
    <div class="p-mv">メインビジュアル的なブロック</div>

    <div class="p-article-list">
      <h2>新着情報</h2>
      <ArticleItem v-for="article of articles" :article="article" :key="article.id" />
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      articles: []
    }
  },
  async asyncData({ $axios, $config }) {
    const res = await $axios.$get(
      $config.apiURL + '?limit=3',
      {
        headers: {
          'X-API-KEY': $config.apiKey
        }
      }
    );
    return {
      articles: res.contents
    }
  }
}
</script>

新着情報一覧も同様に変更します。

<template>
  <div class="p-info">
    <div class="p-article-list">
      <h1>新着情報</h1>
      <ArticleItem v-for="article of articles" :article="article" :key="article.id" />
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      articles: []
    }
  },
  async asyncData({ $axios, $config }) {
    const res = await $axios.$get(
      $config.apiURL,
      {
        headers: {
          'X-API-KEY': $config.apiKey
        }
      }
    );
    return {
      articles: res.contents
    }
  }
}
</script>

これで記事部分をコンポーネント化することができました。

新着情報詳細の作成

次に新着情報詳細を作成します。
pages/info/_slug/index.vue を作成して、以下のように記述します。

<template>
  <div class="p-article-detail">
    <div class="p-article-detail__date">
      <time>{{ dayFormat(article.publishedAt) }}</time>
    </div>
    <h1 class="p-article-detail__ttl">{{ article.title }}</h1>
    <div class="p-article-detail__img" v-if="article.img">
      <img v-bind:src="article.img.url">
    </div>
    <div class="p-article-detail__body" v-html="article.body"></div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      article: {}
    }
  },
  async asyncData({ params, $axios, $config }) {
    const slug = params.slug
    const res = await $axios.$get(
      $config.apiURL + '/' + slug,
      {
        headers: {
          'X-API-KEY': $config.apiKey
        }
      }
    );
    return {
      slug,
      article: res
    }
  },
  computed: {
    dayFormat: function () {
      return function(date) {
        const dateStr = new Date(date);
        return dateStr.getFullYear() + '/' + (dateStr.getMonth() + 1) + '/' + dateStr.getDate();
      }
    }
  }
}
</script>

<style>
.p-article-detail__img img {
  width: 100%;
  height: auto;
}
</style>

これで新着情報詳細も作成できましたが、記事の日付部分の整形が複数ページにあるので、先ほどと同じようにコンポーネント化してみます。
components/DateFormat.vue を作成して、以下のようにします。

<template>
  <time>{{ dayFormat(date) }}</time>
</template>

<script>
export default {
  props: ['date'],
  computed: {
    dayFormat: function () {
      return function(date) {
        const dateStr = new Date(date);
        return dateStr.getFullYear() + '/' + (dateStr.getMonth() + 1) + '/' + dateStr.getDate();
      }
    }
  }
}
</script>

ArticleItem.vueのtemplate内にある日付部分を差し替えて、使用しなくなったdayFormat関数を削除します。

<template>
  <article class="p-article-item">
    <NuxtLink class="p-article-item__link" v-bind:to="'/info/' + article.id + '/'">
      <div class="p-article-item__date">
        <DateFormat :date="article.publishedAt" />
      </div>
      <p class="p-article-item__ttl">{{ article.title }}</p>
    </NuxtLink>
  </article>
</template>

~ 以下略 ~

新着情報詳細も同様に変更します。

<template>
  <div class="p-article-detail">
    <div class="p-article-detail__date">
      <DateFormat :date="article.publishedAt" />
    </div>
    <h1 class="p-article-detail__ttl">{{ article.title }}</h1>
    <div class="p-article-detail__img" v-if="article.img">
      <img v-bind:src="article.img.url">
    </div>
    <div class="p-article-detail__body" v-html="article.body"></div>
  </div>
</template>

~ 以下略 ~

これで日付部分もコンポーネント化できました。

最後に会社概要ですが、pages/company.vue に以下の内容で作成します。

<template>
  <div class="p-company">
    <h1>会社概要</h1>
    <p>会社概要ページです。</p>
  </div>
</template>

これでサイト内のページが一通り作成できました。

サイトの公開

一通りページの作成が完了したので、サイトの公開を行います。
今回はNetlifyとGitHubを連携して公開するので、GitHubにリポジトリを作成してここまでの作業内容をプッシュします。

Netlifyの管理画面にログインして、サイトの追加をします。
New site from Gitをクリックします。

連携するGitのサービスでGitHubをクリックして、先ほどプッシュしたリポジトリを選択します。

デプロイの設定で、Build commandに「npm run generate」、Publish directoryに「dist」と設定します。

これでサイトの追加が完了しました。

次に.envファイルで設定していたエンドポイントやAPIキーといった環境変数をNetlifyで設定します。
Site settings > Build & deploy > Environment variables でEdit variablesをクリックします。

KeyとValueを入力できるので、.envファイルで設定していたNODE_ENV、API_URL、API_KEYを指定します。
その際、NODE_ENVの値をproductionにします。

これで環境変数の設定ができました。
これでGitHubのリポジトリにプッシュするとデプロイされるようになりましたが、microCMSで新着情報を更新してもデプロイされないため、サイトに反映されません。
そのため、Webhookを用意してmicroCMS側を更新した際にもデプロイされるようにします。

Site settings > Build & deploy > Build hooks でAdd build hookをクリックします。

Build hook name とビルド対象のブランチを選択して、Saveをクリックします。

保存後に生成されるURLはmicroCMS側の設定で使用するのでコピーしておきます。
これでNetlifyでの設定が完了しました。

次はmicroCMSの設定ですが、管理画面でAPI設定 > Webhookを選択して、Webhookの追加をクリックします。

Netlifyを選択します。

先ほどコピーしたURLを張り付けて、通知のタイミングを設定します。

通知のタイミングには以下のようなタイミングが用意されているようです。

  • コンテンツの公開時・更新時
    • コンテンツ編集画面による操作
    • APIによる操作
    • レビューによる公開
    • 予約設定による操作
    • コンテンツの並び替え
    • コンテンツIDの変更
  • コンテンツの非公開時
    • コンテンツ編集画面による操作
    • 予約設定による操作
  • コンテンツの下書き保存時
    • コンテンツ編集画面による操作
    • APIによる操作
    • コンテンツの並び替え
    • コンテンツIDの変更
  • 公開中コンテンツの削除時
    • コンテンツ編集画面による操作
    • APIによる操作
  • 下書きコンテンツの削除時
    • コンテンツ編集画面による操作
    • APIによる操作
  • APIの設定変更時
    • APIの設定変更時
  • APIの削除時
    • APIの削除時

今回はデフォルトで選択されている「コンテンツの公開時・更新時」の各項目にチェックが入った状態で設定します。
これでmicroCMS側を更新した際にNetlifyでデプロイされるようになりました。
新着情報を新規で追加してみて、サイトに反映されていればOKです。

参考サイト

このエントリーをはてなブックマークに追加

関連記事

コメントを残す

メールアドレスが公開されることはありません。
* が付いている欄は必須項目です

CAPTCHA


コメントが承認されるまで時間がかかります。

2021年9月
 1234
567891011
12131415161718
19202122232425
2627282930