🗂

Next.jsのSSGで全文検索をする方法

2022/06/20に公開

前提

  • Next.js
  • TypeScript
  • styled-components
  • microCMS
  • AWS
    • S3
    • Cloud Front
    • Route53

Next.jsのSSGで生成した静的ファイルをS3にアップロードして、静的なメディアサイトを作っています。
CMSはmicroCMSを利用していて、メディアの記事の部分をmicroCMSから配信しています。

やりたいこと

Next.jsのSSGで作成した静的なメディアにおいて、全文検索機能を追加したいというのが今回のやりたかったことです。
next devでローカル開発してるときは、getStaticProps内が変更はいるたびに更新され、再buildされるので、うまくいったように見えてましたが、S3にあげてみると、うまくいかなかったので、その対策をしたかったです。

結論

  • microCMSの全文検索APIをつかう
  • AWSのLambdaでエンドポイントを作成し、Next.jsからは、そのLambdaをコールし、Lambda内でmicroCMSの全文検索APIをコールすることで実現させる

microCMSの全文検索APIと呼んでいるのは、こちらのAPIです。
https://document.microcms.io/content-api/get-list-contents

方法

調査したところ、今回の前提条件下でNext.jsのSSGで全文検索を実現するのには、2つの方法があることがわかりました。

  1. Algoliaを使用する
  2. AWSのLambdaでエンドポイントを作成し、Next.jsからは、そのLambdaをコールし、Lambda内でmicroCMSの全文検索APIをコールすることで実現させる

1のAlgoliaを使用するに関しては、こちらの記事が非常にわかりやすいです。
https://fwywd.com/tech/next-algolia

Next.js開発元であるvercelのGitHubを覗いてみたところ、こちらのdiscussionにもあるようにAlgoliaを使ってみては?という提案がありました。
https://github.com/vercel/next.js/discussions/14742

今回は、2. AWSのLambdaでエンドポイントを作成し、Next.jsからは、そのLambdaをコールし、Lambda内でmicroCMSの全文検索APIをコールすることで実現させるという方法を取りました。
理由としては、すでにローカルでmicroCMSの全文検索APIを呼ぶような調査兼実装を進めていて、そこまで大きな方針転換する必要なく、実現できそうだったからです。

実装サンプル

検索用のSearchコンポーネント

まずは、検索用のSearchコンポーネントをつくります。
pages/index.tsxで定義したsubmitを渡してあげます。

components/search.tsx

type Props = {
  submit: () => void;
}

export const Search: React.VFC<Props> = ({submit}) => {
  const [value, setValue] = React.useState('');

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value)
  }

  const onClick = () => submit(value);

  return (
    <form>
      <input type="text" name="search" value={value} onChange={onChange} placeholder={'サイト内検索'} />
      <button onClick={onClick} >
        <img src={'/img/media/search.png'} height={16} width={16} alt={'search'} />
      </button>
    </form>
    )
}

メインのpages/index.tsx

こちらの画面で検索した結果の記事の一覧を表示します。
実装イメージとしては、SSGのときには、すべての記事の一覧を獲得してきます。
Lambda側で、microCMSの全文検索のAPIを使って獲得した記事一覧のIDを照合して、検索結果の記事一覧を表示してあげます。

ex)
SSGで獲得した記事一覧: [記事1, 記事2, 記事3, 記事4, 記事5]
microCMSの全文検索のAPIを使って獲得した記事一覧のIDの配列: [記事3のid, 記事5のid]

↓ 照合後

検索後記事一覧で表示したいもの:[記事3, 記事5]

検索自体は、 クエリパラメータをつかって、実装してあげます。
たとえば、hogehogeと検索したい場合は、
[何かURL].com?search=hogehoge となるようにします。

Next.jsでSSGを使い、microCMSのエンドポイントを叩く方法は、microCMSの公式のサンプルが参考になります。
https://github.com/microcmsio/microcms-sample/tree/master/microcms-next-jamstack-blog

pages/index.tsx
import { Search } from '/components/Search';

