ブログ構築 6. カテゴリーページの構築 #03
2019年10月16日に更新

【Nuxt.js × Contentful】カテゴリー記事一覧ページを作成する

今回達成すること

今回はカテゴリーに関連付く記事一覧を表示していきます。

下の7つの手順を行います。

  1. カテゴリーを表示するページを作成する
  2. Contentfulからカテゴリーを取得しVuexに保存する
  3. カテゴリーページへのリンクをセットする
  4. パンくずリストをグローバルコンポーネントに登録する
  5. カテゴリーに関連付く記事一覧を表示する
  6. generateプロパティにカテゴリーの動的なルーティングを追加する
  7. 本番環境にデプロイする

ちょっと長旅になりますが、頑張りましょう。

カテゴリーページの完成イメージ

2019-10-13 15-56-55

ブランチを作成しておく

今回は作業が大きくなるのでブランチを作成しておきます。

$ git checkout -b category
$ git branch

  * category

カテゴリーページを用意する

まずカテゴリーページを用意します。

Nuxt.jsプロジェクトのルートディレクトリで以下のコマンドを実行してください。

$ mkdir pages/categories && touch pages/categories/_slug.vue

pagesディレクトリに categories ディレクトリを作成し、その中に _slug.vue というファイルを作成しました。

このようなディレクトリ構造になります。

app
 L pages
   L categories
     L _slug.vue

Contentfulからカテゴリーを取得する

続いてContentfulからカテゴリーを取得します。

ロジックは記事を取得するときと同じく、middlewareからindex.jsを呼び出し、帰ってきたAPIをVuexに保管します。

middleware/getContentful.js
export default async ({ store }) => {
  if (!store.state.posts.length) await store.dispatch('getPosts')
  // 追記
  if (!store.state.categories.length) await store.dispatch('getCategories')
}
store/index.js
import defaultEyeCatch from '~/assets/images/defaultEyeCatch.png'
import client from '~/plugins/contentful'

export const state = () => ({
  posts: [],
  categories: []	// 追記
})

export const getters = {
	...
}

export const mutations = {
  setPosts(state, payload) {
    state.posts = payload
  },

  // 追記
  setCategories(state, payload) {
    state.categories = payload
  }

}

export const actions = {
  async getPosts({ commit }) {
    await client.getEntries({
      content_type: process.env.CTF_BLOG_POST_TYPE_ID,
      order: '-fields.publishDate' // desc
    }).then(res =>
      commit('setPosts', res.items)
    ).catch(console.error)
  },
  
  // 追記
  async getCategories({ commit }) {
    await client.getEntries({
      content_type: 'category',
      order: 'fields.sort'
    }).then(res =>
      commit('setCategories', res.items)
    ).catch(console.error)
  }

}
  • order: 'fields.sort'

    並びは、sort順で取得しています。「-」をつけていないので昇順(数が小さい順)です。

    • 降順で取得する場合 … '-fields.sort'

カテゴリーが取得できたが不安な場合はsetCategoriesにconsole.log()を仕込んでみてください。

export const mutations = {
  ...
  setCategories(state, payload) {
    state.categories = payload
    console.log(state.categories)
  }
}

ページをリロードすると、カテゴリーオブジェクトが表示されます。

2019-10-13 10-53-03

カテゴリーを表示する

それでは作成したcaetgories/_slug.vueにカテゴリーを表示していきましょう。

とりあえず、カテゴリー名を表示します。

pages/caetgories/_slug.vue
<template>
  <div>
    {{ category.fields.name }}
  </div>
</template>

<script>
export default {
  async asyncData({ payload, store, params, error }) {
    const category = payload || await store.state.categories.find(cat => cat.fields.slug === params.slug)

    if (category) {
      return { category }
    } else {
      return error({ statusCode: 400 })
    }
  }
}
</script>
  • カテゴリーの取得ロジック

    本番環境ではpayloadにカテゴリーオブジェクトが入っています。

    もしpayloadが空の場合は、Vuexからcategoryを取得します。

  • .find(cat => cat.fields.slug === params.slug)

    Vuexのstate.categoriesから、パラメーターslugに一致するカテゴリーをfindメソッドで取得しています。

    ここのパラメーターはカテゴリー作成のときに入力するslugのことです。

表示確認

カテゴリーページを表示します。下記URLにアクセスしてください。

http://localhost:3000/categories/設定したカテゴリーのslug

例えば筆者の場合は “http://localhost:3000/categories/ruby-on-rails” となります。

2019-10-13 11-09-48

ちゃんとカテゴリー名が表示されていますね。

OK!次へ進みましょう。

トップページにカテゴリーリンクを設定する

各カテゴリーページに飛べるよう、トップページにリンクを設定しましょう。

前回の記事で設定したチップにリンクをつけます。

2019-10-11 14-05-12

pages/index.vue
<template>

  <v-chip
    small
    dark
    :color="categoryColor(post.fields.category)"
     :to="linkTo('categories', post.fields.category)"	<!-- toを書き換え -->
    class="font-weight-bold"
  >
    {{ post.fields.category.fields.name }}
  </v-chip>
