LoginSignup
1
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-08-03

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

序文1. JAMstackアーキテクチャーでサイト内検索機能

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

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

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

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

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

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

【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はもう必要なくなりました。

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

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

文字列"hello world!"を返すAPI、hello.jsを作成します。
image.png

以下のようにコーディング。

//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

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

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

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

下画面は、keyとしてqパラメータを選択し、Value値(検索ワード)を入力して「取得」ボタンをクリックし、返ってきたレスポンスです。
image.png

実装手順1.検索APIを作成

では、これらを使ってまずは検索APIを作成してみましょう。

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

$ npm install microcms-js-sdk 

apiフォルダにsearch.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でつくるコーポレートサイト 後編 -ペーパーバック版』をお手に取ってみて下さい。


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

実装手順2.検索フォームを作成

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

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

$ npm install axios

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

pages/searchform.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 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>
    );
}

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

pages/searchform.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>
    );
}


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

検索画面
http://localhost:8000/searchform
image.png

検索結果画面
image.png

検索結果表示画面(何もヒットしなかった場合)
image.png

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

実装手順2-2-1. 前半部分(フォーム)
  • このsearchform.jsは、主に、検索ワードを"api/search" APIに投げて検索結果をフェッチしてくるブロックと、その戻り値(検索結果)をページ上に表示するブロックの、二つに分かれています。

  • searchInfos関数の中にある、

      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に代入しています。
<input type="text"
     value={keyword}
     onChange={(e) => setKeyword(e.target.value)}       
/>

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

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

<button onClick={searchInfos} 
        disabled={!keyword}
>検索</button>
  • つまり検索ボタンをクリックするとsearchInfos()関数が呼び出され、"api/search"にリクエスト(req)を投げ、検索結果レスポンス(res)を受け取ることになります。

  • 検索結果は定数resに格納されます。

const res = await axios.get("/api/search", {

  • microCMSからの戻り値(検索結果)はres.data.contentsに格納されています。

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

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とのやり取りは完了です。


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

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


実装手順2-2-2. 後半部分(検索結果表示)
  • ステート変数infosに格納された検索結果データを、map()関数で順繰りに表示させているだけになります。ただ、その前に三項演算子の条件式があります。
((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}を使用しています。
    リンクは各記事のタイトルと、「全文を読む →」に貼ってあります。


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


しかし・・・

※.検索フォームをヘッダーナビに持っていき、検索結果をメインページに表示させたいですよね?
このように↓
image.png

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

↓ぜひお手に取ってみて下さい!↓
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
拙書『Gatsby5後編ー最新GatsbyとmicroCMSでつくるコーポレートサイト《サイト内検索機能付き》 -ペーパーバック版(2,790円)

本の宣伝

Gatsbyバージョン5>>>>改訂2版

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


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


Gatsby5前編ー最新Gatsbyでつくるコーポレートサイト ~基礎の基礎から応用、新機能の導入まで(書籍2,980円)



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


Gatsby5後編ー最新GatsbyとmicroCMSでつくるコーポレートサイト《サイト内検索機能付き》(書籍 2,790円)




1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1