今回達成すること
今回はカテゴリーに関連付く記事一覧を表示していきます。
下の7つの手順を行います。
- カテゴリーを表示するページを作成する
- Contentfulからカテゴリーを取得しVuexに保存する
- カテゴリーページへのリンクをセットする
- パンくずリストをグローバルコンポーネントに登録する
- カテゴリーに関連付く記事一覧を表示する
- generateプロパティにカテゴリーの動的なルーティングを追加する
- 本番環境にデプロイする
ちょっと長旅になりますが、頑張りましょう。
カテゴリーページの完成イメージ
ブランチを作成しておく
今回は作業が大きくなるのでブランチを作成しておきます。
$ git checkout -b category
$ git branch
* category
カテゴリーページを用意する
まずカテゴリーページを用意します。
Nuxt.jsプロジェクトのルートディレクトリで以下のコマンドを実行してください。
$ mkdir pages/categories && touch pages/categories/_slug.vue
pagesディレクトリに categories ディレクトリを作成し、その中に
このようなディレクトリ構造になります。
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)
}
}
-
content_type: 'category'
ここには、カテゴリーモデルを作成した時に設定したApi Identifierを指定します。
そうそう、この記事で設定したやつです。
-
order: 'fields.sort'
並びは、sort順で取得しています。「-」をつけていないので昇順(数が小さい順)です。
- 降順で取得する場合 ...
'-fields.sort'
- 降順で取得する場合 ...
カテゴリーが取得できたが不安な場合はsetCategoriesにconsole.log()を仕込んでみてください。
export const mutations = {
...
setCategories(state, payload) {
state.categories = payload
console.log(state.categories)
}
}
ページをリロードすると、カテゴリーオブジェクトが表示されます。
カテゴリーを表示する
それでは作成した
とりあえず、カテゴリー名を表示します。
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" となります。
ちゃんとカテゴリー名が表示されていますね。
OK!次へ進みましょう。
トップページにカテゴリーリンクを設定する
各カテゴリーページに飛べるよう、トップページにリンクを設定しましょう。
前回の記事で設定したチップにリンクをつけます。
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>
パンくずリストにカテゴリーリンクを設定する
前回設定したパンくずリストにリンクを設定します。
このパンくずリストはカテゴリーページでも利用しますので、コンポーネントを作成して共通化します。
パンくずリストコンポーネントの作成
まず components/ui ディレクトリ内に
$ 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()の省略記法です。
オブジェクトの場合も同じ書き方をします。
-
this.items.concat(this.addItems)
concatは配列を結合するjavascriptのメソッドです。
2以上の配列を結合して新しい配列を返します。
コンポーネントをグローバル登録する
このパンくずコンポーネントは、よく利用するのでグローバルに登録しましょう。
コンポーネントをグローバル登録する手順は、
- plugins ディレクトリに
components.js ファイルを作成し、 - コンポーネントファイルを登録。
- そして
nuxt.config.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
export default {
plugins: [
'plugins/vuetify',
'plugins/contentful',
'plugins/components' // 追記
],
}
これでパンくずコンポーネントはグローバル登録されました。
パンくずリストを記事ページに表示する
パンくずリストをコンポーネント化したので、
必要のないコードを削除して、最終的にこのようになりました。
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を追加してリンク先を分かりやすくしてみました。
どうでしょう。クリックするとリンク先に飛びますか?
パンくずリストをカテゴリーページに表示する
コンポーネントをグローバルに登録したおかげで、たった一行で完了します。
pages/categories/_slug.vue
<template>
<div>
<breadcrumbs /> <!-- 追記 -->
{{ category.fields.name }}
</div>
</template>
このように、「ホーム」のリンクが表示されたら成功です。
カテゴリーに関連付く記事を表示する
だいぶ形になってきました。
それでは、メイン作業であるカテゴリーに関連付く記事を表示していきます。
まずは、
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)
(コラム終わり)
投稿一覧を表示する
とりあえずスタイルは後回しで、カテゴリーとタイトルを表示します。
最終的に
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>
編集が終わったら、カテゴリーページを表示してみてください。
こんな感じになります。
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
ターミナルにカテゴリーのパスが表示されたと思います。
これが、動的なルーティングページの無事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本柱が完成します。
頑張りましょう!