はじめに
本ブログは GatsbyJS を使用して静的サイトとして運用しています。
ただ、それでもサイト内検索などの動的な機能が必要であると考え、実装しました。 非常に軽快で満足できる実装になりました。ぜひこちらの検索ページをご覧いただき、お試しください。
実際のコードが見たい場合は、こちらをご覧ください。
全体仕様
- このページは、検索機能を提供します。API コールを行わず、ブラウザの JavaScript で動作します。
- モバイル環境でも軽快に動作しますが、検索対象の増加によって性能が低下する可能性があります。
- 検索文字列の入力 1 文字ごとにインタラクティブに検索結果を表示します。
- URL のクエリパラメータに検索文字列が含まれ、入力フォームと同期しているため、リロードしても同じ表示が行われ、検索結果の URL で共有もできます。
- 検索対象は全ての記事であり、タイトルか記事内容に文字列が含まれるかどうかで判定します。複数ワード検索はスペースで区切って行えます。大文字と小文字は区別しません。
- 検索結果には、ヒット件数と記事のタイトルと説明文が表示されます。
詳細説明
必要最小限のコードを抜粋しています。これに基づいて後述します。
1import React, { useEffect, useState } from "react"
2import { graphql, Link } from "gatsby"
3import { convertCategory, mergePosts } from "../utilFunction"
4
5const Search = ({ data, location }: { data: any; location: Location }) => {
6 const posts = mergePosts(data.allMarkdownRemark, data.allWpPost, data.allFile)
7 const initQuery = decodeURI(
8 location.href?.split("?q=")[1] || ""
9 ).toLowerCase()
10 const [state, setState] = useState({
11 filteredData: filterByQuery(initQuery.split(/\s+/)),
12 query: initQuery,
13 })
14 const { filteredData, query } = state
15
16 function filterByQuery(queryWords: string[]) {
17 return posts.filter(post => {
18 for (const word of queryWords) {
19 if (
20 !post.title.toLowerCase().includes(word) &&
21 !post.description?.toLowerCase().includes(word)
22 ) {
23 return false
24 }
25 }
26 return true
27 })
28 }
29 function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
30 const queryWords = event.target.value.toLowerCase().split(/\s+/)
31
32 setState(prevState => ({
33 ...prevState,
34 filteredData: filterByQuery(queryWords),
35 query: queryWords.join(" "),
36 }))
37 }
38
39 useEffect(() => {
40 // ユーザーの入力があるたびにURLのクエリパラメータを更新
41 const params = new URLSearchParams()
42 if (query) {
43 params.append("q", query)
44 } else {
45 params.delete("q")
46 }
47 window.history.replaceState(
48 "",
49 "",
50 location.href.split("?")[0] +
51 (params.size > 0 ? "?" + params.toString() : "")
52 )
53 }, [state.query])
54
55 return (
56 <>
57 <input
58 type="text"
59 aria-label="Search"
60 placeholder="検索ワードを入力..."
61 onChange={handleInputChange}
62 value={query}
63 />
64 <div className="result-inner__res">
65 {query !== ""
66 ? query + " の検索結果: " + filteredData.length + "件"
67 : filteredData.length + "件の記事があります"}
68 </div>
69 <h1>サイト内検索</h1>
70 <p>{filteredData.length} 記事あります</p>
71 {filteredData.map(post => {
72 return (
73 <li key={post.slug}>
74 <Link to={`/${convertCategory(post.category)}/${post.slug}`}>
75 <h2>
76 <span>{post.title}</span>
77 </h2>
78 <section>
79 <p dangerouslySetInnerHTML={{ __html: post.excerpt }} />
80 </section>
81 </Link>
82 </li>
83 )
84 })}
85 </>
86 )
87}
88
- 検索対象となる全記事データを取得します。これはほぼ GatsbyJS の機能を利用しています。"mergePosts"関数は単に
pageQuery
で取得した data を使いやすい形に整形しているだけです。 - "useState"フックを使用して、フィルタリングされたデータとクエリを保持する"state"と"setState"を定義します。初期値として、URL のクエリパラメータに基づいて設定します。
- "filterByQuery"関数を定義しています。この関数は、クエリワードに基づいて記事をフィルタリングします。各記事のタイトルと説明をクエリワードと比較し、一致する記事のみを返します。
- "handleInputChange"関数も定義しています。ユーザーの入力に応じてクエリを更新し、フィルタリングされたデータを更新します。
- "useEffect"フックは、コンポーネントのレンダリング後に実行されます。ユーザーが入力したクエリで URL のクエリパラメータを更新します。つまり、同期させます。
- 最後に、レイアウトと検索結果を表示する JSX が返されます。入力欄、検索結果の表示、およびフィルタリングされた記事のリストが表示されます。
おわりに
以上です。少ない記述でやりたいことができるので、ぜひ参考にしてみてください。
React の強力さを感じました。