今回達成すること
- タグ一覧ページ =>
tags/index.vue と、 - タグに関連づく記事一覧を表示する =>
tags/_slug.vue (以下、タグコンテンツページ)
を作成します。
タグ一覧とタグコンテンツページを作成する
「pages」ディレクトリに「tags」ディレクトリを作成し、その直下に
% mkdir pages/tags && touch $_/{index.vue,_slug.vue}
index.vue にアクセスするパスは/tags
_slug.vue にアクセスするパスは/tags/<tag.fields.slug>
となります。
タグ一覧ページの編集
タグ一覧ページはVuexに保管したtags
をasyncData()
内で取得し、Htmlに表示しています。
pages/tags/index.vue
<template>
<v-container>
<nuxt-link
to="/"
>
Home
</nuxt-link>
<h2>
タグ一覧
</h2>
<ul>
<li
v-for="(tag, i) in tags"
:key="`tag-${i}`"
>
<nuxt-link
:to="{ name: 'tags-slug', params: { slug: tag.fields.slug } }"
>
{{ tag.fields.title }}
</nuxt-link>
</li>
</ul>
</v-container>
</template>
<script lang="ts">
import { Context } from '@nuxt/types'
import { Component, Vue } from 'nuxt-property-decorator'
import { BlogTag } from '~/store/types'
type AsyncData = {
tags: BlogTag[]
}
@Component
export default class TagsPage extends Vue {
asyncData ({ app: { $accessor } }: Context): AsyncData {
const tags = $accessor.tags
return {
tags
}
}
}
</script>
http://localhost:3000/tags にアクセスしてみましょう。
タグ一覧が表示されました。
タグコンテンツページの編集
pages/tags/_slug.vue
<template>
<v-container>
<nuxt-link
to="/tags"
>
Tags
</nuxt-link>
<div>
Tag title: {{ tag.fields.title }}
</div>
<h2>
タグに関連づく記事
</h2>
<!-- TODO tagPostsのループ -->
</v-container>
</template>
<script lang="ts">
import { Context } from '@nuxt/types'
import { Component, Vue } from 'nuxt-property-decorator'
import { BlogTag } from '~/store/types'
type AsyncData = void | {
tag: BlogTag
}
@Component
export default class TagsSlugPage extends Vue {
validate ({ params }: Context): boolean {
return !!params.slug
}
asyncData ({ app: { $accessor }, params, $my }: Context): AsyncData {
const tag = $accessor.tags.find((tag: BlogTag) =>
tag.fields.slug === params.slug
)
if (!tag) {
return $my.errorHandler(404)
}
return {
tag
}
}
}
</script>
/tags
からタグのリンクをクリックし、タグコンテンツページが表示されることを確認してください。
リンクを生成する共通メソッドを作成する
<nuxt-link>
に渡すリンクオブジェクトを簡素化するために共通メソッドを作成します。
<!-- 下記リンクオブジェクトを共通メソッド化 -->
<nuxt-link
:to="{ name: 'tags-slug', params: { slug: tag.fields.slug } }"
>
linkTo()
というメソッドを追加します。
plugins/my-plugin.ts
// 追加
import { BlogCategory, BlogPost, BlogTag } from '~/store/types'
// 以下、local types追加
/*
local types
*/
// リンクオブジェクトのname値
type LinkName = 'categories-slug' | 'tags-slug' | ''
// LinkTo()のデフォルト戻り値
interface LinkToDefault {
name: LinkName
params: {
slug: string
}
}
// LinkTo()のblogPost戻り値
interface LinkToPost {
name: 'posts-category-slug'
params: {
category: string
slug: string
}
}
/*
class types
*/
export interface MyPluginInterface {
errorHandler (statusCode: number): void
errorMessage (statusCode: number): string
// 追加
linkTo (entry: BlogCategory | BlogPost | BlogTag): LinkToDefault | LinkToPost
}
class MyPlugin implements MyPluginInterface {
...
// 追加
// 引数のリンクオブジェクトを返す
linkTo (entry: BlogCategory | BlogPost | BlogTag) {
const entryType: string = entry.sys.contentType.sys.id
const slug: string = entry.fields.slug
// blogPostPath: /posts/<category.fields.slug>/<post.fields.slug>
if (entryType === 'blogPost') {
const name: 'posts-category-slug' = 'posts-category-slug'
const post = entry as BlogPost
const category: string = post.fields.category.fields.slug
return { name, params: { category, slug } }
}
const linkName = (entryType: string): LinkName => {
switch (entryType) {
case 'blogCategory': return 'categories-slug'
case 'blogTag': return 'tags-slug'
/* 動的ルートネームはここに追加する */
default: return ''
}
}
return { name: linkName(entryType), params: { slug } }
}
}
interface LinkToPost
...blogPost
のリンクオブジェクトには、category
プロパティが必須なので専用の型を用意している。linkTo (): LinkToDefault | LinkToPost
...linkTo()
メソッドの戻り値は、LinkToDefault
もしくはLinkToPost
となる。const name: 'posts-category-slug'
...'blogPost'
の場合、name
プロパティは固定値なので、'posts-category-slug'
の固定値を型として指定。これをしないとstring
型となり、タイプエラーとなる。const post = entry as BlogPost
...as
でBlogPost
型として上書きしている。これによりfields.category
プロパティが使用可能となる。BlogCategory
,BlogTag
の型には、fields.category
プロパティが存在しないのでas
で型を固定しないとタイプエラーとなる。
エディタで解析されたlinkTo()
の型は以下のようになります。
現在のリンクをlinkTo()に書き換える
それではタグ一覧ページのリンクをlinkTo()
に書き換えましょう。
pages/tags/index.vue
<!-- :to="ここを書き換え" -->
<nuxt-link
:to="$my.linkTo(tag)"
>
<!-- <nuxt-link
:to="{ name: 'tags-slug', params: { slug: tag.fields.slug } }"
> -->
{{ tag.fields.title }}
</nuxt-link>
ページ遷移が正しく行われているか確認してください。
Vuexにタグに関連づく記事一覧オブジェクトを作成する
ここからタグに紐づく記事一覧を表示する設定を行います。
VuexにTagIDに紐づいた記事一覧格納する、tagPosts
オブジェクトを作成します。
tagPostsオブジェクトの完成イメージ
tagPosts: {
'tag ID 1': [<'tag ID 1'に関連づく記事の配列>],
'tag ID 2': [<'tag ID 2'に関連づく記事の配列>],
...
}
型の名前を書き換える
型はCategoryPosts
と同じものを使用します。
そこで、CategoryPosts
の型の名前をRelatedPosts
に書き換え、共通の型として使用します。
CategoryPosts
を書き変えましょう。
store/types/index.ts
// 書き換え
// カテゴリー関連 or タグ関連posts
export type RelatedPosts = {
[key: string]: BlogPost[]
}
// export type CategoryPosts = {
// [key: string]: BlogPost[]
// }
RelatedPosts
に書き換えたことにより、
store/index.ts
// { ..., RelatedPosts }に書き換え
import { BlogCategory, BlogPost, BlogTag, RelatedPosts } from './types'
export const state = () => ({
...
// as RelatedPostsに書き換え
categoryPosts: {} as RelatedPosts
})
export const mutations = mutationTree(state, {
...
// (..., payload: RelatedPosts)に書き換え
setCategoryPosts (state: RootState, payload: RelatedPosts) {
state.categoryPosts = payload
}
})
export const actions = actionTree({ state, getters, mutations }, {
...
getCategoryPosts ({ state, commit }) {
// RelatedPostsに書き換え
const categoryPosts: RelatedPosts = {}
}
})
tagPostsを処理するメソッド群を追加する
state
=>tagPosts: {}
mutations
=>setTagPosts()
actions
=>getTagPosts()
をそれぞれ追加します。
アクションメソッドのgetTagPosts()
は、nuxtServerInit()
内で呼び出してください。
store/index.ts
export const state = () => ({
...
// 追加
tagPosts: {} as RelatedPosts
})
...
export const mutations = mutationTree(state, {
// 追加
setTagPosts (state: RootState, payload: RelatedPosts) {
state.tagPosts = payload
}
})
export const actions = actionTree({ state, getters, mutations }, {
async nuxtServerInit ({ dispatch }, _nuxtContext: Context) {
await dispatch('getContentfulApi')
dispatch('getCategoryPosts')
// 追加
dispatch('getTagPosts')
},
...
// 追加
// tagId別のBlogPost配列を作成する
getTagPosts ({ state, commit }) {
const tagPosts: RelatedPosts = {}
for (const tag of state.tags) {
const tagId: string = tag.sys.id
tagPosts[tagId] = []
for (const post of state.posts) {
// postにtagsが存在しない場合は次のループへ
if (!post.fields.tags) {
continue
}
// post.fields.tagsのtagID配列を作成
const postTagIds: string[] =
post.fields.tags.map((tag: BlogTag) => tag.sys.id)
// tagID配列にtagのIDが存在する場合配列にpush
if (postTagIds.includes(tag.sys.id)) {
tagPosts[tagId].push(post)
}
}
}
commit('setTagPosts', tagPosts)
}
})
以上でタグに関連づく記事一覧のオブジェクトが作成できました。
tagPosts
の実態は以下のようになります。
タグコンテンツページにtagPostsを表示しよう
asyncData()
から、tagPosts
を取得し、Html内でループします。
pages/tags/_slug.vue
<template>
<v-container>
...
<h2>
タグに関連づく記事
</h2>
<!-- 以下、追加 -->
<ul>
<li
v-for="(post, postI) in tagPosts"
:key="`post-${postI}`"
>
{{ post.fields.title }}
<ul
class="d-flex"
style="list-style: none"
>
<li
v-for="(tag, tagI) in post.fields.tags"
:key="`tag-${tagI}`"
class="mr-2"
>
<nuxt-link
:to="$my.linkTo(tag)"
>
{{ tag.fields.title }}
</nuxt-link>
</li>
</ul>
</li>
</ul>
</v-container>
</template>
<script lang="ts">
...
// BlogPost 追加
import { BlogTag, BlogPost } from '~/store/types'
type AsyncData = void | {
tag: BlogTag
// 追加
tagPosts: BlogPost[]
}
@Component
export default class TagsSlugPage extends Vue {
...
asyncData ({ app: { $accessor }, params, $my }: Context): AsyncData {
const tag = $accessor.tags.find((tag: BlogTag) =>
tag.fields.slug === params.slug
)
if (!tag) {
return $my.errorHandler(404)
}
// 追加
const tagPosts: BlogPost[] = $accessor.tagPosts[tag.sys.id]
return {
tag,
// 追加
tagPosts
}
}
}
</script>
ブラウザで確認しよう
タグのコンテンツページにアクセスしてください。(筆者の場合: http://localhost:3000/tags/nuxtjs)
タグが関連づいた記事一覧が表示されました。
記事に紐づいたのタグの配列にも、同じタグが表示されているか確認してください。
以上で全てのコンテンツページが作成できました。
Netlifyにデプロイする
ここまでの編集を本番環境にデプロイしましょう。
% yarn netlify:deploy
generate
コマンドが走ると全てのルートが生成されるのが分かります。
今回の作業は以上です。
% git add -A
% git commit -m "Add tag index and content page"
まとめと次回
今回はタグ一覧ページとタグのコンテンツページを作成しました。
次回はプライバシーポリシーページを作成します。