How to add Algolia Search to NuxtJS

Last updated at April 19, 2021

💬

You can provides powerful search feature in your websiste using Algolia.

Install required packages from npm

We need install total 5 package to finish this task.

bash
npm install vue-instantsearch algoliasearch nuxt-content-algolia remove-markdown v-click-outside --save

Create custom plugin file(vue-instantsearch.js) in /plugins folder

/plugins/vue-instantsearch
import Vue from 'vue'
import InstantSearch from 'vue-instantsearch'

Vue.use(InstantSearch)

Register custom plugin to nuxt.config.js

/nuxt.config.js
export default {
  // ...
  plugins: [
    '~/plugins/vue-instantsearch'
  ],
  // ...
}

Set transpile build configuration in nuxt.config.js

/nuxt.config.js
export default {
  build: {
    transpile: ['vue-instantsearch', 'instantsearch.js/es']
  },
}

Configure nuxt-content-algolia to send index to Algolia

/nuxt.config.js
export default {
  // ...
  buildModules: [
    'nuxt-content-algolia'
  ],
  nuxtContentAlgolia: {
    appId: process.env.ALGOLIA_APP_ID,
    apiKey: process.env.ALGOLIA_API_KEY,
    paths: [
      {
        name: 'articles',
        index: 'articles',
        fields: ['title', 'description', 'bodyPlainText']
      }
    ]
  },
}

Create .env file and set required variables

Algolia API Key is is not your search only key but the key that grants access to modify the index.

You can generate a new API key in Algolia admin page.

/.env
ALGOLIA_APP_ID=your-algolia-app-id
ALGOLIA_API_KEY=your-algolia-api-key

Create custom hook to remove html from content body

Html tags and other attributes is not needed when search. So, we create a plain body text using remove-markdown package.

/nuxt.config.js
export default {
  // ...
  hooks: {
    'content:file:beforeInsert': (content) => {
      const removeMd = require('remove-markdown');
      if (content.extension == 'md') {
        content.bodyPlainText = removeMd(content.text);
      }
    }
  }
}

Create Search.vue component in /components folder

/components/Search.vue
<template>
  <ais-instant-search
    :search-client="searchClient"
    index-name="articles"
  >
    <ais-configure :attributesToSnippet="['bodyPlainText']" :hits-per-page.camel="5" />
    <ais-autocomplete v-click-outside="onClickOutside">
      <div slot-scope="{ currentRefinement, indices, refine }" class="md:relative">
        <div class="relative">
          <font-awesome-icon :icon="['fas', 'search']" class="absolute h-4 text-gray-400 mt-3 ml-3" />
          <input
            type="search"
            ref="searchInput"
            class="w-full py-2 px-4 pl-10 bg-gray-100 rounded"
            :value="currentRefinement"
            @input="refine($event.currentTarget.value)"
            placeholder="Search - Ctrl+K to focus"
            autocomplete="off"
            @focus="showResults = true"
            @keydown.up.prevent="highlightPrevious(indices[0].hits.length)"
            @keydown.down.prevent="highlightNext(indices[0].hits.length)"
            @keydown.enter="goToArticle(indices)"
          >
        </div>
        <div v-if="currentRefinement.length && showResults" class="absolute right-0 z-10 transform mt-3 px-2 w-screen max-w-md sm:px-0">
          <div class="rounded-lg shadow-lg ring-1 ring-black ring-opacity-5 overflow-hidden">
            <div class="relative grid gap-6 bg-white text-gray-700 px-4 py-4 sm:gap-8 sm:p-4">
              <div v-if="currentRefinement" v-for="section in indices" :key="section.objectID" class="divide-y divide-gray-300">
                <div v-if="section.hits.length">
                  <h2 class="uppercase text-gray-700 py-1 px-2">{{ section.indexName }}</h2>
                </div>
                <NuxtLink to="#" v-for="(hit, index) in section.hits" :key="hit.objectID"
                  class="block text-sm col-span-2 py-2 transition ease-in-out duration-150"
                  :class="{ 'bg-gray-100': isCurrentIndex(index) }">
                  <div class="px-2" @mouseover="highlightedIndex = index">
                    <ais-highlight attribute="title" :hit="hit" class="block text-gray-600 font-semibold tracking-wide" />
                    <ais-snippet attribute="bodyPlainText" :hit="hit" class="block text-gray-400 font-base" />
                  </div>
                </NuxtLink>
              </div>
              <ais-powered-by theme="light" class="px-2" />
            </div>
          </div>
        </div>
      </div>
    </ais-autocomplete>
  </ais-instant-search>
</template>

<script>
import algoliasearch from 'algoliasearch/lite'
import vClickOutside from 'v-click-outside'

export default {
  directives: {
    clickOutside: vClickOutside.directive
  },
  data() {
    return {
      searchClient: algoliasearch ('6C3W4JP2I6', 'ea72adbc6e9f7b4da0b111f7319cd3a3'),
      showResults: false,
      highlightedIndex: -1 
    }
  },
  mounted() {
    this.$nextTick(function () {
      window.addEventListener('keydown', event => {
        if((event.metaKey || event.ctrlKey) && event.key === 'k') {
          this.$refs.searchInput.focus()
          event.preventDefault()
        }
      })
    })
  },
  watch: {
    '$route' () {
      this.showResults = false
      this.$refs.searchInput.blur()
    }
  },
  methods: {
    onClickOutside() {
      this.showResults = false
    },
    highlightPrevious(resultsCount) {
      if (this.highlightedIndex > 0) {
        this.highlightedIndex -= 1
      } else {
        this.highlightedIndex = resultsCount - 1
      }
    },
    highlightNext(resultsCount) {
      if (this.highlightedIndex < resultsCount - 1) {
        this.highlightedIndex += 1
      } else {
        this.highlightedIndex = 0
      }
    },
    isCurrentIndex(index) {
      return index === this.highlightedIndex
    },
    goToArticle(indices) {
      this.$nuxt.$router.push('/articles/' + indices[0].hits[this.highlightedIndex].objectID)
    }
  }
}
</script>

Insert <Search /> vue component in your layout of page

xxx.vue
<template>
  <header>
    <Search />
  </header>
</template>