Skip to content

Commit c8cb35d

Browse files
committed
add basic lunr search implementation
1 parent f62201f commit c8cb35d

File tree

5 files changed

+227
-0
lines changed

5 files changed

+227
-0
lines changed

gatsby-config.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,47 @@ module.exports = {
8282
path: `${__dirname}/content/media`,
8383
},
8484
},
85+
{
86+
resolve: `gatsby-plugin-lunr`,
87+
options: {
88+
languages: [
89+
// {
90+
// // ISO 639-1 language codes. See https://lunrjs.com/guides/language_support.html for details
91+
// name: 'en',
92+
// // A function for filtering nodes. () => true by default
93+
// filterNodes: node => node.frontmatter.lang === 'en',
94+
// // Add to index custom entries, that are not actually extracted from gatsby nodes
95+
// customEntries: [{ title: 'Pictures', content: 'awesome pictures', url: '/pictures' }],
96+
// },
97+
// {
98+
// name: 'fr',
99+
// filterNodes: node => node.frontmatter.lang === 'fr',
100+
// },
101+
],
102+
// Fields to index. If store === true value will be stored in index file.
103+
// Attributes for custom indexing logic. See https://lunrjs.com/docs/lunr.Builder.html for details
104+
fields: [
105+
{ name: 'title', store: true, attributes: { boost: 20 } },
106+
{ name: 'content' },
107+
{ name: 'url', store: true },
108+
],
109+
// How to resolve each field's value for a supported node type
110+
resolvers: {
111+
// For any node of type MarkdownRemark, list how to resolve the fields' values
112+
MarkdownRemark: {
113+
title: node => node.frontmatter.title,
114+
content: node => node.rawMarkdownBody,
115+
url: node => node.fields.url,
116+
},
117+
},
118+
//custom index file name, default is search_index.json
119+
filename: 'search_index.json',
120+
//custom options on fetch api call for search_ındex.json
121+
fetchOptions: {
122+
credentials: 'same-origin',
123+
},
124+
},
125+
},
85126
/* work with images
86127
- need to be after sourcing images
87128
- docs: https://www.gatsbyjs.com/plugins/gatsby-plugin-image */

gatsby-node.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,61 @@ exports.onCreateNode = props => {
1717
addPathFieldToMarkdown(props);
1818
}
1919
};
20+
21+
const { GraphQLJSONObject } = require(`graphql-type-json`)
22+
const striptags = require(`striptags`)
23+
const lunr = require(`lunr`)
24+
25+
exports.createResolvers = ({ cache, createResolvers }) => {
26+
createResolvers({
27+
Query: {
28+
LunrIndex: {
29+
type: GraphQLJSONObject,
30+
resolve: (source, args, context, info) => {
31+
const markdownNodes = context.nodeModel.getAllNodes({
32+
type: `MarkdownRemark`,
33+
})
34+
const type = info.schema.getType(`MarkdownRemark`)
35+
return createIndex(markdownNodes, type, cache)
36+
},
37+
},
38+
},
39+
})
40+
}
41+
const createIndex = async (markdownNodes, type, cache) => {
42+
const cacheKey = `IndexLunr`
43+
const cached = await cache.get(cacheKey)
44+
if (cached) {
45+
return cached
46+
}
47+
const documents = []
48+
const store = {}
49+
for (const node of markdownNodes) {
50+
const {path} = node.fields
51+
const title = node.frontmatter.title
52+
const [html, excerpt] = await Promise.all([
53+
type.getFields().html.resolve(node),
54+
type.getFields().excerpt.resolve(node, { pruneLength: 40 }),
55+
])
56+
documents.push({
57+
path: node.fields.path,
58+
title: node.frontmatter.title,
59+
content: striptags(html),
60+
})
61+
store[path] = {
62+
title,
63+
excerpt,
64+
}
65+
}
66+
const index = lunr(function() {
67+
this.ref(`path`)
68+
this.field(`title`)
69+
this.field(`content`)
70+
for (const doc of documents) {
71+
this.add(doc)
72+
}
73+
})
74+
const json = { index: index.toJSON(), store }
75+
await cache.set(cacheKey, json)
76+
return json
77+
}

src/components/site/Header.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Logo from '../site/Logo';
99
import DocsButton from './DocsButton';
1010
import NavMain from './NavMain';
1111
import NavMobile from './NavMobile';
12+
import SearchForm from './SearchForm';
1213
import ThemeToggle from './ThemeToggle';
1314

