Nuxt3とmicroCMSで作るブログ ⑥タグでの絞り込み

Nuxt3とmicroCMSで作るブログシリーズの6番目の記事です。
今回はタグでの絞り込みを実装していきます。

ルートの追加

まずは、タグで絞り込まれた場合のルートを追加します。
nuxt.config.tsのhookに追記します。

import { defineNuxtConfig } from 'nuxt'
import { resolve } from 'path'
import { createCommonJS } from 'mlly'
const { __dirname } = createCommonJS(import.meta.url)

const { API_KEY, SERVICE_DOMAIN } = process.env;

// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
   ...
    hooks: {
        'pages:extend'(pages) {
            pages.push({
                name: 'page',
                path: '/page/:p',
                file: resolve(__dirname, 'client/pages/index.vue')
            }),
            // 追加
            pages.push({
                name: 'tag',
                path: '/tag/:tagId/page/:p',
                file: resolve(__dirname, 'client/pages/index.vue')
            })
        }
    },
})


こうしておくことで、例えば/tag/my-pretty-tag/page/1にアクセスされた際に、route.params.tagIdとすればmy-pretty-tagというタグIDが取り出せるようになります。

タグ一覧取得API


おなじみでserver/api配下に作成していきます。

// client/server/api/tagList.ts
import type { IncomingMessage, ServerResponse } from 'http'
import client from './client'
import { Tag } from './types'


export default async (req: IncomingMessage, res: ServerResponse) => {

    const data = client.getList<Tag>({
        endpoint: 'tag',
        queries: {
            fields: 'id,name',
            // これより増えることはないだろう、という値
            limit: 30,
        },
    })

    return data
}


記事一覧と違ってタグは基本的にすべて表示させます。
なので、limitはこれより増えることはないだろう、という値を設定しています。
この辺を動的にやるとすると、少し面倒そうです。

タグ一覧コンポーネンツ

次にTags.vueコンポーネンツを作成していきます。

<!-- client/components/Tags.vue -->
<script setup lang="ts">

const props = defineProps({
    tags: Object,
})

</script>

<template>
    <div class="wrapper">
        <h1 class="pageTitle">タグ</h1>
        <ul>
            <li v-for="tag in tags.contents" :key="tag.id" class="list">
                <NuxtLink :to="`/tag/${tag.id}/page/1`" class="link">
                    {{ tag.name }}
                </NuxtLink>
            </li>
        </ul>
    </div>
</template>

<style scoped>
.wrapper {
    padding: 20px 0;
}

.pageTitle {
    font-size: 2rem;
    font-weight: bold;
    color: #5ba9f7;
    background-color: #c8e4ff;
    padding: 6px 10px;
    margin-bottom: 10px;
    border-radius: 5px;
}

.list {
    border-bottom: 1px solid #eee;
    list-style-type: none;
}

.link {
    display: block;
    padding: 10px;
    color: #888;
    font-size: 1.6rem;
}
</style>


親からタグオブジェクトを受け取り、タグ一覧とリンクを表示させるだけのシンプルな作りです。

index.vue,APIの編集

次にトップページと記事一覧APIを編集していきます。
以下の機能を追加します。

  • routeからタグIDを取得
  • タグIDに応じて表示記事をフィルタする
  • タグ一覧を取得してTag.vueに渡す


<!-- client/pages/index.vue -->
<script setup lang="ts">

const route = useRoute()
const page: number = Number(route.params.p || 1)

// routeからタグIDを取得
const selectedTagId: string = String(route.params.tagId == undefined ? '' : route.params.tagId)

const limit: number = 5
const offset: number = (page - 1) * limit
const params: object = { limit: limit, offset: offset }

// タグIDがあればparamsに追加
if (selectedTagId) {
    params["tagId"] = selectedTagId
}

const { data: posts } = await useFetch('/api/postList', { params: params })
// タグ一覧の取得
const { data: tags, refresh } = await useFetch(`/api/tagList`)


// ページ数の計算
const totalCount: number = posts.value.totalCount
const numPages: number = Math.ceil(totalCount / limit)

</script>

<template>
    <div>
        <div class="divider">
            <section class="container">
                <!-- 記事一覧 -->
                <PostList :posts="posts" />
            </section>
            <aside class="aside">
                <!-- キーワード検索、タグ一覧 -->
                <SearchForm />
                <!-- 追加 -->
                <Tags :tags="tags" />
            </aside>
        </div>
        <Pagination :numPages="numPages" :current="page" />
    </div>
</template>


先ほど作成したタグ一覧コンポーネントを配置しつつ、URLからタグIDを取得してparamに加えるようにしています。
タグIDが存在しないこともあるのでif分でparamに追加するようにしました。

