Gatsby FunctionsとmicroCMSのqパラメーターでサイト内検索機能を実装

# Gatsby FunctionsとmicroCMSのqパラメーターでサイト内検索機能を実装するには

### 序文1. JAMstackアーキテクチャーでサイト内検索機能
Jamstackアーキテクチャーで構成しているWebサイトにおいて、検索機能はなかなか悩みの種です。Gatsbyの場合、①gatsby-plugin-localsearchプラグインをインストールし、GraphQLにインデックスを持たせ、GraphQLクエリで検索させる、といった方法や、サードパーティーの②Algolia利用が一般的でした。

:::note info
① 試したところ、Gatsbyバージョン４への更新が追い付いていないせいかバグエラーが出まくり頓挫しました。
② Algolia利用は、最適解とはいえないものの一つの有益な方法です。とはいえGatsby＋microCMSで構成しているWebサイトがさらに複雑化することにはなります。
:::


### 序文2. 新しい潮流、Gatsby FunctionsとmicroCMSのｑパラメーター

Gatsbyに、バージョン４から（正確には3.7から）**Gatsby Functions**というAPIを実装できる機能が追加されました。Nextをご存じであれば、Nextの**API Routes**と同じものです。

一方、ヘッドレスCMSであるmicroCMSは、2021年末頃から**qパラメーター**という、microCMSのコンテンツに対し**全文検索**を行える機能を追加してきました。


この二つの組み合わせによって、**ようやく『サイト内検索機能』をGatsbyプロジェクトにスマートに（簡単にとは申しませんがスマートに）実装可能となりました**。ここではその手順について、基本的な実装方法を記しておきます。

:::note info
【Prerequisite（前提）】
+ すでにmicroCMSにコンテンツAPIが作成されている
+ すでにgatsby-cliがインストールされておりGatsbyプロジェクトが作成されている。
+ すでにGatsbyプロジェクトでmicroCMSのコンテンツAPIが表示されるところまでgatsby node api等の設定・コーディングが済んでいる。

:::


### 序文2-1. Gatsby Functionsとは。
srcフォルダ配下に**apiフォルダ**を作成し、apiフォルダに任意の名前のJSファイルやTSファイルを作成しコーディングするだけでAPIの作成が可能になりました。すべてのケースで、とはもちろんいかないでしょうが、理屈上はAWS Lambdaやgoogle Cloud Functionsはもう必要なくなりました。

:::note warn
※ 公式マニュアルでは、Gatsby Functionsを公開して利用するには**Gatsby Cloud**へのデプロイが必要です。
:::

簡単なAPIを作成してみましょう。

文字列"hello world!"を返すAPI、hello.jsを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/27ad2b83-90e5-6194-9835-d22c19149354.png)


以下のようにコーディング。
```js
//api/hello.js
export default function handler(req, res) {  
    res.status(200).json({ message: 'Hello World!' });
}
```

ブラウザでlocalhost:8000/api/helloにアクセス。

ブラウザに次のように表示されました。
JSONデータ{ message: 'Hello World!' }を返すAPIの完成です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/a9ee0ef8-c3df-b6c1-77d7-409e869eb4cb.png)


### 序文2-2. microCMSのqパラメーターとは。


microCMSのコンテンツに対し、**qパラメーター**を使うことで全文検索が実現できるようになりました。検索対象となるフィールドは、その種類が「テキストフィールド」「テキストエリア」「リッチエディタ」のものです。

microCMSの管理画面の「APIプレビュー画面」でその挙動を確認できます。

下画面は、keyとしてqパラメータを選択し、Value値（検索ワード）を入力して「取得」ボタンをクリックし、返ってきたレスポンスです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/4e176b16-a8b3-800b-2290-2009561fcd11.png)


## 実装手順１．検索APIを作成
では、これらを使ってまずは検索APIを作成してみましょう。

次のコマンドラインでを**microcms-js-sdk**をインストールします。

```bash
$ npm install microcms-js-sdk 
```

apiフォルダに**search.js** ファイルを新規作成し、以下のようにコーディングします。

```js
// api/search.js
import { createClient } from 'microcms-js-sdk';

export default async function formHandler(req, res) {
    const keyword = req.query.keyword;

    const client = createClient({
        serviceDomain: "process.env.MICROCMS_SERVICEDOMAIN",
        apiKey: process.env.MICROCMS_APIKEY,
    });

    const response = await client
        .get({
            endpoint: 'information',
            queries: {
              q: decodeURI(keyword)
            },
        })
        return res.status(200).json(response)
}
```