</template>

パンくずリストにカテゴリーリンクを設定する

前回設定したパンくずリストにリンクを設定します。

2019-10-11 14-05-59

このパンくずリストはカテゴリーページでも利用しますので、コンポーネントを作成して共通化します。

パンくずリストコンポーネントの作成

まず components/ui ディレクトリ内にbreadcrumbs.vueファイルを作成しましょう。

$ touch components/ui/breadcrumbs.vue

そして以下のように編集します。

components/ui/breadcrumbs.vue
<template>
  <v-breadcrumbs :items="breadcrumbs">
    <template #item="props">
      <v-breadcrumbs-item
        :to="props.item.to"
      >
        <v-icon v-if="props.item.icon" color="primary">
          {{ props.item.icon }}
        </v-icon>
        <span class="ml-1">
          {{ props.item.text }}
        </span>
      </v-breadcrumbs-item>
    </template>
    <template #divider>
      <v-icon>mdi-chevron-right</v-icon>
    </template>
  </v-breadcrumbs>
</template>

<script>
export default {
  props: {
    addItems: {
      type: Array,
      default() { return [] }
    }
  },
  data: () => ({
    items: [
      { icon: 'mdi-home', text: 'ホーム', to: '/' }
    ]
  }),
  computed: {
    breadcrumbs() {
      return this.items.concat(this.addItems)
    }
  }
}
</script>
  • breadcrumbsコンポーネントの仕組み

    このコンポーネントは、常に「ホーム」のリンクを表示します。

    addItemsプロパティに配列を渡した場合に、パンくずリストが追加されます。

  • default() { return [] }

    propsのデフォルトに配列を渡すときは、関数を返します。

    default()は、default: function()の省略記法です。

    オブジェクトの場合も同じ書き方をします。

    プロパティ - Vuejs

  • this.items.concat(this.addItems)

    concatは配列を結合するjavascriptのメソッドです。

    2以上の配列を結合して新しい配列を返します。

    Array.prototype.concat()

コンポーネントをグローバル登録する

このパンくずコンポーネントは、よく利用するのでグローバルに登録しましょう。

コンポーネントをグローバル登録する手順は、

  1. plugins ディレクトリにcomponents.jsファイルを作成し、
  2. コンポーネントファイルを登録。
  3. そしてnuxt.config.jsにプラグインファイルを登録します。

まずは、components.jsファイルを作成しましょう。

$ touch plugins/components.js

以下のように編集します。

plugins/components.js
import Vue from 'vue'
import breadcrumbs from '~/components/ui/breadcrumbs'

Vue.component('breadcrumbs', breadcrumbs)

最後にnuxt.config.jsへ登録します。

nuxt.config.js
export default {
	
  plugins: [
    'plugins/vuetify',
    'plugins/contentful',
    'plugins/components'	// 追記
  ],
}

これでパンくずコンポーネントはグローバル登録されました。

パンくずリストを記事ページに表示する

パンくずリストをコンポーネント化したので、posts/_slug.vueを書き換えましょう。

必要のないコードを削除して、最終的にこのようになりました。

pages/posts/_slug.vue
<template>
  <v-container fluid>
    <breadcrumbs :add-items="addBreads" />	<!-- 追記 -->

    {{ currentPost.fields.title }}
    <v-img
      :src="setEyeCatch(currentPost).url"
      :alt="setEyeCatch(currentPost).title"
      :aspect-ratio="16/9"
      width="700"
      height="400"
      class="mx-auto"
    />
    {{ currentPost.fields.publishDate }}<br>
    {{ currentPost.fields.body }}

  </v-container>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  computed: {
    ...mapGetters(['setEyeCatch', 'linkTo']),	// 追記
    // 追記
    addBreads() {
      return [
        {
          icon: 'mdi-folder-outline',
          text: this.category.fields.name,
          to: this.linkTo('categories', this.category)
        }
      ]
    }
    // 追記終了
  },
  async asyncData({ payload, store, params, error }) {
    const currentPost = payload || await store.state.posts.find(post => post.fields.slug === params.slug)

    if (currentPost) {
      return {
        currentPost,
        category: currentPost.fields.category	// 追記
      }
    } else {
      return error({ statusCode: 400 })
    }
  }
}
</script>

前回のパンくずリストに、iconを追加してリンク先を分かりやすくしてみました。

どうでしょう。クリックするとリンク先に飛びますか?

2019-10-16 18-23-05

パンくずリストをカテゴリーページに表示する

categories/_slug.vueにもパンくずリストを表示しましょう。

コンポーネントをグローバルに登録したおかげで、たった一行で完了します。

pages/categories/_slug.vue
<template>
  <div>
    <breadcrumbs />		        <!-- 追記 -->
    {{ category.fields.name }}
  </div>
</template>

このように、「ホーム」のリンクが表示されたら成功です。

2019-10-16 18-27-08

カテゴリーに関連付く記事を表示する

だいぶ形になってきました。

それでは、メイン作業であるカテゴリーに関連付く記事を表示していきます。

