Gatsby×Contentfulで爆速な検索フォームを実装 | Gatsbyブログカスタマイズ

Webサイト高速化のための 静的サイトジェネレーター活用入門を通じて作成したこのブログのカスタマイズとして、サイト内検索機能を加えました。
ほとんど参考サイトのおかげで実装が可能でした。
フロントのみで検索機能が完結しているので、ほとんどリアルタイムに検索結果が表示される、爆速な点がとても気に入っています。

サイト内検索機能の仕様

仕様は参考サイトに則る形です。

  • バックエンドにContentfulを使用

  • 検索フォームはモーダルで表示

  • 記事を全件表示

  • 全件表示から検索ワードでフィルタリングする

プラグイン(react-modal)をインストール

モーダルウインドウを簡単に実装できるReactのライブラリ、react-modalをインストールします。

yarn add react-modal

npmの場合は以下の通り。

npm install react-modal

modalsearch.jsの作成

src/components/modalsearch.jsを新規作成します。

モーダルウインドウの実装と、検索コンポーネントの呼び出しを記述します。
検索の虫眼鏡アイコンや、モーダルウインドウを閉じるための×アイコンを追記しています。
検索コンポーネントは後程作成するので、この時点ではビルドしてもエラーとなります。

import React from "react";
import Modal from "react-modal";
import Search from "./search";

import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faSearch, faTimes } from "@fortawesome/free-solid-svg-icons"

Modal.setAppElement("#___gatsby")  //public/htmlのid参照
class ModalWindow extends React.Component {
  constructor() {
    super();
    this.state = {
      modalIsOpen: false
    };
    this.openModal = this.openModal.bind(this);
    this.closeModal = this.closeModal.bind(this);
  }
  openModal() {
    this.setState({modalIsOpen: true});
  }
  closeModal() {
    this.setState({modalIsOpen: false});
  }
  render() {
    return (
      <div className="modalWrapper">
        <button className="searchFor" onClick={this.openModal}><FontAwesomeIcon icon={faSearch} />SEARCH</button>
        <Modal
          isOpen={this.state.modalIsOpen}
          onRequestClose={this.closeModal}
          contentLabel="Search Modal"
          className="modalSearchWindow"
          overlayClassName="modalSearchOverlay"
        >
          <Search />
          <button className="searchModalClose" onClick={this.closeModal}><FontAwesomeIcon icon={faTimes} />CLOSE</button>
        </Modal>
      </div>
    );
  }
}
export default ModalWindow;

search.jsの作成

src/components/search.jsを新規作成します。

検索コンポーネントを実装します。
年月表示を日本語にしたり、内部リンクの記述をLinkにしたりと、変更を加えています。

import React, { useState } from "react"
import { useStaticQuery, graphql, Link } from "gatsby"

const SearchResult = props => {
  const tempData = useStaticQuery(graphql`
    query SearchData {
      allContentfulBlogPostForSeach: allContentfulBlogPost( sort: {fields: publishDate, order: DESC}) {
        edges {
          node {
            title
            slug
            publishDateJP:publishDate(formatString: "YYYY年MM月DD日")
            publishDate
          }
        }
      }
    }
  `)

  const className = useState("")
  const allPosts = tempData.allContentfulBlogPostForSeach.edges
  const emptyQuery = ""
  const [state, setState] = useState({
    filteredData: [],
    query: emptyQuery,
  })
  const handleInputChange = event => {
    const query = event.target.value
    const posts = tempData.allContentfulBlogPostForSeach.edges || []

    const filteredData = posts.filter(post => {
      const title = post.node.title
      return (
        title.toLowerCase().includes(query.toLowerCase())
      )
    })
    setState({
      query,
      filteredData,
    })
  }
  const { filteredData, query } = state
  const hasSearchResults = filteredData && query !== emptyQuery
  const result = hasSearchResults ? filteredData : allPosts

  return (
    <div className={className}>
      <div className="result-inner">
        <div className="result-content">
          <input
            type="text"
            aria-label="Search"
            placeholder="検索ワードを入力..."
            onChange={handleInputChange}
          />
          <p className="result-inner__res">
            {query !== "" ?
              query + " の検索結果: " + result.length + "件"
              : result.length + "件の記事があります"
            }
          </p>
          <ul className="result-inner__search">
            {result && result.map(({ node: post }) => {
              return (
                <li key={post.slug}>
                  <Link to={`/blog/post/${post.slug}/`}>
                    <span className="result-inner__date">{post.publishDateJP}</span><span className="result-inner__title">{post.title}</span>
                  </Link>
                </li>
              )
            })}
          </ul>
        </div>
      </div>
    </div>
  )
}

export default SearchResult

ModalSeachの呼び出し

任意の箇所で検索機能を呼び出します。
このブログの場合は、src/components/header.jsで呼び出しています。

...
import ModalSeach from "./modalsearch"
...
<ModalSeach />

スタイル調整

CSSでスタイルを調整します。
SCSSではありますが、参考までに以下に示します。

.searchFor,
.searchModalClose {
    background-color: transparent;
    border: none;
    cursor: pointer;
    padding: 0;
    appearance: none;
    font-size: inherit;
    font-family: inherit;

    .svg-inline--fa {
        padding-right: 0.25em;
    }
}

.modalSearchOverlay {
    background-color: rgba(0, 0, 0, 0.75);
    position: fixed;
    display: flex;
    align-items: center;
    justify-content: center;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    z-index: 11;
}

.modalSearchWindow {
    z-index: 12;
    background: #fff;
    outline: none;
    position: relative;
}

.result-inner {
    width: 80vw;
    max-width: 800px;

    &__search {
        overflow: auto;
        max-height: 350px;
        min-height: 350px;

        li {
            padding: 1em 0;
            border-bottom: 1px solid #808080;

            &:first-child {
                border-top: 1px solid #808080;
            }
        }

        a {
            display: inline-flex;
            flex-direction: column;

            @media (min-width: 768px) {
                flex-direction: row;
            }

            .result-inner__date {
                white-space: nowrap;
            }
        }
    }

    &__res {
        padding: 1em 0;
    }
}

.result-content {
    padding: 1.5em;
    display: flex;
    flex-direction: column;
    box-sizing: border-box;

    @media (min-width: 768px) {
        padding: 3em;
    }

    input {
        padding: 1em;
        font-size: inherit;
    }
}

.searchModalClose {
    position: absolute;
    background-color: #fff;
    padding: 0.5em 0.75em;
    top: -2em;
    right: 0;

    @media (min-width: 768px) {
        top: 0;
        background: none;
        box-sizing: border-box;
    }
}

今後の展望

現時点では検索対象が記事タイトルのみとなっていますので、ゆくゆくは改修して本文も検索対象にしたいと思います。

参考サイト

▼Gatsby.js + Contentfulに爆速な検索フォームを実装する! | DevelopersIO
https://dev.classmethod.jp/articles/gatsby-js-contentful-search/