#### 実装手順1-1. api/search.jsコード解説

- 検索フォームで入力したキーワードは`req.query.keyword`に入ってきます。それを定数**keyword**に代入し、 `{ q: decodeURI(keyword) }` といった形でqパラメーターのvalue値とします（`decodeURL()`はマルチバイトである日本語をURLエンコードするために使用しています）。

- **microcms-js-sdk**からインポートした`createClient`に、microCMSの**サービスドメイン（serviceDomain）**と**APIキー**を指定することで`client`オブジェクト作成し、`client.get`でmicroCMSにクエリを投げています。

- `client.get`に指定するパラメーターは、**endpoint**とqueries オプションである`queries: { q: decodeURI(keyword) }`になります。


- 最後に、`return res.status(200).json(response)`で検索結果を返します。

> **サービスドメイン（serviceDomain）**、**APIキー**、**エンドポイント（endpoint**）とは何か？それらの取得方法は？につきましては、microCMSをご利用でしたらお分かりかと存じますが、あるいは拙書『[JAMStackを学ぼう Gatsby4＋microCMSでつくるコーポレートサイト 後編 －ペーパーバック版](https://www.amazon.co.jp/gp/product/B08KRPTW2R/ref=as_li_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=B08KRPTW2R&linkCode=as2&tag=kokkahasan-22&linkId=5ad235bbd59c9c43c5492687fb840791)』をお手に取ってみて下さい。

<br />


これで検索API"**api/search**" 完成です！


## 実装手順２．検索フォームを作成

次に検索フォームを作っていきましょう。


まず、**axios**をインストールします。

```cmd
$ npm install axios
```

そして、pagesフォルダ配下に**searchform.js**ファイルを新規作成し、以下のようにコーディングします。

<details><summary><strong>pages/searchform.jsコード全文</strong></summary>

```javascript
// pages/searchform.js
import * as React from "react"
import axios from "axios";
import { Link, graphql } from "gatsby"
import Layout from "../components/layout"

export default function SearchForm() {
    const [keyword, setKeyword] = React.useState("");
    const [infos, setInfos] = React.useState([]);
    const [action, setAction] = React.useState(false)
  
    const searchInfos = async () => {
      // 検索APIにリクエストを送信
      const res = await axios.get("/api/search", {
        params: {
          keyword,
        },
      });
      // 検索結果をステート変数にセット
      setInfos(res.data.contents);

      // 検索を実行した場合actionフラッグをtrueに
      setAction(true)
    };
  
    return (
      <Layout>
          {/* コンテナ */}
          <div class="container w-full grid grid-cols-12 mx-auto gap-2">

          {/* 表題ブロック */}
            <div class="col-span-12 bg-indigo-400 text-xl text-white p-2 mt-10">
                検索ページ
            </div>
            
          {/* 検索フォームブロック */}
            <div class="flex flex-col col-span-12 items-center">
                <div class="relative w-full sm:w-full md:w-1/2 lg:w-1/3 mt-5">
                    <input type="text"
                           value={keyword}
                           onChange={(e) => setKeyword(e.target.value)}
                           class="
                            block p-2 pl-2 w-full text-md 
                            text-gray-900 bg-gray-50 rounded-lg 
                            border border-gray-300 
                            focus:ring-blue-500 focus:border-blue-500
                           "
                    />
                    <button onClick={searchInfos} 
                            disabled={!keyword}
                            class="
                            text-white absolute right-2.5 bottom-1 
                            bg-blue-700 
                            hover:bg-blue-800 
                            focus:ring-4 focus:outline-none focus:ring-blue-300 
                            font-medium rounded-lg 
                            text-sm 
                            px-4 py-1.5
                            "
                    >検索</button>
                </div>
            </div>

            {/* 検索結果ブロック */}          
            {((infos.length > 0 && action == true) || (action == false)) ? 
                infos.map((info) => (
                <div class="col-start-2 col-span-10 p-3 w-full">
                    <hr />
                    <section class="text-gray-600 body-font">             
                        <div class="flex-grow sm:text-left text-center items-center justify-center mt-6 sm:mt-0">
                            <Link to={`/information/${info.id}`}>                         
                                <h1 class="text-black text-xl font-bold mb-2">{info.title}</h1>
                            </Link>
                            <p>{(info.date).substring(0,10)}</p>
                            <p>{info.exerpt}</p>
                            <Link to={`/information/${info.id}`}>   
                                <a class="mt-0 text-indigo-500 inline-flex items-center">全文を読む
                                    <svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="w-4 h-4 ml-2" viewBox="0 0 24 24">
                                        <path d="M5 12h14M12 5l7 7-7 7"></path>
                                    </svg>
                                </a>
                            </Link>
                        </div>
                    </section>
                </div>
            )) : (<p class="col-span-12 m-5 w-full">検索結果はありませんでした</p>)
           }
     
        </div>
      </Layout>
    );
}
```

</details>

Tailwindでスタイル指定しているのでかなり長いコードになってます。見ずらいと思いますので、分かりやすくするため、スタイル指定やその他を省略したコードを掲載します。

<details><summary><strong>pages/searchform.jsコード省略形</strong></summary>

```js
// pages/searchform.js
import * as React from "react"
import axios from "axios";
import { Link, graphql } from "gatsby"
import Layout from "../components/layout"

export default function SearchForm() {
    const [keyword, setKeyword] = React.useState("");
    const [infos, setInfos] = React.useState([]);
    const [action, setAction] = React.useState(false)
  
    const searchInfos = async () => {
      // 検索APIにリクエストを送信
      const res = await axios.get("/api/search", {
        params: {
          keyword,
        },
      });
      // 検索結果をステート変数にセット
      setInfos(res.data.contents);

      // 検索を実行した場合actionフラッグをtrueに
      setAction(true)
    };
  
    return (
      <Layout>
        
          {/* 検索フォームブロック */}
            <div>
                <div>
                    <input type="text"
                           value={keyword}
                           onChange={(e) => setKeyword(e.target.value)}       
                    />
                    <button onClick={searchInfos} 
                            disabled={!keyword}
                    >検索</button>
                </div>
            </div>

            {/* 検索結果ブロック */}          
            {((infos.length > 0 && action == true) || (action == false)) ? 
                infos.map((info) => (
                <div>
                    <hr />
                    <section>             
                        <div>
                            <Link to={`/information/${info.id}`}>                         
                                <h1>{info.title}</h1>
                            </Link>
                            <p>{(info.date).substring(0,10)}</p>
                            <p>{info.exerpt}</p>
                            <Link to={`/information/${info.id}`}>   
                                全文を読む
                            </Link>
                        </div>
                    </section>
                </div>
            )) : (<p>検索結果はありませんでした</p>)
           }
     
        </div>
      </Layout>
    );
}

```

</details>

<br />


#### 実装手順2-1. 画面紹介

検索画面
http://localhost:8000/searchform
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/67f55d9a-9cc8-cb98-c7f8-b3ea89d14f17.png)


