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を作成します。
以下のようにコーディング。
//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の完成です。
序文2-2. microCMSのqパラメーターとは。
microCMSのコンテンツに対し、qパラメーターを使うことで全文検索が実現できるようになりました。検索対象となるフィールドは、その種類が「テキストフィールド」「テキストエリア」「リッチエディタ」のものです。
microCMSの管理画面の「APIプレビュー画面」でその挙動を確認できます。
下画面は、keyとしてqパラメータを選択し、Value値(検索ワード)を入力して「取得」ボタンをクリックし、返ってきたレスポンスです。
実装手順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
実装手順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}
を使用しています。
リンクは各記事のタイトルと、「全文を読む →」に貼ってあります。
これで検索フォームも完成です!
しかし・・・
※.検索フォームをヘッダーナビに持っていき、検索結果をメインページに表示させたいですよね?
このように↓
この実装はややこしいです。useContextフックを使いコンポーネント間でステート変数の共有を行うのですがそれだけでは…。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円)