{"id":306624,"date":"2020-04-22T07:54:48","date_gmt":"2020-04-22T14:54:48","guid":{"rendered":"https:\/\/css-tricks.com\/?p=306624"},"modified":"2021-08-18T07:47:08","modified_gmt":"2021-08-18T14:47:08","slug":"how-to-add-lunr-search-to-your-gatsby-website","status":"publish","type":"post","link":"https:\/\/css-tricks.com\/how-to-add-lunr-search-to-your-gatsby-website\/","title":{"rendered":"How to Add Lunr Search to Your Gatsby Website"},"content":{"rendered":"\n

The Jamstack way<\/a> of thinking and building websites is becoming more and more popular.<\/p>\n\n\n\n

Have you already tried Gatsby,<\/a> Nuxt,<\/a> or Gridsome<\/a> (to cite only a few)? Chances are that your first contact was a \u201cWow!\u201d moment \u2014 so many things are automatically set up and ready to use. <\/p>\n\n\n\n

There are some challenges, though, one of which is search functionality. If you\u2019re working on any sort of content-driven site, you\u2019ll likely run into search and how to handle it. Can it be done without any external server-side technology? <\/p>\n\n\n\n

Search is not<\/em> one of those things that come out of the box with Jamstack. Some extra decisions and implementation are required.<\/p>\n\n\n\n\n\n\n\n

Fortunately, we have a bunch of options that might be more or less adapted to a project. We could use Algolia\u2019s<\/a> powerful search-as-service API. It comes with a free plan that is restricted to non-commercial projects with  a limited capacity. If we were to use WordPress with WPGraphQL<\/a> as a data source, we could take advantage of WordPress native search functionality and Apollo Client.<\/a> Raymond Camden recently explored a few Jamstack search options<\/a>, including pointing a search form directly at Google.<\/p>\n\n\n\n

In this article, we will build a search index and add search functionality to a Gatsby website with Lunr<\/a>, a lightweight JavaScript library providing an extensible and customizable search without the need for external, server-side services. We used it recently to add \u201cSearch by Tartan Name\u201d to our Gatsby project tartanify.com<\/a>. We absolutely wanted persistent search as-you-type functionality, which brought some extra challenges. But that\u2019s what makes it interesting, right? I\u2019ll discuss some of the difficulties we faced and how we dealt with them in the second half of this article.<\/p>\n\n\n\n

\"\"<\/figure>\n\n\n

Getting started<\/h3>\n\n\n

For the sake of simplicity, let\u2019s use the official Gatsby blog starter<\/a>. Using a generic starter lets us abstract many aspects of building a static website. If you\u2019re following along, make sure to install and run it:<\/p>\n\n\n\n

gatsby new gatsby-starter-blog https:\/\/github.com\/gatsbyjs\/gatsby-starter-blog\ncd gatsby-starter-blog\ngatsby develop<\/code><\/pre>\n\n\n\n

It\u2019s a tiny blog with three posts we can view by opening up http:\/\/localhost:8000\/___graphql<\/code> in the browser.<\/p>\n\n\n\n

\"Showing<\/figure>\n\n\n

Inverting index with Lunr.js ?<\/h2>\n\n\n

Lunr uses a record-level inverted index<\/a> as its data structure. The inverted index stores the mapping for each word found within a website to its location (basically a set of page paths). It\u2019s on us to decide which fields (e.g. title, content, description, etc.) provide the keys (words) for the index.<\/p>\n\n\n\n

For our blog example, I decided to include all titles and the content of each article. Dealing with titles is straightforward since they are composed uniquely of words. Indexing content<\/em> is a little more complex. My first try was to use the rawMarkdownBody<\/code> field. Unfortunately, rawMarkdownBody<\/code> introduces some unwanted keys resulting from the markdown syntax.<\/p>\n\n\n\n

\"Showing<\/figure>\n\n\n\n

I obtained a \u201cclean\u201d index using the html field in conjunction with the striptags<\/a> package (which, as the name suggests, strips out the HTML tags). Before we get into the details, let\u2019s look into the Lunr documentation.<\/a><\/p>\n\n\n\n

Here\u2019s how we create and populate the Lunr index. We will use this snippet in a moment, specifically in our gatsby-node.js<\/code> file.<\/p>\n\n\n\n

const index = lunr(function () {\n  this.ref('slug')\n  this.field('title')\n  this.field('content')\n  for (const doc of documents) {\n    this.add(doc)\n  }\n})<\/code><\/pre>\n\n\n\n

 documents<\/code> is an array of objects, each with a slug<\/code>, title<\/code> and content<\/code> property:<\/p>\n\n\n\n

{\n\u00a0 slug: '\/post-slug\/',\n\u00a0 title: 'Post Title',\n\u00a0 content: 'Post content with all HTML tags stripped out.'\n}<\/code><\/pre>\n\n\n\n

We will define a unique document key (the slug<\/code>) and two fields (the title<\/code> and content<\/code>, or the key providers). Finally, we will add all of the documents, one by one.<\/p>\n\n\n\n

Let\u2019s get started.<\/p>\n\n\n

Creating an index in gatsby-node.js <\/h3>\n\n\n

Let\u2019s start by installing the libraries that we are going to use.<\/p>\n\n\n\n

yarn add lunr graphql-type-json striptags<\/code><\/pre>\n\n\n\n

Next, we need to edit the gatsby-node.js<\/code> file. The code from this file runs once in the process of building a site, and our aim is to add index creation to the tasks that Gatsby executes on build. <\/p>\n\n\n\n

CreateResolvers<\/a><\/code> is one of the Gatsby APIs controlling the GraphQL data layer. In this particular case, we will use it to create a new root field; Let’s call it LunrIndex<\/code>. <\/p>\n\n\n\n

Gatsby\u2019s internal data store and query capabilities are exposed to GraphQL field resolvers on context.nodeModel<\/code><\/a>. With getAllNodes<\/code><\/a>, we can get all nodes of a specified type:<\/p>\n\n\n\n

\/* gatsby-node.js *\/\nconst { GraphQLJSONObject } = require(`graphql-type-json`)\nconst striptags = require(`striptags`)\nconst lunr = require(`lunr`)\n\nexports.createResolvers = ({ cache, createResolvers }) => {\n  createResolvers({\n    Query: {\n      LunrIndex: {\n        type: GraphQLJSONObject,\n        resolve: (source, args, context, info) => {\n          const blogNodes = context.nodeModel.getAllNodes({\n            type: `MarkdownRemark`,\n          })\n          const type = info.schema.getType(`MarkdownRemark`)\n          return createIndex(blogNodes, type, cache)\n        },\n      },\n    },\n  })\n}<\/code><\/pre>\n\n\n\n

Now let\u2019s focus on the createIndex<\/code> function. That\u2019s where we will use the Lunr snippet we mentioned in the last section. <\/p>\n\n\n\n

\/* gatsby-node.js *\/\nconst createIndex = async (blogNodes, type, cache) => {\n\u00a0 const documents = []\n\u00a0 \/\/ Iterate over all posts\u00a0\n\u00a0 for (const node of blogNodes) {\n\u00a0 \u00a0 const html = await type.getFields().html.resolve(node)\n\u00a0 \u00a0 \/\/ Once html is resolved, add a slug-title-content object to the documents array\n\u00a0 \u00a0 documents.push({\n\u00a0 \u00a0 \u00a0 slug: node.fields.slug,\n\u00a0 \u00a0 \u00a0 title: node.frontmatter.title,\n\u00a0 \u00a0 \u00a0 content: striptags(html),\n\u00a0 \u00a0 })\n\u00a0 }\n\u00a0 const index = lunr(function() {\n\u00a0 \u00a0 this.ref(`slug`)\n\u00a0 \u00a0 this.field(`title`)\n\u00a0 \u00a0 this.field(`content`)\n\u00a0 \u00a0 for (const doc of documents) {\n\u00a0 \u00a0 \u00a0 this.add(doc)\n\u00a0 \u00a0 }\n\u00a0 })\n\u00a0 return index.toJSON()\n}<\/code><\/pre>\n\n\n\n

Have you noticed that instead of accessing the HTML element directly with  const html = node.html<\/code>, we\u2019re using an  await<\/code> expression? That\u2019s because node.html<\/code> isn\u2019t available yet. The gatsby-transformer-remark<\/a> plugin (used by our starter to parse Markdown files) does not generate HTML from markdown immediately when creating the MarkdownRemark<\/code> nodes. Instead,  html<\/code> is generated lazily when the html field resolver is called in a query. The same actually applies to the excerpt<\/code> that we will need in just a bit.<\/p>\n\n\n\n

Let\u2019s look ahead and think about how we are going to display search results. Users expect to obtain a link to the matching post, with its title as the anchor text. Very likely, they wouldn\u2019t mind a short excerpt as well.<\/p>\n\n\n\n

Lunr\u2019s search returns an array of objects representing matching documents by the ref<\/code> property (which is the unique document key slug<\/code> in our example). This array does not contain the document title nor the content. Therefore, we need to store somewhere the post title and excerpt corresponding to each slug. We can do that within our LunrIndex<\/code> as below:<\/p>\n\n\n\n

\/* gatsby-node.js *\/\nconst createIndex = async (blogNodes, type, cache) => {\n\u00a0 const documents = []\n\u00a0 const store = {}\n\u00a0 for (const node of blogNodes) {\n\u00a0 \u00a0 const {slug} = node.fields\n\u00a0 \u00a0 const title = node.frontmatter.title\n\u00a0 \u00a0 const [html, excerpt] = await Promise.all([\n\u00a0 \u00a0 \u00a0 type.getFields().html.resolve(node),\n\u00a0 \u00a0 \u00a0 type.getFields().excerpt.resolve(node, { pruneLength: 40 }),\n\u00a0 \u00a0 ])\n\u00a0 \u00a0 documents.push({\n\u00a0 \u00a0 \u00a0 \/\/ unchanged\n\u00a0 \u00a0 })\n\u00a0 \u00a0 store[slug] = {\n\u00a0 \u00a0 \u00a0 title,\n\u00a0 \u00a0 \u00a0 excerpt,\n\u00a0 \u00a0 }\n\u00a0 }\n\u00a0 const index = lunr(function() {\n\u00a0 \u00a0 \/\/ unchanged\n\u00a0 })\n\u00a0 return { index: index.toJSON(), store }\n}<\/code><\/pre>\n\n\n\n

Our search index changes only if one of the posts is modified or a new post is added. We don\u2019t need to rebuild the index each time we run gatsby develop<\/code>. To avoid unnecessary builds, let\u2019s take advantage of the cache API<\/a>:<\/p>\n\n\n\n

\/* gatsby-node.js *\/\nconst createIndex = async (blogNodes, type, cache) => {\n\u00a0 const cacheKey = `IndexLunr`\n\u00a0 const cached = await cache.get(cacheKey)\n\u00a0 if (cached) {\n\u00a0 \u00a0 return cached\n\u00a0 }\n\u00a0 \/\/ unchanged\n\u00a0 const json = { index: index.toJSON(), store }\n\u00a0 await cache.set(cacheKey, json)\n\u00a0 return json\n}<\/code><\/pre>\n\n\n

Enhancing pages with the search form component<\/h2>\n\n\n

We can now move on to the front end of our implementation. Let\u2019s start by building a search form component.<\/p>\n\n\n\n

touch src\/components\/search-form.js\u00a0<\/code><\/pre>\n\n\n\n

I opt for a straightforward solution: an input of type=\"search\"<\/code>, coupled with a label and accompanied by a submit button, all wrapped within a form tag with the search<\/code> landmark role.<\/p>\n\n\n\n

We will add two event handlers, handleSubmit<\/code> on form submit and handleChange<\/code> on changes to the search input.<\/p>\n\n\n\n

\/* src\/components\/search-form.js *\/\nimport React, { useState, useRef } from \"react\"\nimport { navigate } from \"@reach\/router\"\nconst SearchForm = ({ initialQuery = \"\" }) => {\n\u00a0 \/\/ Create a piece of state, and initialize it to initialQuery\n\u00a0 \/\/ query will hold the current value of the state,\n\u00a0 \/\/ and setQuery will let us change it\n\u00a0 const [query, setQuery] = useState(initialQuery)\n\u00a0\u00a0\n  \/\/ We need to get reference to the search input element\n\u00a0 const inputEl = useRef(null)\n\n\u00a0 \/\/ On input change use the current value of the input field (e.target.value)\n\u00a0 \/\/ to update the state's query value\n\u00a0 const handleChange = e => {\n\u00a0 \u00a0 setQuery(e.target.value)\n\u00a0 }\n\u00a0\u00a0\n\u00a0 \/\/ When the form is submitted navigate to \/search\n\u00a0 \/\/ with a query q paramenter equal to the value within the input search\n\u00a0 const handleSubmit = e => {\n\u00a0 \u00a0 e.preventDefault()\n    \/\/ `inputEl.current` points to the mounted search input element\n\u00a0 \u00a0 const q = inputEl.current.value\n\u00a0 \u00a0 navigate(`\/search?q=${q}`)\n\u00a0 }\n\u00a0 return (\n\u00a0 \u00a0 <form role=\"search\" onSubmit={handleSubmit}>\n\u00a0 \u00a0 \u00a0 <label htmlFor=\"search-input\" style={{ display: \"block\" }}>\n\u00a0 \u00a0 \u00a0 \u00a0 Search for:\n\u00a0 \u00a0 \u00a0 <\/label>\n\u00a0 \u00a0 \u00a0 <input\n\u00a0 \u00a0 \u00a0 \u00a0 ref={inputEl}\n\u00a0 \u00a0 \u00a0 \u00a0 id=\"search-input\"\n\u00a0 \u00a0 \u00a0 \u00a0 type=\"search\"\n\u00a0 \u00a0 \u00a0 \u00a0 value={query}\n\u00a0 \u00a0 \u00a0 \u00a0 placeholder=\"e.g. duck\"\n\u00a0 \u00a0 \u00a0 \u00a0 onChange={handleChange}\n\u00a0 \u00a0 \u00a0 \/>\n\u00a0 \u00a0 \u00a0 <button type=\"submit\">Go<\/button>\n\u00a0 \u00a0 <\/form>\n\u00a0 )\n}\nexport default SearchForm<\/code><\/pre>\n\n\n\n

Have you noticed that we\u2019re importing navigate<\/code> from the @reach\/router<\/code> package? That is necessary since neither Gatsby\u2019s <Link\/><\/code> nor navigate<\/code> provide in-route navigation with a query parameter. Instead, we can import @reach\/router<\/code> \u2014 there\u2019s no need to install it since Gatsby already includes it \u2014 and use its navigate<\/code> function.<\/p>\n\n\n\n

Now that we\u2019ve built our component, let\u2019s add it to our home page (as below) and 404 page.<\/p>\n\n\n\n

\/* src\/pages\/index.js *\/\n\/\/ unchanged\nimport SearchForm from \"..\/components\/search-form\"\nconst BlogIndex = ({ data, location }) => {\n\u00a0 \/\/ unchanged\n\u00a0 return (\n\u00a0 \u00a0 <Layout location={location} title={siteTitle}>\n\u00a0 \u00a0 \u00a0 <SEO title=\"All posts\" \/>\n\u00a0 \u00a0 \u00a0 <Bio \/>\n\u00a0 \u00a0 \u00a0 <SearchForm \/>\n\u00a0 \u00a0 \u00a0 \/\/ unchanged<\/code><\/pre>\n\n\n

Search results page<\/h3>\n\n\n

Our SearchForm<\/code> component navigates to the \/search<\/code> route when the form is submitted, but for the moment, there is nothing behing this URL. That means we need to add a new page:<\/p>\n\n\n\n

touch src\/pages\/search.js\u00a0<\/code><\/pre>\n\n\n\n

I proceeded by copying and adapting the content of the the index.js<\/code> page. One of the essential modifications concerns the page query<\/a> (see the very bottom of the file). We will replace allMarkdownRemark<\/code> with the LunrIndex<\/code> field. <\/p>\n\n\n\n

\/* src\/pages\/search.js *\/\nimport React from \"react\"\nimport { Link, graphql } from \"gatsby\"\nimport { Index } from \"lunr\"\nimport Layout from \"..\/components\/layout\"\nimport SEO from \"..\/components\/seo\"\nimport SearchForm from \"..\/components\/search-form\"\n\u2028\n\/\/ We can access the results of the page GraphQL query via the data props\nconst SearchPage = ({ data, location }) => {\n\u00a0 const siteTitle = data.site.siteMetadata.title\n\u00a0\u00a0\n\u00a0 \/\/ We can read what follows the ?q= here\n\u00a0 \/\/ URLSearchParams provides a native way to get URL params\n\u00a0 \/\/ location.search.slice(1) gets rid of the \"?\"\u00a0\n\u00a0 const params = new URLSearchParams(location.search.slice(1))\n\u00a0 const q = params.get(\"q\") || \"\"\n\u2028\n\u00a0 \/\/ LunrIndex is available via page query\n\u00a0 const { store } = data.LunrIndex\n\u00a0 \/\/ Lunr in action here\n\u00a0 const index = Index.load(data.LunrIndex.index)\n\u00a0 let results = []\n\u00a0 try {\n\u00a0 \u00a0 \/\/ Search is a lunr method\n\u00a0 \u00a0 results = index.search(q).map(({ ref }) => {\n\u00a0 \u00a0 \u00a0 \/\/ Map search results to an array of {slug, title, excerpt} objects\n\u00a0 \u00a0 \u00a0 return {\n\u00a0 \u00a0 \u00a0 \u00a0 slug: ref,\n\u00a0 \u00a0 \u00a0 \u00a0 ...store[ref],\n\u00a0 \u00a0 \u00a0 }\n\u00a0 \u00a0 })\n\u00a0 } catch (error) {\n\u00a0 \u00a0 console.log(error)\n\u00a0 }\n\u00a0 return (\n\u00a0 \u00a0 \/\/ We will take care of this part in a moment\n\u00a0 )\n}\nexport default SearchPage\nexport const pageQuery = graphql`\n\u00a0 query {\n\u00a0 \u00a0 site {\n\u00a0 \u00a0 \u00a0 siteMetadata {\n\u00a0 \u00a0 \u00a0 \u00a0 title\n\u00a0 \u00a0 \u00a0 }\n\u00a0 \u00a0 }\n\u00a0 \u00a0 LunrIndex\n\u00a0 }\n`<\/code><\/pre>\n\n\n\n

Now that we know how to retrieve the query value and the matching posts, let\u2019s display the content of the page. Notice that on the search page we pass the query value to the <SearchForm \/><\/code> component via the initialQuery<\/code> props. When the user arrives to the search results page, their search query should remain in the input field. <\/p>\n\n\n\n

return (\n\u00a0 <Layout location={location} title={siteTitle}>\n\u00a0 \u00a0 <SEO title=\"Search results\" \/>\n\u00a0 \u00a0 {q ? <h1>Search results<\/h1> : <h1>What are you looking for?<\/h1>}\n\u00a0 \u00a0 <SearchForm initialQuery={q} \/>\n\u00a0 \u00a0 {results.length ? (\n\u00a0 \u00a0 \u00a0 results.map(result => {\n\u00a0 \u00a0 \u00a0 \u00a0 return (\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <article key={result.slug}>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <h2>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <Link to={result.slug}>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 {result.title || result.slug}\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <\/Link>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <\/h2>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <p>{result.excerpt}<\/p>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <\/article>\n\u00a0 \u00a0 \u00a0 \u00a0 )\n\u00a0 \u00a0 \u00a0 })\n\u00a0 \u00a0 ) : (\n\u00a0 \u00a0 \u00a0 <p>Nothing found.<\/p>\n\u00a0 \u00a0 )}\n\u00a0 <\/Layout>\n)<\/code><\/pre>\n\n\n\n

You can find the complete code in this gatsby-starter-blog fork<\/a> and the live demo deployed on Netlify<\/a>.<\/p>\n\n\n

Instant search widget<\/strong><\/h3>\n\n\n

Finding the most \u201clogical\u201d and user-friendly way of implementing search may be a challenge in and of itself. Let\u2019s now switch to the real-life example of tartanify.com<\/a> \u2014 a Gatsby-powered website gathering 5,000+ tartan patterns. Since tartans are often associated with clans or organizations, the possibility to search a tartan by name <\/em>seems to make sense. <\/p>\n\n\n\n

We built tartanify.com as a side project where we feel absolutely free to experiment with things. We didn\u2019t want a classic search results page but an instant search <\/em><\/strong>\u201cwidget.\u201d Often, a given search keyword corresponds with a number of results \u2014 for example, \u201cRamsay\u201d comes in six variations.  We imagined the search widget would be persistent, meaning it should stay in place when a user navigates from one matching tartan to another.<\/p>\n\n\n\n

\/* gatsby-node.js *\/\nexports.createResolvers = ({ cache, createResolvers }) => {\n\u00a0 createResolvers({\n\u00a0 \u00a0 Query: {\n\u00a0 \u00a0 \u00a0 LunrIndex: {\n\u00a0 \u00a0 \u00a0 \u00a0 type: GraphQLJSONObject,\n\u00a0 \u00a0 \u00a0 \u00a0 resolve(source, args, context) {\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 const siteNodes = context.nodeModel.getAllNodes({\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 type: `TartansCsv`,\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 })\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 return createIndex(siteNodes, cache)\n\u00a0 \u00a0 \u00a0 \u00a0 },\n\u00a0 \u00a0 \u00a0 },\n\u00a0 \u00a0 },\n\u00a0 })\n}\nconst createIndex = async (nodes, cache) => {\n\u00a0 const cacheKey = `LunrIndex`\n\u00a0 const cached = await cache.get(cacheKey)\n\u00a0 if (cached) {\n\u00a0 \u00a0 return cached\n\u00a0 }\n\u00a0 const store = {}\n\u00a0 const index = lunr(function() {\n\u00a0 \u00a0 this.ref(`slug`)\n\u00a0 \u00a0 this.field(`title`)\n\u00a0 \u00a0 for (node of nodes) {\n\u00a0 \u00a0 \u00a0 const { slug } = node.fields\n\u00a0 \u00a0 \u00a0 const doc = {\n\u00a0 \u00a0 \u00a0 \u00a0 slug,\n\u00a0 \u00a0 \u00a0 \u00a0 title: node.fields.Unique_Name,\n\u00a0 \u00a0 \u00a0 }\n\u00a0 \u00a0 \u00a0 store[slug] = {\n\u00a0 \u00a0 \u00a0 \u00a0 title: doc.title,\n\u00a0 \u00a0 \u00a0 }\n\u00a0 \u00a0 \u00a0 this.add(doc)\n\u00a0 \u00a0 }\n\u00a0 })\n\u00a0 const json = { index: index.toJSON(), store }\n\u00a0 cache.set(cacheKey, json)\n\u00a0 return json\n}<\/code><\/pre>\n\n\n\n

We opted for instant search, which means that search is triggered by any change in the search input instead of a form submission.<\/p>\n\n\n\n

\/* src\/components\/searchwidget.js *\/\nimport React, { useState } from \"react\"\nimport lunr, { Index } from \"lunr\"\nimport { graphql, useStaticQuery } from \"gatsby\"\nimport SearchResults from \".\/searchresults\"\n\u2028\nconst SearchWidget = () => {\n\u00a0 const [value, setValue] = useState(\"\")\n\u00a0 \/\/ results is now a state variable\u00a0\n\u00a0 const [results, setResults] = useState([])\n\u2028\n\u00a0 \/\/ Since it's not a page component, useStaticQuery for quering data\n\u00a0 \/\/ https:\/\/www.gatsbyjs.org\/docs\/use-static-query\/\n\u00a0 const { LunrIndex } = useStaticQuery(graphql`\n\u00a0 \u00a0 query {\n\u00a0 \u00a0 \u00a0 LunrIndex\n\u00a0 \u00a0 }\n\u00a0 `)\n\u00a0 const index = Index.load(LunrIndex.index)\n\u00a0 const { store } = LunrIndex\n\u00a0 const handleChange = e => {\n\u00a0 \u00a0 const query = e.target.value\n\u00a0 \u00a0 setValue(query)\n\u00a0 \u00a0 try {\n\u00a0 \u00a0 \u00a0 const search = index.search(query).map(({ ref }) => {\n\u00a0 \u00a0 \u00a0 \u00a0 return {\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 slug: ref,\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 ...store[ref],\n\u00a0 \u00a0 \u00a0 \u00a0 }\n\u00a0 \u00a0 \u00a0 })\n\u00a0 \u00a0 \u00a0 setResults(search)\n\u00a0 \u00a0 } catch (error) {\n\u00a0 \u00a0 \u00a0 console.log(error)\n\u00a0 \u00a0 }\n\u00a0 }\n\u00a0 return (\n\u00a0 \u00a0 <div className=\"search-wrapper\">\n\u00a0 \u00a0 \u00a0 \/\/ You can use a form tag as well, as long as we prevent the default submit behavior\n\u00a0 \u00a0 \u00a0 <div role=\"search\">\n\u00a0 \u00a0 \u00a0 \u00a0 <label htmlFor=\"search-input\" className=\"visually-hidden\">\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 Search Tartans by Name\n\u00a0 \u00a0 \u00a0 \u00a0 <\/label>\n\u00a0 \u00a0 \u00a0 \u00a0 <input\n          id=\"search-input\"\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 type=\"search\"\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 value={value}\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 onChange={handleChange}\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 placeholder=\"Search Tartans by Name\"\n\u00a0 \u00a0 \u00a0 \u00a0 \/>\n\u00a0 \u00a0 \u00a0 <\/div>\n\u00a0 \u00a0 \u00a0 <SearchResults results={results} \/>\n\u00a0 \u00a0 <\/div>\n\u00a0 )\n}\nexport default SearchWidget<\/code><\/pre>\n\n\n\n

The SearchResults<\/code> are structured like this:<\/p>\n\n\n\n

\/* src\/components\/searchresults.js *\/\nimport React from \"react\"\nimport { Link } from \"gatsby\"\nconst SearchResults = ({ results }) => (\n\u00a0 <div>\n\u00a0 \u00a0 {results.length ? (\n\u00a0 \u00a0 \u00a0 <>\n\u00a0 \u00a0 \u00a0 \u00a0 <h2>{results.length} tartan(s) matched your query<\/h2>\n\u00a0 \u00a0 \u00a0 \u00a0 <ul>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 {results.map(result => (\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <li key={result.slug}>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <Link to={`\/tartan\/${result.slug}`}>{result.title}<\/Link>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <\/li>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 ))}\n\u00a0 \u00a0 \u00a0 \u00a0 <\/ul>\n\u00a0 \u00a0 \u00a0 <\/>\n\u00a0 \u00a0 ) : (\n\u00a0 \u00a0 \u00a0 <p>Sorry, no matches found.<\/p>\n\u00a0 \u00a0 )}\n\u00a0 <\/div>\n)\nexport default SearchResults<\/code><\/pre>\n\n\n

Making it persistent<\/h3>\n\n\n

Where should we use this component? We could add it to the Layout<\/code> component. The problem is that our search form will get unmounted on page changes that way. If a user wants to browser all tartans associated with the \u201cRamsay\u201d clan, they will have to retype their query several times. That\u2019s not ideal.<\/p>\n\n\n\n

Thomas Weibenfalk<\/a> has written a great article on keeping state between pages with local state in Gatsby.js<\/a>. We will use the same technique, where the wrapPageElement<\/code><\/a> browser API sets persistent UI elements around pages. <\/p>\n\n\n\n

Let\u2019s add the following code to the gatsby-browser.js<\/code>. You might<\/em> need to add this file to the root of your project.<\/p>\n\n\n\n

\/* gatsby-browser.js *\/\nimport React from \"react\"\nimport SearchWrapper from \".\/src\/components\/searchwrapper\"\nexport const wrapPageElement = ({ element, props }) => (\n\u00a0 <SearchWrapper {...props}>{element}<\/SearchWrapper>\n)<\/code><\/pre>\n\n\n\n

Now let\u2019s add a new component file:<\/p>\n\n\n\n

touch src\/components\/searchwrapper.js<\/code><\/pre>\n\n\n\n

Instead of adding SearchWidget<\/code> component to the Layout<\/code>, we will add it to the SearchWrapper<\/code> and the magic happens. ✨<\/p>\n\n\n\n

\/* src\/components\/searchwrapper.js *\/\nimport React from \"react\"\nimport SearchWidget from \".\/searchwidget\"\n\u2028\nconst SearchWrapper = ({ children }) => (\n\u00a0 <>\n\u00a0 \u00a0 {children}\n\u00a0 \u00a0 <SearchWidget \/>\n\u00a0 <\/>\n)\nexport default SearchWrapper<\/code><\/pre>\n\n\n

Creating a custom search query<\/strong><\/h3>\n\n\n

At this point, I started to try different keywords but very quickly realized that Lunr\u2019s default search query might not be the best solution when used for instant search.<\/p>\n\n\n\n

Why? Imagine that we are looking for tartans associated with the name MacCallum. <\/em>While typing \u201cMacCallum\u201d letter-by-letter, this is the evolution of the results:<\/p>\n\n\n\n