Nuxt.jsmicroCMS

Nuxt3とmicroCMSで作るブログ ⑤キーワード検索

公開日:2022-04-30 更新日:2023-06-11

Nuxt3とmicroCMSで作るブログシリーズの5番目の記事です。

今回はキーワード検索を実装していきます。

検索の流れ

以前ですがNuxt2でブログの記事にしたことがありました。

Nuxt.js + microCMS キーワード検索を実装する方法 (Netlify Function 使用)

Nuxt3でも似たような流れで行っていきます。

  • 検索ボックスコンポーネントを作る
  • 検索を実行したら結果表示ページに遷移
  • クエリをサーバーに投げて絞り込み

Nuxt2では結構ややこしくて、Netlify Functionを使ったりしたのですが、Nuxt3では自前でサーバーサイドのAPIを作れます。

なので、非常に処理がシンプルになったという印象です。

検索フォームコンポーネント

まずはSearchForm.vueを作成して以下のようにします。

<!-- src/components/SearchForm.vue -->
<script setup lang="ts">

type Props = {
    keyword?: string
}

const { keyword } = defineProps<Props>()

const query = ref(keyword)

function canSubmit() {
    // 空白もしくはスペースのみの場合false
    return !!query.value && !/^\s+$/.test(query.value)
}

function submit() {
    if (canSubmit()) {
        return navigateTo({
            path: '/search',
            query: {
                q: query.value
            }
        })
    }
}

</script>
    
<template>
    <form class="search-form" @submit.prevent="submit">
        <input type="text" v-model="query" ref="searchForm" placeholder="Keyword">
    </form>
</template>
    
<style scoped>
input[type=text] {
    font-size: 14px;
    padding: 4px 8px;
    box-sizing: border-box;
    border-radius: 10px;
    border: solid 1px #ccc;
    background-color: #fff;
    font-family: "Ubuntu", "Noto Sans JP", sans-serif;
    display: inline-block;
    width: 100%;
    height: 28px;
}

