Counts the Clouds

Gatsbyでalgolia検索を導入する
2021.10.25

Gatsby
algolia

ben-griffiths-ybFAhjUV4Cs-unsplash.jpg

プラグインの導入と設定

公式プラグインがあるので、それを使う。

% npm install gatsby-plugin-algolia

環境変数を設定する必要があるので、まずalgoliaに登録。

登録を済ませると“Overview”に秘密情報などがあるので、それをコピーする。ALGOLIA_API_KEYは“Admin API Key”。ローカルでbuildする場合でもproductionのほうに設定。

// .env.production
ALGOLIA_APP_ID=XXX
ALGOLIA_API_KEY=XXX
ALGOLIA_INDEX_NAME=XXX

gatsby-config.jsでプラグインの設定。コンテンツはremarkなのでクエリもremarkに合わせた。

require('dotenv').config({
  path: `.env.${process.env.NODE_ENV}`,
});

// gatsby-config.js
const myQuery = `{
  posts: allMarkdownRemark {
    edges {
      node {
        fields {
          slug
        }
        frontmatter {
          tags
          title
        }
        id
      }
    }
  }
}`;

const queries = [
  {
    query: myQuery,
    transformer: ({ data }) => {
      return data.posts.edges.map(edge => edge.node)
    },
  },
];

module.exports = {
  plugins: [
    // ...
    {
      resolve: `gatsby-plugin-algolia`,
      options: {
        appId: process.env.ALGOLIA_APP_ID,
        apiKey: process.env.ALGOLIA_API_KEY,
        indexName: process.env.ALGOLIA_INDEX_NAME,
        queries,
        chunkSize: 10000, // default: 1000
      },
    },
  ],
};

ビルドタイムでコンテンツをalgoliaに送信する。

% gatsby build
...
success index to Algolia - 5.743s - Done!

できた。

UIを追加

まずは公式のReact InstantSearchを使ってみる。

% npm install react-instantsearch-dom algoliasearch
import { InstantSearch, SearchBox, Hits } from 'react-instantsearch-dom';

const searchClient = algoliasearch(
  '<Application ID>',
  '<Search-Only API Key>'
);

return (
  <InstantSearch
    indexName="<index name>"
    searchClient={searchClient}
  >
    <SearchBox />
    <Hits />
  </InstantSearch>
)

公式のスタイルを入れてみるが、tailwindcss-typographyと外見的に競合してしまうので手入れが必要。

% npm install instantsearch.css
// Include only the reset
import 'instantsearch.css/themes/reset.css';
// or include the full Satellite theme
import 'instantsearch.css/themes/satellite.css';

React InstantSearchのカスタマイズ

React InstantSearchのドキュメントの、Basicsの各項目の最後に“Customize the UI”という項目があるので参考にする。

SearchBoxのカスタマイズ

createConnectorを使う。HOCは久しぶりに見た。

getProvidedPropspropsも渡しておいて、任意のpropsを使えるようにした。

useRefで自動的にfocusするようにした。

import * as React from 'react'
import { createConnector } from 'react-instantsearch-dom'

const connectWithQuery = createConnector({
  displayName: 'WidgetWithQuery',
  getProvidedProps(props, searchState) {
    const currentRefinement = searchState.attributeForMyQuery || ''
    return { ...props, currentRefinement };
  },
  refine(props, searchState, nextRefinement) {
    return {
      ...searchState,
      attributeForMyQuery: nextRefinement,
    }
  },
})

const SearchBox = ({ show, currentRefinement, refine }) => {
  const inputRef = React.useRef(null)
  React.useEffect(() => {
    if (inputRef.current && show) {
      inputRef.current.focus()
    }
  }, [show, inputRef])
  return (
    <input
      ref={inputRef}
      id="search_keyword"
      label="Search"
      name="search_keyword"
      className="..."
      placeholder="Search"
      value={currentRefinement}
      onChange={e => refine(e.currentTarget.value)}
    />
  )
}

export default connectWithQuery(SearchBox)

Hitsのカスタマイズ

connectHitsを使う。こちらは特段のことはない。

import * as React from 'react'
import { connectHits } from 'react-instantsearch-dom'
import PostDigest from '../post-digest'

const Hits = ({ hits }) => (
  <div>
    {hits.map(hit => (
      <PostDigest
        key={hit.id}
        id={hit.id}
        slug={hit.fields.slug}
        title={hit.frontmatter.title}
        date={hit.frontmatter.date}
        tags={hit.frontmatter.tags}
        timeToRead={hit.timeToRead}
      />
    ))}
  </div>
)

export default connectHits(Hits)

検索ダイアログとして集約

カスタマイズしたパーツを集約。フルスクリーンモーダルの形にした。

searchClientsearchをカスタマイズして、クエリが空のときリクエストしないようにした。

import * as React from 'react'
import algoliasearch from 'algoliasearch/lite'
import { XIcon } from '@heroicons/react/outline'
import { InstantSearch } from 'react-instantsearch-dom'
import SearchBox from './search-dialog/search-box'
import Hits from './search-dialog/hits'

const algoliaClient = algoliasearch(
  process.env.GATSBY_ALGOLIA_APP_ID,
  process.env.GATSBY_ALGOLIA_SEARCH_API_KEY,
)

const searchClient = {
  ...algoliaClient,
  search(requests) {
    if (requests.every(({ params }) => !params.query)) {
      return Promise.resolve({
        results: requests.map(() => ({
          hits: [],
          nbHits: 0,
          nbPages: 0,
          page: 0,
          processingTimeMS: 0,
        })),
      });
    }
    return algoliaClient.search(requests)
  },
}

const SearchDialog = ({ show, onClose = () => {} }) => {
  return (
    <div className="...">
      <InstantSearch
        indexName={process.env.GATSBY_ALGOLIA_INDEX_NAME}
        searchClient={searchClient}
      >
        <div className="flex items-center pl-4">
          <div className="h-6 w-6"></div>
          <SearchBox show={show} />
          <button className="p-4" onClick={onClose}>
            <XIcon className="h-6 w-6 text-gray-800 dark:text-gray-50" />
          </button>
        </div>
        <div className="px-4 pb-4 overflow-y-auto">
          <div className="md:w-1/2 mx-auto">
            <Hits />
          </div>
        </div>
      </InstantSearch>
    </div>
  )
}

export default SearchDialog

これで、ブログにalgolia検索を導入できた。

そのほか、参考になったサイト。