Physical Address

304 North Cardinal St.
Dorchester Center, MA 02124

How to add Algolia Search to NuxtJS

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

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.

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.js

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

<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

<template>
  <header>
    <Search />
  </header>
</template> 

Leave a Reply

Your email address will not be published. Required fields are marked *