公式ドキュメントの実装方法を参考にマークダウンファイルを検索できるようにする。
検索の仕組みは
という感じです。
こちらを参考に一覧ページを詳細ページを作成。
dataディレクトリを新しく作成し、検索キー用にsearchを追加します。
---
path: "/file1"
date: "2019-06-16"
title: "マークダウンファイル その1"
search: "検索その1"
---
## マークダウンファイルの内容
---
path: "/file2"
date: "2019-06-19"
title: "マークダウンファイル その2"
search: "検索その2箇条書き"
---
+ 箇条書き
+ 箇条書き
+ 箇条書き
---
path: "/file2"
date: "2019-06-19"
title: "マークダウンファイル その2"
search: "検索その3テーブル"
---
|A |B |C |
|:--|:-:|--:|
|1 |2 |3 |
|4 |5 |6s |
検索結果用に日付とタイトルのみ表示します。
import React from "react"
import { Link,graphql } from "gatsby"
export default ({data}) => {
return(
<div>
{data.allMarkdownRemark.edges.map(({ node }, index) => (
<div key={index}>
<span>{node.frontmatter.date}</span>
<Link to={node.fields.slug} >{node.frontmatter.title}</Link>
</div>
))}
</div>
)
}
export const query = graphql`
query MyQuery {
allMarkdownRemark(sort: {fields: frontmatter___date, order: ASC}) {
totalCount
edges {
node {
frontmatter {
title
path
date
}
excerpt
fields {
slug
}
}
}
}
}
`
サーバー起動時にJSONを出力するようにgatsby-node.jsを変更
exports.onCreateNode = ({ node,actions }) => {
if (node.internal.type === `MarkdownRemark`) {
const { createNodeField } = actions
createNodeField({
node,//このノードに
name: `slug`,//slugという名前で
value: node.frontmatter.path//pathを設定
})
}
}
//ページ生成
const path = require(`path`)
//1. ファイル出力用にfsを読み込む
var fs=require("fs");
exports.createPages = ({ graphql, actions }) => {
const blogTemplate = path.resolve(`src/templates/blog.js`)
const { createPage } = actions
// マークダウンファイルのslugを取得
return graphql(`
{
allMarkdownRemark {
edges {
node {
fields {
slug
}
# 2.検索用に追加
frontmatter {
title
search
date
}
}
}
}
}
`).then(
result => {
// 検索用JSONの内容を格納する
const search = [];
result.data.allMarkdownRemark.edges.forEach(edge => {
createPage({
path: edge.node.fields.slug,
component: blogTemplate,
context: {
slug: edge.node.fields.slug,
},
})
// 3.slug、タイトル、検索キー、日付を配列に入れていく
search.push({
slug: edge.node.fields.slug,
title: edge.node.frontmatter.title,
search: edge.node.frontmatter.search,
date: edge.node.frontmatter.date
})
})
// JSONファイルに出力
fs.writeFileSync('./static/search.json', JSON.stringify(search, null, 4))
}
)
}
サーバを起動するとstatic配下にsearch.jsonが作成されます。
[
{
"slug": "/file2",
"title": "マークダウンファイル その2",
"search": "検索その2箇条書き",
"date": "2019-10-28"
},
{
"slug": "/file1",
"title": "マークダウンファイル その1",
"search": "検索その1",
"date": "2019-10-12"
},
{
"slug": "/file3",
"title": "マークダウンファイル その3",
"search": "検索その3テーブル",
"date": "2019-11-01"
}
]
の前にindex.jsでSearchContainerを呼び出して、出力するように修正します。
import React from "react"
import Search from "../components/SearchContainer"
// 表示はすべてSearchで行う
export default () => {
return(
<div>
<Search />
</div>
)
}
検索を行うSearchContainer.jsをsrc/components/に作成します。
※公式ドキュメントの内容を一部消しています(エラー処理など)。
import React, { Component } from "react"
import Axios from "axios"
import * as JsSearch from "js-search"
import { Link } from "gatsby"
class Search extends Component {
state = {
bookList: [],//JSONの内容を格納
search: [],//JsSearchインスタンスを格納
searchResults: [],//検索結果を格納
searchQuery: "",//検索ワード
}
/**
* 初期化 search.jsonを読み込む
*/
async componentDidMount() {
Axios.get("search.json")
.then(result => {
const bookData = result
this.setState({ bookList: bookData.data })
this.rebuildIndex()
})
.catch(err => {
})
}
/**
* 検索方法の設定
*/
rebuildIndex = () => {
const { bookList } = this.state;
// JsSearchインスタンス作成(検索対象のリストでユニークとなるキーを指定する)
const dataToSearch = new JsSearch.Search("slug");
// 検索ワードをいい感じに変換する(とりあえずスペースで分割し、複数文字列で検索)
dataToSearch.tokenizer = {
tokenize( text ) {
return text.split(/\s+/i);
}
};
// 部分一致で検索する
dataToSearch.indexStrategy = new JsSearch.AllSubstringsIndexStrategy();
// 検索ワードを小文字変換、trimする(記述しなくてもデフォルトで設定されている)
dataToSearch.sanitizer = new JsSearch.LowerCaseSanitizer();
//検索方法の設定
dataToSearch.searchIndex = new JsSearch.TfIdfSearchIndex("slug");
// 検索を行うキー
dataToSearch.addIndex("title");
dataToSearch.addIndex("search");
// 検索対象となるリストを設定
dataToSearch.addDocuments(bookList);
this.setState({ search: dataToSearch });
}
/**
* 検索ワード変更時に検索を行う
*/
searchData = e => {
const { search } = this.state
const queryResult = search.search(e.target.value);
this.setState({ searchQuery: e.target.value, searchResults: queryResult })
}
render() {
const { bookList, searchResults, searchQuery } = this.state
const queryResults = searchQuery === "" ? bookList : searchResults
return (
<div>
<div>
<div>
検索ワード:<input
id="Search"
value={searchQuery}
onChange={this.searchData}
/>
</div>
<div>
検索結果:{queryResults.length}
{queryResults.map(item => {
return (
<div key={item.slug}>
<span>{item.date}</span>
<Link to={item.slug} >{item.title}</Link>
</div>
)
})}
</div>
</div>
</div>
)
}
}
export default Search
画面の表示すると、最初は3件表示されていますが、検索ワードで絞り込みができます。
「その2」で検索。
画面には表示されていないけど、検索キーsearchに指定した「テーブル」で検索。
「2019」は画面に表示されているが、検索キーに指定していないので該当なし。
コメントもいっぱい書いたけど、せっかくなのでSearchContainerのまとめ。
コンポーネントの配置後に1回だけ実行される。
search.jsonの内容を取得して、stateのbookListに格納した後、rebuildIndexを実行。
JsSearchインスタンスのオプションや検索キーを設定し、stateのsearchに格納。
各オプションは自前で実装することも可能です。
stateからJsSearchインスタンスを取得し、入力された検索ワードで検索を行う。