Setup Algolia InstantSearch on Nuxt (with Query/Routing Url and SSR)

August 19, 2020
Perform search via query parameter

Install

npm install vue-instantsearch algoliasearch instantsearch.css

Setup

Edit nuxt.config.js

export default {
  // ...
  /*
  ** for Algolia InstantSearch nested query parameter
  */
  router: {
    parseQuery(query) {
      const qs = require("qs")
      /*
      return qs.parse(query, {
        arrayLimit: 100,
        ignoreQueryPrefix: true
      });
       */
      return qs.parse(query)
    },
    stringifyQuery(query) {
      const qs = require("qs")

      const result = qs.stringify(query)
      return result ? `?${result}` : ''
    }
  },
  /*
  ** Build configuration
  */
  build: {
    transpile: ['vue-instantsearch', 'instantsearch.js/es'],
    /*
    ** You can extend webpack config here
    */
    /*
    extend (config, ctx) {
    }
     */
  }
  // ...
}

Usage

Create pages/search.vue. Replace

  • index-name (Under Indices in Algolia Dashboard)
  • Application ID (Under API Keys in Algolia Dashboard)
  • Search-Only API Key

A few changes are made

  • Prevent search/listing if there if no query
  • Show result in row instead of grid
  • Hide pagination if only 1 page of result
  • Support Routing Url

Routing Url

  • By default, routing url use the format of /search?INDEX_NAME%5Bquery%5D=Hello ({INDEX_NAME: {query: "Hello"}})
  • writeState and readState is implemented to support /search?query=Hello instead. (NOTE: this might not work if multiple index is used)
<template>
  <ais-instant-search-ssr>
    <ais-search-box />
    <client-only>
      <ais-powered-by />
    </client-only>

    <ais-hits>
      <div slot="item" slot-scope="{ item }">
        <!-- show name -->
        <h2><ais-highlight attribute="name" :hit="item"/></h2>
        <!-- show content -->
        <div>{{ item.content }}</div>
        <!-- show content with highlight -->
        <div><ais-highlight attribute="content" :hit="item"/></div>
        <!-- show content with snippet: need to setup Snipetting in Indices -->
        <div><ais-snippet attribute="content" :hit="item"/></div>
      </div>
    </ais-hits>

    <ais-state-results>
      <template slot-scope="{ state: { query }, results: { hits, nbPages } }">
        <div v-if="query && hits.length == 0">No results</div>
        <div v-else></div>

        <ais-pagination v-if="nbPages > 0"/>
      </template>
    </ais-state-results>
  </ais-instant-search-ssr>
</template>

<script>
import {
  AisInstantSearchSsr,
  // AisRefinementList,
  AisHits,
  AisHighlight,
  AisSearchBox,
  // AisStats,
  AisPagination,
  AisSnippet,
  AisStateResults,
  AisPoweredBy,
  createServerRootMixin,
} from 'vue-instantsearch'
import algoliasearch from 'algoliasearch/lite'
import 'instantsearch.css/themes/algolia-min.css'

const indexName = 'live'

const algoliaClient = algoliasearch(
  '3P********', // Application ID
  '0a5f4c5c0b18********************'  // Search-Only API Key
);

// to disable listing on load
const searchClient = {
  search(requests) {
    if (requests.every(({ params }) => !params.query)) {
     return Promise.resolve({
        results: requests.map(() => ({
          hits: [],
          nbHits: 0,
          nbPages: 0,
          processingTimeMS: 0,
        })),
      });
    }

    return algoliaClient.search(requests);
  },
};

// remove indexName
function writeState(routeState) {
  if (indexName in routeState)
    routeState = routeState[indexName]

  return routeState
}

// restore indexName
function readState(routeState) {
  routeState = {
    [indexName]: routeState
  }
  return routeState
}

// read and write router state to support algolia query parameter
function nuxtRouter(vueRouter) {
  return {
    read() {
      return readState(vueRouter.currentRoute.query);
    },
    write(routeState) {
      routeState = writeState(routeState)
      vueRouter.push({
        query: routeState,
      });
    },
    createURL(routeState) {
      routeState = writeState(routeState)
      const url = vueRouter.resolve({
        query: routeState,
      }).href;
      return url
    },
    onUpdate(cb) {
      if (typeof window === 'undefined') return;

      this._onPopState = event => {
        const routeState = event.state;
        // On initial load, the state is read from the URL without
        // update. Therefore, the state object isn't present. In this
        // case, we fallback and read the URL.
        if (!routeState) {
          cb(this.read());
        } else {
          cb(routeState);
        }
      };
      window.addEventListener('popstate', this._onPopState);
    },
    dispose() {
      if (typeof window === 'undefined') return;

      window.removeEventListener('popstate', this._onPopState);
    },
  };
}


export default {
  serverPrefetch() {
    return this.instantsearch.findResultsState(this).then(algoliaState => {
      this.$ssrContext.nuxt.algoliaState = algoliaState;
    });
  },
  beforeMount() {
    const results = this.$nuxt.context.nuxtState.algoliaState || window.__NUXT__.algoliaState;

    this.instantsearch.hydrate(results);


  },
  data() {
    const mixin = createServerRootMixin({
      searchClient,
      indexName,
      routing: {
        router: nuxtRouter(this.$nuxt.$router),
      }
    })
    return {
      ...mixin.data(),
    };     
  },
  provide() {
    return {
      // Provide the InstantSearch instance for SSR
      $_ais_ssrInstantSearchInstance: this.instantsearch,
    };
  }, 
  components: {
    AisInstantSearchSsr,
    // AisRefinementList,
    AisHits,
    AisHighlight,
    AisSearchBox,
    // AisStats,
    AisPagination,
    AisSnippet,
    AisStateResults,
    AisPoweredBy,
  },
  /*
  head() {
    return {
      link: [
        {
          rel: 'stylesheet',
          href: 'https://cdn.jsdelivr.net/npm/instantsearch.css@7.3.1/themes/algolia-min.css',
        },
      ],
    };
  },
   */
};
</script>

<style>
.ais-SearchBox {
  margin-bottom: 1em;
}

.ais-Hits-item {
  width: 100%;
  border: none;
  box-shadow: none;

}

.ais-Highlight {
  font-size: inherit;
}

.ais-Highlight-highlighted {
  font-size: inherit;
}
</style>

References:

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.