export const Main: React.VFC<Props> = ({ articles }) => {
  const router = useRouter();
  const [data, setData] = React.useState<IMedia[]>(articles);

  const load = React.useCallback(async () => {
    const value = router.query.search as string;
    // 検索してない場合は、lambdaのURLをコールしないようにする
    // コールすればするほどお金がかかるから
    if(!value){
      setData(articles);
      return;
    }
    
    // lambdaで作成したAPIをコールして、microCMSの全文検索APIをコール
    const response = await fetch("https://[lambda側で自動生成された値].lambda-url.us-east-1.on.aws/", {
      method: "POST",
      body: JSON.stringify({ query: value }),
    });
    
    const result = await response.json() as SearchResponse;
    
    // 検索して検索結果が0件なら、記事すべてを表示する
    if(result.totalCount === 0){
      setData(articles);
      return;
    }
    
    const ids = result.contents.map((r) => r.id);
    const newArticles = articles.filter((m) => ids.includes(m.id));
    setData(newArticles);
  }, [articles, router.query.search])

  const submit = React.useCallback(async (value: string) => {
    await router.push({
      pathname: '/',
      query: { search: encodeURI(value) },
    })
  }, [router]);

 // useEffectでloadを呼んだタイミングで検索の必要があれば、検索するようにする
  React.useEffect(() => {
    load();
  } , [load])

  return (
    <>
     <ul>
       {articles.map((article, i) => <li key={i}>{article.title}</li>)}
     </ul>
     <Search submit={submit}>
    </>
  )
};

//SSG処理を記載。build時にmicroCMSのエンドポイントをコールして、記事一覧を獲得する
export const getStaticProps: GetStaticProps<Props> = async () => {
  const key = {
    headers: {'X-API-KEY': process.env.API_KEY},
  };

  const res = await fetch('https://[サービス名].microcms.io/api/v1/[エンドポイント名]', key);

  const data = await res.json();
 
  const data = await client.get({
    endpoint: [エンドポイント名],
  });
  return {
    props: {
      articles: data.contents,
    },
  };
};

export default Main;

Lambda

今(執筆当時2022年6月)は、AWS Lambda Function URLsの機能が追加されてるので、API Gatewayを経由せずに、HTTPSのエンドポイント作成ができます。
詳しくは、こちらを参考にしてください。

https://dev.classmethod.jp/articles/aws-lambda-function-urls-built-in-https-endpoints/

lambda
// 軽量に使うなら、すこし冗長な書き方をする必要があるが、`https`を使う。
// axiosなどをもちろんimportするのも、OK
const https = require('https');

function getRequest(query) {
  const options = {
   // API Keyは、忘れずに環境変数に入れる
    headers: { 'X-MICROCMS-API-KEY': process.env['MICRO_CMS_API_KEY'] }
  }
  const url = `https://[サービス名].microcms.io/api/v1/[エンドポイント名]?q=${query}`
  return new Promise((resolve, reject) => {
    const req = https.get(url, options, res => {
      let rawData = '';

      res.on('data', chunk => {
        rawData += chunk;
      });

      res.on('end', () => {
        try {
          resolve(JSON.parse(rawData));
        } catch (err) {
          reject(new Error(err));
        }
      });
    });

    req.on('error', err => {
      reject(new Error(err));
    });
  });
}

exports.handler = async (event) => {
  try {
    const body = JSON.parse(event.body);
    // 上で作ったgetRequestをコールして、microCMSの全文検索APIから記事を獲得してくる
    const result = await getRequest(body.query);

    // statusコードなどと一緒に獲得した結果をJSONにして返してあげる
    return {
      statusCode: 200,
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify(result),
    };
  } catch (error) {
    return {
      statusCode: 400,
      body: error.message,
    };
  }
};

最後に

この方法はVercel代をケチり、SSGをして、S3でホスティングしようという意思決定をしたがゆえの工夫です。
もし、はじめに戻れるなら、Vercelにちゃんとお金を払って、Vercelでホスティングしたほうが実装コストは抑えられるので、おすすめです。

もし同じようなことで困ってることの参考に少しでもなれば嬉しいです。
最後まで読んでいただきありがとうございます。

GitHubで編集を提案

Discussion