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

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

microCMSNext.jsTypeScriptMaterial-UI

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

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

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

実行環境

  • Next.js 10.0.4
  • React 17.0.1

グローバルなStateの管理

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

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

context/searchContext.ts
1import { createContext } from 'react';
2
3interface SearchContextValue {
4 search: string;
5 setSearch: React.Dispatch<React.SetStateAction<string>>;
6}
7
8export const SearchContext = createContext<SearchContextValue>({
9 search: '',
10 setSearch: () => undefined,
11});
12

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

pages/_app.tsx
1import React, { useState } from 'react';
2import { AppProps } from 'next/app';
3
4const MyApp = (props: AppProps): JSX.Element => {
5 const { Component, pageProps } = props;
6 const [search, setSearch] = useState<string>('');
7
8 return (
9 <SearchContext.Provider value={{ search, setSearch }}>
10 <Component {...pageProps} />
11 </SearchContext.Provider>
12 );
13};
14
15export default MyApp;
16

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

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

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

components/SearchInput.tsx
1import React, { useCallback, useContext } from 'react';
2import { IconButton, InputBase } from '@material-ui/core';
3import SearchIcon from '@material-ui/icons/Search';
4import { useRouter } from 'next/router';
5import style from './SearchInput.module.scss';
6import SearchContext from '../../context/searchContext';
7
8const SearchInput: React.FC = () => {
9 const { search, setSearch } = useContext(SearchContext);
10 const router = useRouter();
11
12 const handleChangeKeyword = useCallback(
13 (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
14 const { value } = e.currentTarget;
15 setSearch(value);
16 },
17 [setSearch],
18 );
19
20 const handleClickSearchButton = useCallback(() => {
21 void router.push(`/blogs/search/?keyword=${search}`);
22 }, [search, router]);
23
24 const handleKeyDownSearch = useCallback(
25 (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
26 if (e.key === 'Enter') {
27 void router.push(`/blogs/search/?keyword=${search}`);
28 }
29 },
30 [search, router],
31 );
32
33 return (
34 <div className={style.search}>
35 <InputBase
36 placeholder="Search…"
37 inputProps={{ 'aria-label': 'search' }}
38 className={style.searchInput}
39 value={search}
40 onChange={handleChangeKeyword}
41 onKeyDown={handleKeyDownSearch}
42 />
43 <IconButton
44 className={style.searchIcon}
45 onClick={handleClickSearchButton}
46 aria-label="Search"
47 >
48 <SearchIcon />
49 </IconButton>
50 </div>
51 );
52};
53
54export default SearchInput;

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

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

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

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

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

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

ページ側の処理

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

1const [blogsQuery, setBlogsQuery] = useState<{ keyword:string } | null>(null);
2
3useEffect(() => {
4 const urlQuery = router.query { keyword:string };
5
6 if (!isEmpty(urlQuery)) setBlogsQuery(urlQuery);
7}, [router.query]);
8
9useEffect(() => {
10 if (blogsQuery) {
11 void (async (): Promise<void> => {
12 try {
13 const res = await fetch(`/api/blogs`, {
14 body: JSON.stringify(blogsQuery),
15 });
16 } catch {
17 return;
18 }
19 })();
20 }
21}, [blogsQuery]);

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

pages/_app.tsx
1 useEffect(() => {
2 const urlQuery = router.query { keyword:string };
3 if (urlQuery && urlQuery.keyword) {
4 setSearch(urlQuery.keyword);
5 } else {
6 setSearch('');
7 }
8 }, [router]);
9

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

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

pages/api/blogs/index.ts
1import { NextApiResponse, NextApiRequest } from 'next';
2
3const isBlogsQuery = (item: unknown): item is { keyword: string } => {
4 const target = item as { keyword: string };
5 return 'keyword' in target && typeof target.keyword === 'string';
6};
7
8const getSearchBlogs = async (
9 req: NextApiRequest,
10 res: NextApiResponse,
11): Promise<void> => {
12 // クエリのチェック
13 if (!isBlogsQuery(req.body)) {
14 return res.status(404).end();
15 }
16
17 const key = {
18 headers: { 'X-API-KEY': process.env.API_KEY ?? '' },
19 };
20 const blogs = await fetch(
21 `https://your-service.microcms.io/api/v1/blogs?q=${encodeURI(
22 req.body.keyword,
23 )}`,
24 key,
25 )
26 .then((res) => res.json())
27 .catch(() => null);
28
29 return res.status(200).json(blogs);
30};
31
32export default getSearchBlogs;
33

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

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

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

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

さいごに

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