検索結果画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/adf5d0ac-4031-270a-1098-cef59a4ceb08.png)

検索結果表示画面（何もヒットしなかった場合）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/97152f1c-2c9e-659c-084d-b516a0b277b2.png)


#### 実装手順2-2. pages/searchform.jsコード解説

##### 実装手順2-2-1. 前半部分（フォーム）

- この**searchform.js**は、主に、検索ワードを"**api/search**" APIに投げて検索結果をフェッチしてくるブロックと、その戻り値（検索結果）をページ上に表示するブロックの、二つに分かれています。

- **searchInfos関数**の中にある、
```js
      const res = await axios.get("/api/search", {
        params: {
          keyword,
        },
      });
```
のパラグラフが"**api/search**" から検索結果を`axios.get`でフェッチしている箇所です。

- パラメーターに指定しているのは、ステートフック`setKeyword(e.target.value)`で格納したステート変数**keyword**になります。`setKeyword(e.target.value)`は`<input>`タグの中の`onChange=`で呼ばれています。つまり、`e.target.value`にはテキストボックスに入力した検索ワードが格納されており、それをステート変数**keyword**に代入しています。

```js
<input type="text"
     value={keyword}
     onChange={(e) => setKeyword(e.target.value)}       
/>
```

<br />

- "**api/search**" から検索結果を`axios.get`でフェッチするパラグラフは、関数`searchInfos()`の中で実行されています。

