ブログトップ画像

Next.js × microCMSでブログの検索を実装してみた

フロントエンド

おはこんばんは。最近、 Next.js × microCMS で自作したこのサイトにブログの検索機能を実装してみました。

ブログでは、サイト内検索のフォームをよく見かけますよね。ただ、全文検索の機能等を実装する必要があり、意外と面倒だったりします。

そんな面倒なことも、microCMS のAPIは全文検索機能を備えているので、簡単に実装出来てしまいます。今回はその全文検索機能を用いたブログの検索機能の実装方法をご紹介します。

実行環境

  • Next.js 10.0.4
  • React 17.0.1


グローバルなStateの管理

まずは、複数のドメインでキーワードを扱うことを想定し、グローバルなStateで管理するようにします。今回は、React Context を用いた方法でご紹介します。

createContext で以下のように Context を定義します。

// context/searchContext.ts

import { createContext } from 'react';

interface SearchContextValue {
  search: string;
  setSearch: React.Dispatch<React.SetStateAction<string>>;
}

export const SearchContext = createContext<SearchContextValue>({
  search: '',
  setSearch: () => undefined,
});


次に _app.tsx で Context を呼び出して、グローバルで管理するStateをセットします。

// pages/_app.tsx

import React, { useState } from 'react';
import { AppProps } from 'next/app';

const MyApp = (props: AppProps): JSX.Element => {
  const { Component, pageProps } = props;
  const [search, setSearch] = useState<string>('');

  return (
    <SearchContext.Provider value={{ search, setSearch }}>
        <Component {...pageProps} />
    </SearchContext.Provider>
  );
};

export default MyApp;


これでグローバルでStateを扱えるようになりました。

検索用のコンポーネントを用意

以下のように検索用のコンポーネントを用意します。今回は Material-UI を用いています。

// components/SearchInput

import React, { useCallback, useContext } from 'react';
import { IconButton, InputBase } from '@material-ui/core';
import SearchIcon from '@material-ui/icons/Search';
import { useRouter } from 'next/router';
import style from './SearchInput.module.scss';
import SearchContext from '../../context/searchContext';

const SearchInput: React.FC = () => {
  const { search, setSearch } = useContext(SearchContext);
  const router = useRouter();

  const handleChangeKeyword = useCallback(
    (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
      const { value } = e.currentTarget;
      setSearch(value);
    },
    [setSearch],
  );

  const handleClickSearchButton = useCallback(() => {
    void router.push(`/blogs/search/?keyword=${search}`);
  }, [search, router]);

  const handleKeyDownSearch = useCallback(
    (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
      if (e.key === 'Enter') {
        void router.push(`/blogs/search/?keyword=${search}`);
      }
    },
    [search, router],
  );

  return (
    <div className={style.search}>
      <InputBase
        placeholder="Search…"
        inputProps={{ 'aria-label': 'search' }}
        className={style.searchInput}
        value={search}
        onChange={handleChangeKeyword}
        onKeyDown={handleKeyDownSearch}
      />
      <IconButton
        className={style.searchIcon}
        onClick={handleClickSearchButton}
        aria-label="Search"
      >
        <SearchIcon />
      </IconButton>
    </div>
  );
};

export default SearchInput;


先ほど、定義した Context で定義したグローバルなStateは以下のように useContext で呼び出しています。

const { search, setSearch } = useContext(SearchContext);


handleClickSearchButtonhandleKeyDownSearch で検索結果のページへ遷移するようにしています。

ポイントは、検索キーワードをURLクエリに入れていることです。Context で入れているから不要では?と思いますよね。

ただ、ブラウザをリロードすると Context のは値が消えてしまいます。なので、リロードされても検索結果が残るようにURLクエリにキーワードを入れています。

void router.push(`/blogs/search/?keyword=${search}`);


ページ側の処理

ページ側は先ほどセットしたURLクエリを元にブログの一覧を取得します。フロントから直接 microCMS のAPIを叩いてしまうと、API keyが漏洩してしまうので、API Routerを叩くようにしています。

const [blogsQuery, setBlogsQuery] = useState<{ keyword:string } | null>(null);

useEffect(() => {
  const urlQuery = router.query { keyword:string };

  if (!isEmpty(urlQuery)) setBlogsQuery(urlQuery);
}, [router.query]);

useEffect(() => {
  if (blogsQuery) {
    void (async (): Promise<void> => {
      try {
          const res = await fetch(`/api/blogs`, {
            body: JSON.stringify(blogsQuery),
          });
      } catch {
        return;
      }
    })();
  }
}, [blogsQuery]);


一応、URLクエリがないページに遷移した際に検索キーワードが消えるように _app.tsx に以下の処理も入れておくと良いでしょう。

// pages/_app.tsx

  useEffect(() => {
    const urlQuery = router.query { keyword:string };
    if (urlQuery && urlQuery.keyword) {
      setSearch(urlQuery.keyword);
    } else {
      setSearch('');
    }
  }, [router]);


API Routerでブログ一覧を取得する

以下のようにAPI Router を用いてサーバーサイド側から microCMS のAPIを叩くようにします。

// pages/api/blogs/index.ts

import { NextApiResponse, NextApiRequest } from 'next';

const isBlogsQuery = (item: unknown): item is { keyword: string } => {
  const target = item as { keyword: string };
  return 'keyword' in target && typeof target.keyword === 'string';
};

const getSearchBlogs = async (
  req: NextApiRequest,
  res: NextApiResponse,
): Promise<void> => {
  // クエリのチェック
  if (!isBlogsQuery(req.body)) {
    return res.status(404).end();
  }

  const key = {
    headers: { 'X-API-KEY': process.env.API_KEY ?? '' },
  };
  const blogs = await fetch(
    `https://your-service.microcms.io/api/v1/blogs?q=${encodeURI(
      req.body.keyword,
    )}`,
    key,
  )
    .then((res) => res.json())
    .catch(() => null);

  return  res.status(200).json(blogs);
};

export default getSearchBlogs;


microCMS ではqで全文検索が行えます。検索対象は「テキストフィールド」「テキストエリア」「リッチエディタ」となるので、必要なフィールドに対して検索してくれます。便利ですね。

日本語などのキーワードはencodeURIをしないといけなので、ご注意ください。

`https://your-service.microcms.io/api/v1/blogs?q=${encodeURI(
      req.body.keyword,
    )}`


これで検索されたキーワードに関連するブログが取得出来るようになったはずです。お疲れ様でした。

さいごに

今回は microCMS のAPIは全文検索機能を用いて、ブログのサイト内検索を実装する方法をご紹介しました!誰かの参考になれば幸いです。