まずは、store/index.jsにGettersを作成します。

store/index.js
export const getters = {
	
  // 追記
  relatedPosts: state => (category) => {
    const posts = []
    for (let i = 0; i < state.posts.length; i++) {
      const catId = state.posts[i].fields.category.sys.id
      if (category.sys.id === catId) posts.push(state.posts[i])
    }
    return posts
  }
}
  • if (category.sys.id === catId) posts.push(state.posts[i])

    引数で渡したカテゴリーのIDと、postが持っているカテゴリーのIDが一致した時に、postオブジェクトをpushして配列を作成しています。

(コラム) for vs filter

上記のfor文は、filterに変えてスッキリと書くことができます。

これは全く同じ結果を返します。

relatedPosts: state => (category) => {
  return state.posts.filter(post => post.fields.category.sys.id === category.sys.id)
}

ではなぜfor文なのでしょう。

それは処理スピードにあります。

javascriptでは、mapやfind、filterなど様々なループメソッドがありますが、for文が一番早いとされています。

そしていずれのメソッドもfor文で代用することが可能です。

Why using for is faster than some() or filter() - stack overflow

Javascript performance test - for vs for each vs (map, reduce, filter, find)

(コラム終わり)

投稿一覧を表示する

とりあえずスタイルは後回しで、カテゴリーとタイトルを表示します。

最終的にcategories/_slug.vueはこのようになります。

pages/categories/_slug.vue
<template>
  <div>
    <breadcrumbs />
    <!-- 追記 -->
    <h1>{{ category.fields.name }}</h1>
    <div
      v-for="(post, i) in relatedPosts"
      :key="i"
    >
      {{ post.fields.category.fields.name }},
      {{ post.fields.title }}
    </div>
    <!-- 追記終了-->
  </div>
</template>

<script>
export default {
  // 追記
  computed: {
    relatedPosts() {
      return this.$store.getters.relatedPosts(this.category)
    }
  },
  // 追記終了
  async asyncData({ payload, store, params, error }) {
    const category = payload || await store.state.categories.find(cat => cat.fields.slug === params.slug)

    if (category) {
      return { category }
    } else {
      return error({ statusCode: 400 })
    }
  }
}
</script>

編集が終わったら、カテゴリーページを表示してみてください。

こんな感じになります。

2019-10-13 15-56-55

generateプロパティに動的ルーティングを追加する

さあ、後少しです。

nuxt.config.jsのgenerateプロパティにカテゴリーページのルートを追加します。

nuxt.config.js
export default {

  generate: {
    routes() {
      return Promise.all([
        client.getEntries({
          content_type: process.env.CTF_BLOG_POST_TYPE_ID
        }),
        client.getEntries({        							// 追記
          content_type: 'category'
        })
      ]).then(([posts, categories]) => {        // 追記
        return [
          ...posts.items.map((post) => {
            return { route: `posts/${post.fields.slug}`, payload: post }
          }),
          ...categories.items.map((category) => {        // 追記
            return { route: `categories/${category.fields.slug}`, payload: category }
          })
        ]
      })
    }
  }

}
  • payload: category

    このpayloadにカテゴリーオブジェクトを代入しています。

    このpayloadはasyncData({ payload })の引数として受け取ることができます。

動的なルーティングの確認

上記で設定が正しく行われているか確認してみましょう。

下記コマンドを実行してください。

$ yarn generate

ターミナルにカテゴリーのパスが表示されたと思います。

2019-10-16 18-44-52

これが、動的なルーティングページの無事HTMLファイルが生成された証拠です。

本番環境にデプロイする

今までの編集をコミットして本番環境にデプロイしましょう。

$ git add -A
$ git commit -m "add_category_pages"
$ git checkout master
$ git merge category 
$ git push

HTMLファイル生成の確認をする

本番環境へデプロイが完了したら、カテゴリーページをリロードしてみてください。

「404 Not Find」エラーが出なければ、HTMLファイルの生成に成功しています。

まとめ

長旅でしたねー。お疲れ様でした。

今回は、

  • カテゴリーページの作成
  • パンくずリストの作成
  • generateプロパティへルーティングの追加

を行いました。

ちょっとスタイルは不格好ですが、カテゴリーページの骨組みはできましたね。

これで「記事」と「カテゴリー」の基本機能は完成です。

さて、次回は?

次回はタグ機能の構築です。

まずはContentfulでタグモデルを作成するところから始めます。

後少しで「記事」「カテゴリー」「タグ」のブログ機能の3本柱が完成します。

頑張りましょう!

現在、カテゴリー「Rails apiとNuxt.jsでSPA開発」のデモアプリを構築中です。記事になるまでもう少々のお時間が必要です。ブログの更新が止まって申し訳ありません。デモアプリの進捗状況は こちらの記事 で随時お伝えしてまいります。
スポンサー広告
次の記事はこちらです
ブログ構築
1. 今回作るアプリケーション
#01
Nuxt.jsとContentfulで作るマイブログ
今日のTweet
スポンサー広告