- `searchInfos()`関数を呼び出しているのは`<button>`タグの`onClick=`です。

```js
<button onClick={searchInfos} 
        disabled={!keyword}
>検索</button>
```

- つまり検索ボタンをクリックすると**searchInfos()関数**が呼び出され、"**api/search**"にリクエスト（req）を投げ、検索結果レスポンス（res）を受け取ることになります。


- 検索結果は定数**res**に格納されます。
```js
const res = await axios.get("/api/search", {
```

<br />


- microCMSからの戻り値（検索結果）は**res.data.contents**に格納されています。

:::note info
 **res.data.contents**の中身を覗いてみたい方は、`console.log(res.data.contents)`の一行コードを`searchInfos()`関数の中に入れ込み、ブラウザの開発ツール（F12）のコンソールで確認してみてください。microCMSからの返り値がJSONデータとして表示されるはずです。

```js
const searchInfos = async () => {
  // 検索APIにリクエストを送信
  const res = await axios.get("/api/search", {
    params: {
      keyword,
    },
   });
   // 検索結果をステート変数にセット
   setInfos(res.data.contents);
   console.log('◆res.data.contents') //←この辺りに入れ込んで下さい
   console.log(res.data.contents)     //←この辺りに入れ込んで下さい

   // 検索を実行した場合actionフラッグをtrueに
   setAction(true)
};
```

:::


- 最後に、ステートフック`setInfos(res.data.contents)`でステート変数**infos**に検索結果のデータが格納されます。


これで"**api/search**" APIとのやり取りは完了です。

<br />

- `searchInfos()`関数の最後に、`setAction(true)`でステート変数**action**をtrueにしています。ステート変数**action**は、「検索を実行したが**infos**（検索結果）にデータがなかった場合、『検索結果はありませんでした』メッセージを表示するためのフラグとして使用しています。


- ステート変数**keyword**に値がない場合、検索ボタンは`disabled={!keyword}`で無効化されています。

<br />

##### 実装手順2-2-2. 後半部分（検索結果表示）

- ステート変数**infos**に格納された検索結果データを、map()関数で順繰りに表示させているだけになります。ただ、その前に三項演算子の条件式があります。
```js
((infos.length > 0 && action == true) || (action == false)) ? 
　infos.map((info) => ( 
　…省略…
　) :  (<p>検索結果はありませんでした</p>)
```

- この三項演算子は、検索結果がなかった場合に「検索結果はありませんでした」のメッセージを表示させるためものです。

- ただ単に、『**infos**に何もデータがなかった場合「検索結果はありませんでした」メッセージを表示させる』、というような条件式ですと、この検索ページに訪問しただけでもそのように表示されてしまいます。したがって、**infos**に何もデータがない、しかし検索は実行した（ステート変数**action**がtrueになった）場合にのみ、「検索結果はありませんでした」メッセージを表示させています。

- map()関数で表示させていく要素は、タイトルであれば`{info.title}`、日付であれば`{info.date}`、という具合に**infos**から一つずつ抜いていきます。
各記事ページのスラグはmicroCMSから割り当てられたidを使用しているので、リンク先（`<Link to="">`）として`{info.id}`を使用しています。
リンクは各記事のタイトルと、「全文を読む →」に貼ってあります。

<br />

これで検索フォームも完成です！

<br />
しかし・・・

:::note warn
※．検索フォームをヘッダーナビに持っていき、検索結果をメインページに表示させたいですよね？
このように↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/09547969-82f4-f847-cfc1-84f7e61b81cc.png)

この実装はややこしいです。useContextフックを使いコンポーネント間でステート変数の共有を行うのですがそれだけでは…:sweat:。React、Nextではない、Gatsbyゆえの困難性があります。
※．microCMSとGatsbyのインプリ（設定）から詳しく知りたい。
※．もう少し丁寧に順を追った、画面遷移も詳細に明示した解説が欲しい。


