Quick & Dirty: How to add a client search feature to your Ghost blog
Tags: javascript, webtech
Published: 2021-10-17 16:38:10

> Note: This article assumes that you have a basic idea about Ghost theme development. If you need a refresher, [check out the official docs](https://ghost.org/docs/themes/) before reading ahead.

TL: DR - Take me to the code.

*   [The related partial](https://github.com/tq-bit/candidus-lite/blob/master/partials/search.hbs)
*   [The wrapper function](https://github.com/tq-bit/candidus-lite/blob/master/assets/js/lib/search.js)
*   [The plugin registration](https://github.com/tq-bit/candidus-lite/blob/master/assets/js/index.js) (starting @ line 18)
*   [The gulp task to build the web worker](https://github.com/tq-bit/candidus-lite/blob/master/gulpfile.js) (starting @ line 96)

The content of these files might change in the future, but the logic will remain the same.

Considerations for development
------------------------------

I created this feature with the following questions in mind:

*   Where do I get the post data from? -> [Ghost's Content API](https://ghost.org/docs/content-api/)
*   Do I want to execute an HTTP request every time I make a search? -> No
*   How do I store relevant information in the client? -> I'll use a [clientside index](https://en.wikipedia.org/wiki/Search_engine_indexing)
*   How do I create and search through an index? -> With the fantastic library [Lunr.js](https://lunrjs.com/)
*   Building an index takes time. How do I reduce stress on the main thread? -> By using a dedicated [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Worker) to build the index every X hours

Naturally, you could do the same thing on the server-side. I'm happy with how the result turned out though. So let's dive in.

Build the partial
-----------------

> Link to Github: [The related partial](https://github.com/tq-bit/candidus-lite/blob/master/partials/search.hbs)

The result looks like this.

![](https://blog.q-bit.me/content/images/2021/10/image.png)

It will render all the results from my local index whenever a search is made by the user.

![](https://blog.q-bit.me/content/images/2021/10/image-1.png)

Register the plugin
-------------------

To build an index, we need to feed it with data. We'll receive these from Ghost's content API, which requires an API key to be accessible.  So head over to your admin panel and [create a new integration](https://ghost.org/integrations/custom-integrations/).

Then, [inject the API key](https://ghost.org/help/using-the-editor/#using-code-injection), as well as the domain of your blog.

```html
<script>
const Q_GHOST_API_ROOT = 'https://blog.q-bit.me';
const Q_GHOST_API_KEY = 'content-api-key';
</script>
```

Next, we can use the plugin `useLunrSearch` and extract the necessary functions:

```js
const { 
 initLocalIndex, 
 search, 
 // ... other methods
} = useLunrSearch(Q_GHOST_API_ROOT, Q_GHOST_API_KEY);
```

Let's check the next two functions in detail now.

Initialize a local index
------------------------

We'll need to take the following three steps:

1.  Get the data from the API -> Done by function `[fetchPosts](https://github.com/tq-bit/candidus-lite/blob/master/assets/js/lib/search.js)` (line 123)
2.  Create an index -> Done by WebWorker's `[buildIndex](https://github.com/tq-bit/candidus-lite/blob/master/assets/js/workers/indexworker.js)`
3.  Save the index and a timestamp to local storage -> Done by `[saveIndex](https://github.com/tq-bit/candidus-lite/blob/master/assets/js/lib/search.js)` (line 113)

Let's take a look at how it's done in detail.

### Get the posts

We've passed the URL, as well as the API - key to the parent function `useLunrSearch`, so they're both available within the nested function's scope. Since we will fetch the posts only once to build our index, I felt free enough to set the limit to a bigger number.

In the [search.js](https://github.com/tq-bit/candidus-lite/blob/master/assets/js/lib/search.js) file:

```js
const fetchPosts = async (limit = 9999) => {
  if (!rootUrl) {
    console.warn('No rootUrl url specified. Using relative path');
  }
  if (!key) {
    throw new Error('No content api key found: Q_GHOST_API_KEY is undefined');
  }

  const url = rootUrl + path + '?limit=' + limit + '&key=' + key;
  const response = await fetch(url);
  const { meta, posts } = await response.json();
  return posts;
};
```

### Build the index

As creating an index is a memory-intensive task, it makes sense to offload it to a Web Worker. It'll do its thing on a separate thread and delivers the result back when it's ready.

> Make sure lunr.js is available in both, the search.js and the indexworker.js context - both files will be executed in separate contexts of the browser.

In the [`search.js`](https://github.com/tq-bit/candidus-lite/blob/master/assets/js/lib/search.js) - file

```js
// register the worker
const indexWorker = new Worker('/assets/built/workers/indexworker.js');

//  get the posts
const posts = await fetchPosts();

// send them to the worker
indexWorker.postMessage(posts);

// receive the index back from the worker
indexWorker.onmessage = (ev) => {
 const { data: serializedIndex } = ev;
 const lunrIndex = JSON.parse(serializedIndex);
 const timestamp = saveIndex(lunrIndex);
 return timestamp;
};
// ...
```

And then in the `[indexworker.js](https://github.com/tq-bit/candidus-lite/blob/master/assets/js/workers/indexworker.js)` file:

1.  We'll create a `blueprint` variable to hold the serialized title and the URL
2.  We'll receive this string back when searching for posts - it'll be our reference
3.  Lunr then creates the index by classifying posts and adding documents to it
4.  Finally, the index is serialized and returned to the calling function when it's requested

```js
const buildIndex = (posts) => {
  const documents = posts.map((post) => {
    const blueprint = JSON.stringify({ // 1
      title: post.title,
      url: post.url,
    });
    return {
      blueprint,
      title: post.title,
      excerpt: post.excerpt,
      html: post.html,
    };
  });

  const lunrIndex = lunr(function () {
    this.ref('blueprint'); // 2
    this.field('title');
    this.field('excerpt');
    this.field('html');
    documents.forEach(function (doc) {
      this.add(doc); // 3
    }, this);
  });
  return lunrIndex;
};

onmessage = ({ data }) => {
  const lunrIndex = buildIndex(data);
  const serializedIndex = JSON.stringify(lunrIndex); // 4
  postMessage(serializedIndex);
};
```

### Store the result in local storage and retrieve it

Now that we've got the index, we'll have to store it somewhere. The easiest way to do so turned out to be local storage. And to ensure it is regularly updated, we'll add a timestamp to it when it's saved.

In the [`search.js`](https://github.com/tq-bit/candidus-lite/blob/master/assets/js/lib/search.js) - file

```js
const saveIndex = (lunrIndex) => {
  const timestamp = new Date().getTime();
  const payload = {
    timestamp,
    lunrIndex,
  };
  localStorage.setItem(localIndexName, JSON.stringify(payload));
  return timestamp;
};

const getIndex = () => {
  const payload = JSON.parse(localStorage.getItem(localIndexName));
  if (payload) {
    return {
      timestamp: payload.timestamp,
      // Load the deserialized index 
      lunrIndex: lunr.Index.load(payload.lunrIndex),
    };
  }
};
```

Query the index for blueprints
------------------------------

And that's it. We can now use `getIndex` and query for something a user puts in. In my example, I use the `blueprints` (serialized title & URL) to render a list of items.

In the [`search.js`](https://github.com/tq-bit/candidus-lite/blob/master/assets/js/lib/search.js) - file

```js
const search = (ev) => {
  const {
    target: { value },
  } = ev;
  const { lunrIndex } = getIndex();
  const blueprints = queryIndex(lunrIndex, value);
  return renderQueryResults(value, blueprints);
};
```

If you are reading this post on my [blog.q-bit.me](https://blog.q-bit.me/), try and press `CTRL` + `K` to see the plugin in action.

> This feature is part of my Ghost theme Candidus-Lite. If the post above helped you, I'd appreciate [if you starred its repos on Github](https://github.com/tq-bit/candidus-lite)