1415
const Header = ({ mode, header }) => {
@@ -63,6 +64,9 @@ const Header = ({ mode, header }) => {
6364
<NavMain header={header} isScrolled={isScrolled} />
6465
</div>
6566
<div className=" w-1/2 flex items-center justify-end">
67+
<div>
68+
<SearchForm />
69+
</div>
6670
<div className="pl-8 pr-6">
6771
<DocsButton />
6872
</div>

src/components/site/SearchForm.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/* src/components/search-form.js */
2+
import { navigate } from '@reach/router';
3+
import React, { useRef, useState } from 'react';
4+
5+
import Icon from '../default/Icon';
6+
const SearchForm = ({ initialQuery = '' }) => {
7+
// Create a piece of state, and initialize it to initialQuery
8+
// query will hold the current value of the state,
9+
// and setQuery will let us change it
10+
const [query, setQuery] = useState(initialQuery);
11+
12+
// We need to get reference to the search input element
13+
const inputEl = useRef(null);
14+
15+
// On input change use the current value of the input field (e.target.value)
16+
// to update the state's query value
17+
const handleChange = e => {
18+
setQuery(e.target.value);
19+
};
20+
21+
// When the form is submitted navigate to /search
22+
// with a query q paramenter equal to the value within the input search
23+
const handleSubmit = e => {
24+
e.preventDefault();
25+
// `inputEl.current` points to the mounted search input element
26+
const q = inputEl.current.value;
27+
navigate(`/search?q=${q}`);
28+
};
29+
return (
30+
<form
31+
role="search"
32+
onSubmit={handleSubmit}
33+
className="border-none flex h-full items-center justify-between p-2 cursor-text active:outline-none focus:outline-none"
34+
>
35+
<input
36+
ref={inputEl}
37+
id="search-input"
38+
type="search"
39+
value={query}
40+
placeholder="Search Docs"
41+
onChange={handleChange}
42+
className="border-none mb-0 pr-4 text-sm text-substrateDark dark:text-white text-opacity-25 dark:text-opacity-9 border-none bg-substrateGray active:outline-none focus:outline-none ring-offset-transparent rounded"
43+
/>
44+
<button
45+
type="submit"
46+
className="h-9 ml-2 flex items-center justify-center bg-substrateDark dark:bg-white text-sm py-2 w-10 rounded focus:outline-none fill-current text-white dark:text-black"
47+
>
48+
<Icon name="search" width="18" className="h-9 fill-current dark:text-black text-white" />
49+
</button>
50+
</form>
51+
);
52+
};
53+
export default SearchForm;

src/pages/search.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/* src/pages/search.js */
2+
import { graphql, Link } from 'gatsby';
3+
import { Index } from 'lunr';
4+
import React from 'react';
5+
6+
import Layout from '../components/site/Layout';
7+
//import SearchForm from '../components/site/SearchForm';
8+
import SEO from '../components/site/SEO';
9+
10+
// We can access the results of the page GraphQL query via the data props
11+
const SearchPage = ({ data, location }) => {
12+
const siteTitle = data.site.siteMetadata.title;
13+
14+
// We can read what follows the ?q= here
15+
// URLSearchParams provides a native way to get URL params
16+
// location.search.slice(1) gets rid of the "?"
17+
const params = new URLSearchParams(location.search.slice(1));
18+
const q = params.get('q') || '';
19+
20+
// LunrIndex is available via page query
21+
const { store } = data.LunrIndex;
22+
// Lunr in action here
23+
const index = Index.load(data.LunrIndex.index);
24+
let results = [];
25+
try {
26+
// Search is a lunr method
27+
results = index.search(q).map(({ ref }) => {
28+
// Map search results to an array of {slug, title, excerpt} objects
29+
return {
30+
path: ref,
31+
...store[ref],
32+
};
33+
});
34+
} catch (error) {
35+
console.log(error);
36+
}
37+
return (
38+
<Layout location={location} title={siteTitle}>
39+
<SEO title="Search results" />
40+
<div className="pt-5 w-8/12 block text-left mx-auto">
41+
{q ? <h1>Search results for &quot;{q}&quot;</h1> : <h1>What are you looking for?</h1>}
42+
{results.length ? (
43+
results.map(result => {
44+
return (
45+
<article key={result.path}>
46+
{console.log(result)}
47+
<h2>
48+
<Link to={result.path}>{result.title || result.path}</Link>
49+
</h2>
50+
<p>{result.excerpt}</p>
51+
</article>
52+
);
53+
})
54+
) : (
55+
<p>Nothing found.</p>
56+
)}
57+
</div>
58+
</Layout>
59+
);
60+
};
61+
export default SearchPage;
62+
export const pageQuery = graphql`
63+
query {
64+
site {
65+
siteMetadata {
66+
title
67+
}
68+
}
69+
LunrIndex
70+
}
71+
`;

0 commit comments

Comments
 (0)