input[type=text]:focus {
    outline: 0;
    box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
</style>

色々と書いていますが、テキストボックスを表示して、検索ようのsearch.vueに遷移させるだけのコンポーネントです。

function canSubmit() {
  return !!query.value && !/^\s+$/.test(query.value)
}

ここの部分は空白繰り返しではない時にtrueとなる正規表現です。

`!/^\s+$/.test(query.value)

基本的な正規表現一覧

また、nuxt3ではページを遷移させる際に、router.push()ではなくて、navigateTo()と書くようです。

return navigateTo({
      path: '/search',
      query: {
        q: query.value
      }
    })

変数queryはscript内で宣言された値なので、query.valueとして値を取り出す点が注意です。

次に親コンポーネントから読み込んでおきましょう。

<!-- src/components.Home.vue -->
<template>
    <div>
        <div class="divider">
            <section class="container">
                <PostList :posts="posts.contents" />
            </section>
            <aside class="aside">
                <!-- 追加 -->
                <SearchForm />
            </aside>
        </div>
        <Pagination :numPages="numPages" :current="page" />
    </div>
</template>

検索結果表示ページの作成

次にpagesディレクトリにsearch.vueを作成します。

<!-- src/pages/search.vue -->
<script setup lang="ts">
import { MicroCMSQueries } from 'microcms-js-sdk'

const route = useRoute()
const query = String(route.query.q)
const queries: MicroCMSQueries = {
    q: query,
    orders: '-publishedAt',
}

const { data: posts } = await useFetch('/api/postList', { params: queries })

// queryが変化した場合にページをリロードする
watch(() => route.query, () => location.reload())

</script>

<template>
    <div class="wrapper">
        <SearchForm :keyword="query" />
        <div class="results">
            <div v-if="posts.contents">
                <PostList :posts="posts.contents" />
            </div>
            <div v-if="posts.contents.length == 0">
                <h1 class="no-result">お探しの記事は見つかりませんでした。</h1>
            </div>
        </div>
    </div>
</template>

<style scoped>
.wrapper {
    padding-top: 112px;
    position: relative;
    width: 960px;
    margin: 0 auto 0;
}

.results {
    margin-top: 3rem;
}

.no-result {
    text-align: center;
    font-size: 2.4rem;
    margin-bottom: 30px;
}

@media (max-width: 1024px) {
    .wrapper {
        max-width: 600px;
        position: relative;
        width: 100%;
        padding-right: 1rem;
        padding-left: 1rem;
        margin-right: auto;
        margin-left: auto;
    }
}
</style>

まずsearch.vueでは、SearchForm.vueで入力された検索キーワードを受け取っています。

const query = String(route.query.q)

そして、microCMS APIのキーワード検索queryを伴って、一覧取得APIにリクエストを送ることで、検索結果が返ってきます。

const queries: MicroCMSQueries = {
    q: query,
    orders: '-publishedAt',
}

const { data: posts } = await useFetch('/api/postList', { params: queries })

試しにブログと入力してトップページから検索を実行すると、以下のように表示がされます。

場合によってはこのページからさらに検索したいということがあると思います。

Nuxt3ではデフォルトでクエリの変化時にはページをリロードしないらしいです。

なので、以下のwatch文を記述することでクエリ変化時にリロードして再検索をかけるようにしています。

// queryが変化した場合にページをリロードする
watch(() => route.query, () => location.reload())

検索結果もページングする

Nuxt2ではこの実装が難しくて挫折した経験があるのですが、Nuxt3では比較的簡単に実装ができました。

記事一覧ページと同様にlimitとoffsetを伴ってクエリを投げるようにしていきます。

まず、search.vueのscriptを以下のように変更します。

<!-- src/pages/search.vue -->
<script setup lang="ts">
import { MicroCMSQueries } from 'microcms-js-sdk'
import { BLOG_PER_PAGE } from '../settings/siteSettings';

const route = useRoute()
const query = String(route.query.q)
// 追加 ページ遷移時にページ情報を受け取る
const page = Number(route.query.page || 1)
const limit = BLOG_PER_PAGE
// ページ情報を持たせる
const queries: MicroCMSQueries = {
    q: query,
    orders: '-publishedAt',
    limit: limit,
    offset: (page - 1) * limit,
}

const { data: posts } = await useFetch('/api/postList', { params: queries })

const totalCount = posts.value.totalCount
const numPages = Math.ceil(totalCount / limit)

// queryが変化した場合にページをリロードする
watch(() => route.query, () => location.reload())


</script>

例えばhogeと検索した場合に/search?q=hoge&page=2というようなクエリパラメーターを付けて遷移させるイメージです。

こうすることでページ情報とキーワード情報を伴ってAPIを投げることができます。

なので、ページネーションコンポーネントで、そのようなルールにのっとったリンクを生成すればよいです。

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

type Props = {
    numPages: number;
    current: number;
    // オプショナルで追加
    keyword?: string;
}

const { numPages, current, keyword } = defineProps<Props>();

function getPath(p: number) {
    // 追加 キーワードありのリンク
    if (keyword) return `/search?q=${keyword}&page=${p}`
    return `/page/${p}`
}

function getClass(page: number, current: number) {
    if (page == current) return 'current'
    return 'link'
}

</script>

<template>
    <div class="pagination">
        <NuxtLink v-for="page in numPages" :key="page" :class="getClass(page, current)" :to="getPath(page)">
            {{ page }}
        </NuxtLink>
    </div>
</template>

<style scoped>
.pagination {
    position: relative;
    width: 100%;
    margin: 8em 0 8rem;
    font-family: 'Open Sans', sans-serif;
    font-weight: 300;
    line-height: 1.1;
    text-align: center;
    vertical-align: middle;
}

.current,
.link {
    display: inline-block;
    margin: 0 2rem;
    padding: 2px 0;
    text-align: center;
    font-size: 3rem;
    font-weight: lighter;
}

.current {
    color: #000;
}

.link {
    color: #A2A2A6;
}
</style>

つぎにsearch.vueでページネーションコンポーネントを読み込みます。

<template>
    <div class="wrapper">
        <SearchForm :keyword="query" />
        <div class="results">
            <div v-if="posts.contents">
                <PostList :posts="posts.contents" />
                <!-- 追加 -->
                <Pagination :numPages="numPages" :current="page" :keyword="query" />
            </div>
            <div v-if="posts.contents.length == 0">
                <h1 class="no-result">お探しの記事は見つかりませんでした。</h1>
            </div>
        </div>
    </div>
</template>

適当な検索ワードで、例えばと入力した場合。

このような形で表示されます。

2ページ目のリンクをクリックした場合、PaginationのgetPath関数が呼ばれます。

function getPath(p: number) {
    // 追加 キーワードありのリンク
    if (keyword) return `/search?q=${keyword}&page=${p}`
    return `/page/${p}`
}

ここで、/search?q=こ&page=2というリンクに遷移します。

あとはここから一覧ページと同様にoffsetを計算してサーバーに投げれば、応じた結果が返ってきます。

次回はタグでの絞り込みを実装していきます。

Twitter Share