// タグIDが選択されているときはパラメーターに加える
if (selectedTagId) {
    params["tagId"] = selectedTagId
}


次に記事一覧APIでもタグIDをキャッチできるようにしていきます。

// client/server/api/postList.ts
import type { IncomingMessage, ServerResponse } from 'http'
import client from './client'
import { Post } from './types'
import * as url from "url";

export default async (req: IncomingMessage, res: ServerResponse) => {
    const params = url.parse(req.url as string, true).query

    const queries = {
        fields: 'id,title,publishedAt,tag',
        limit: Number(params.limit),
        offset: Number(params.offset)
    }

    // タグIDがキーに存在するときはqueryを加える
    if ("tagId" in params) {
        queries["filters"] = `tag[contains]${params.tagId}`
    }

    const data = client.getList<Post>({
        endpoint: 'post',
        queries: queries
    })

    return data
}


この段階でタグでの絞り込みが行えるようになります。
タグプログラミングを選択した状態


パンくずリストで選択中タグを表示する

いまのままですと、ユーザーが現在選択中のタグが良く分かりません。
パンくずリストを使って表示させましょう。

<!-- client/components/Breadcrumb.vue -->
<script setup lang="ts">

const props = defineProps({
  tag: Object,
})

function hasTag(arg: object) {
  return Object.keys(arg).length > 0
}

</script>

<template>
  <ul class="breadcrumb">
    <li class="breadcrumbList">
      <nuxt-link to="/">記事一覧</nuxt-link>
    </li>
    <li v-if="hasTag(tag)" class="breadcrumbList">
      <nuxt-link :to="`/tag/${tag.id}/page/1`">{{ tag.name }}</nuxt-link>
    </li>
  </ul>
</template>


<style scoped>
ul {
  list-style-type: none;
  margin-bottom: 2rem;
}

.breadcrumb {
  display: flex;
  flex-wrap: wrap;
}

.breadcrumbList {
  color: #616269;
  font-size: 1.4rem;
}

.breadcrumbList a {
  color: #331cbf;
}

.breadcrumbList::after {
  content: '>';
  margin: 0 10px;
}

.breadcrumbList:last-child::after {
  content: '';
  margin: 0;
}

@media (max-width: 600px) {
  .breadcrumbList {
    font-size: 14px;
  }
}
</style>


次にindex.vueから呼び出します。
選択中のタグIDを元にタグオブジェクトを特定してパンくずコンポーネントに渡します。

<!-- client/pages/index.vue -->
<script setup lang="ts">
...
// routeからタグIDを取得
const selectedTagId: string = String(route.params.tagId == undefined ? '' : route.params.tagId)
...
// タグ一覧の取得
const { data: tags, refresh } = await useFetch(`/api/tagList`)

// 追加 選択中のタグに応じてタグを特定
// 選択中のタグがないときは空のオブジェクト
const selectedTagObject: object =
    selectedTagId !== '' ?
        tags.value.contents.find((content) => content.id === selectedTagId) : {};
...
</script>

<template>
    <div>
        <div class="divider">
            <section class="container">
                <!-- パンくずリスト -->
                <Breadcrumb :tag="selectedTagObject" />
                <!-- 記事一覧 -->
                <PostList :posts="posts" />
            </section>
            <aside class="aside">
               ...
            </aside>
        </div>
        <Pagination :numPages="numPages" :current="page" />
    </div>
</template>


このように表示されます。


タグありのページネーション

だいたいできてきましたが、タグありのページネーションも追加する必要があります。
いまのままですと、タグを絞り込んだ状態で2ページ目に遷移した際に、絞り込みが解除されてしまいます。
これは簡単で現在のページネーションのコンポーネントを少し編集するだけです。

<!-- client/components/Pagination.vue -->
<script setup lang="ts">

const props = defineProps({
    numPages: Number,
    current: Number,
    // タグIDを受け取るようにする
    selectedTagId: String,
})

// タグIDがある場合とそうでない場合で返すリンクを分岐
function getPath(p: number) {
    if (props.selectedTagId) {
        return `/tag/${props.selectedTagId}/page/${p}`
    } else {
        return `/page/${p}`
    }
}

</script>

<template>
    <div class="pagination">
        <NuxtLink v-for="num in numPages" :key="num" :class="[num == current ? 'current' : 'link']" :to="getPath(num)">
            {{ num }}
        </NuxtLink>
    </div>
</template>


あとは親からPaginationに対してタグIDを渡すだけです。

<!-- client/pages/index.vue -->
<template>
    <div>
        <div class="divider">
          ...
        </div>
        <Pagination :numPages="numPages" :current="page" :selectedTagId="selectedTagId" />
    </div>
</template>


おわり

こちらで一旦このシリーズを終了にします。

TOPページ