↓ぜひお手に取ってみて下さい！↓
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
拙書『Gatsby５後編ー最新GatsbyとmicroCMSでつくるコーポレートサイト《サイト内検索機能付き》 －ペーパーバック版（2,790円）
<a href="https://www.amazon.co.jp/gp/product/B0BXN99WSV?&linkCode=li3&tag=kokkahasan-22&linkId=68f1905bf915f6bd365559a0c65bcdd1&language=ja_JP&ref_=as_li_ss_il" target="_blank"><img border="0" src="//ws-fe.amazon-adsystem.com/widgets/q?_encoding=UTF8&ASIN=B0BXN99WSV&Format=_SL250_&ID=AsinImage&MarketPlace=JP&ServiceVersion=20070822&WS=1&tag=kokkahasan-22&language=ja_JP" ></a><img src="https://ir-jp.amazon-adsystem.com/e/ir?t=kokkahasan-22&language=ja_JP&l=li3&o=9&a=B0BXN99WSV" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />

:::


# 本の宣伝
### Gatsbyバージョン５>>>>改訂２版

**前編**の『Gatsby５前編ー最新Gatsbyでつくるコーポレートサイト』と**後編**の『Gatsby５後編ー最新GatsbyとmicroCMSでつくるコーポレートサイト《サイト内検索機能付き》』を合わせ、次のようなデモサイトを構築します。
→ **https://yah-space.work**
<hr />


静的サイトジェネレーターGatsby**最新バージョン５**の基本とFile System Route APIを使用して動的にページを生成する方法を解説。またバージョン５の新機能《**Slicy API**》《**Script API**》《**Head API**》を紹介、実装方法も。《**Gatsby Functions**》での問い合わせフォーム実装やGatsby Cloudへのアップロード方法も！


<a href="https://www.amazon.co.jp/gp/product/B0BXNJTJMY?&linkCode=li3&tag=kokkahasan-22&linkId=93c5e3bf1f226141ddc807300e81b07a&language=ja_JP&ref_=as_li_ss_il" target="_blank"><img border="0" src="//ws-fe.amazon-adsystem.com/widgets/q?_encoding=UTF8&ASIN=B0BXNJTJMY&Format=_SL250_&ID=AsinImage&MarketPlace=JP&ServiceVersion=20070822&WS=1&tag=kokkahasan-22&language=ja_JP" ></a><img src="https://ir-jp.amazon-adsystem.com/e/ir?t=kokkahasan-22&language=ja_JP&l=li3&o=9&a=B0BXNJTJMY" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />
<a target="_blank" href="https://www.amazon.co.jp/gp/product/B0BXXBDJDJ?&linkCode=ll1&tag=kokkahasan-22&linkId=df7e600968c58c0090cd735681c6732c&language=ja_JP&ref_=as_li_ss_tl">Gatsby５前編ー最新Gatsbyでつくるコーポレートサイト ～基礎の基礎から応用、新機能の導入まで（書籍2,980円）</a>

<br />
<br />

**最新Gatsby５**とmicroCMSを組み合わせてのコーポレートサイト作成手順を解説。《**サイト内検索機能**》をGatsbyバージョン４からの新機能《**Gatsby Functions**》と《**microCMSのqパラメータ**》で実装。また、**SEOコンポーネントをカスタマイズ**して**microCMS APIをツイッターカードに表示**させる**OGPタグ**実装方法も解説。

<a href="https://www.amazon.co.jp/gp/product/B0BXN99WSV?&linkCode=li3&tag=kokkahasan-22&linkId=68f1905bf915f6bd365559a0c65bcdd1&language=ja_JP&ref_=as_li_ss_il" target="_blank"><img border="0" src="//ws-fe.amazon-adsystem.com/widgets/q?_encoding=UTF8&ASIN=B0BXN99WSV&Format=_SL250_&ID=AsinImage&MarketPlace=JP&ServiceVersion=20070822&WS=1&tag=kokkahasan-22&language=ja_JP" ></a><img src="https://ir-jp.amazon-adsystem.com/e/ir?t=kokkahasan-22&language=ja_JP&l=li3&o=9&a=B0BXN99WSV" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />
<a target="_blank" href="https://www.amazon.co.jp/dp/B0BXN99WSV?_encoding=UTF8&qid=&sr=&linkCode=li2&tag=kokkahasan-22&linkId=f02e2f9c50736e3f074b38b4cd73894a&language=ja_JP&ref_=as_li_ss_il">Gatsby５後編ー最新GatsbyとmicroCMSでつくるコーポレートサイト《サイト内検索機能付き》（書籍 2,790円）</a>

<br />


<br /><br />