Content search with Nuxt, Algolia, and Contentful

Integrating Algolia into a Nuxt+Contentful stack.

Intro

If you’re looking to cut to the nitty gritty of indexing Contentful content in Aloglia and then searching that index in your Nuxt site I’ve included the links to the guides I followed below.

How I’d seen search done before

I’d inherited 2 sections of brightcove.com with search functionality on them, both implemented entirely different ways…

Approach #1 was just a little helper script written by the team that essentially said ‘given a search query and an array of page data nodes, return the id of the nodes where the body text includes the query text’. It was hacky, poor in terms of performance, and not super thorough, but the scope of what was being searched was limited enough that it worked well for the use case.

Approach #2 was using a service called Swiftype. Switftype crawls your live site content, indexes it, and allows you to access that index via their API. This approach is nice because its automated. Its not so nice because it indexes everything on the front end of my site, so common elements (navigation for example) get indexed for every page unless I explicitly add attributes to their markup.

I wanted to find a service that did the best of both and test it out on larocque.dev. When I say best of both I mean automated, but with a limited-by-default and easily defineable scope of what to index.

I can’t say I did a ton of research into search engine services to find the absolute best, I just happened to find information about integrating Aloglia quickly into the process and their integration seemed simple to get running. Also they had a continually free plan so here we go!

Implementing Algolia

At the time of writing this (May 2022) Algolia’s pricing page says ‘Forever free’, so I snagged an account

Getting Contentful entries indexed by Algolia

I ended up using the webhook approach outlined in this doc. The Contentful webhooks page for your space has ‘Webhook Templates’, one of which lets you send content to Algolia as it gets published. Now all new entries will get indexed as they come online, and a simple batch unpublish+republish indexed the current content.

Setting searchable parameters

With the webhook set up, Algolia will automatically index everything sent to it from Contentful, but you configure what fields it searches through when you start sending requests to their API. By going to the ‘Configure’ tab in Algolia I was able to specify that I care about my entry title, body, and relatedTech, the rest can be ignored.

Getting and showing results

Algolia has an SDK similar to Contentful, you just initialize a client with an API key and then can reuse the client for requests to their API.

SearchBox component

The component for search itself was built with the algoliasearch/lite for the client and vue-instantsearch for the components.

<script>

script-tag

imports

Starting out we import 3 components from vue-instantsearch, the ‘lite’ search client from algoliasearch/lite, and the base styles for the 3 vue-instantsearch components:

import { AisInstantSearch, AisSearchBox, AisHits } from "vue-instantsearch";
import algoliasearch from "algoliasearch/lite";
import "instantsearch.css/themes/satellite-min.css";
components

In the export script we include those 3 components as-is

export default {
	components: {
		AisInstantSearch,
		AisSearchBox,
		AisHits,
	},
	///
};
data()

The returned data() function includes the search client, and a small object to lookup ‘human’ names for Contentful content types.

data() {
  return {
    searchClient: algoliasearch(
      "APP_ID",
      "API_KEY"
    ),
    contentTypeLookup: {
      chrisProjectPage: "Project",
      tech: "Tech",
    },
  };
},
methods

The returned methods object includes a lookup similar to contentTypeLookup for getting the link paths based on the content type returned. Looking at this now I realize I could probably roll contentTypeLookup into this and return both pieces of info in an object.

methods: {
  linkLookup: function (slug, contentType) {
    switch (contentType) {
      case "chrisProjectPage":
        return `projects/${slug}`;
      case "tech":
        return `tech/${slug}`;
      default:
        "/";
    }
  },
},

<template>

template-tag

<AisInstantSearch>
<AisInstantSearch :search-client="searchClient" index-name="larocque.dev">

The <AisInstantSearch> component is a wrapper for the suite of Algolia instant search components in vue-instantsearch. In my site I just imported AisSearchBox and AisHits but they have several other components available to extend functionality and UI.

<AisInstantSearch> takes a search cient (initialized in our <script>) and an index name. Now we have a component wired up to our Algolia index that can take children components that interact with and show that data.

<AisSearchBox />
<AisSearchBox class="mt-3" />

The <AisSearchBox /> component is super straightforward, passed as a child component it queries your Algolia index based on whatever text the user inputs.

<AisHits>
<AisHits>
  <template #item="{ item }">
    <div class="card">
      <div class="card-content">
        <p class="subtitle has-text-grey is-size-5">
          {{ contentTypeLookup[item.sys.contentType.sys.id] }}
        </p>
        <p class="title has-text-weight-bold is-size-4">
          {{ item.fields.title["en-US"] }}
        </p>
        <div class="content">
          {{ item.fields.description["en-US"] }}
        </div>
      </div>
      <footer class="card-footer">
        <NuxtLink
          :to="
            linkLookup(
              item.fields.slug['en-US'],
              item.sys.contentType.sys.id
            )
          "
          class="card-footer-item"
          >{{
            `${contentTypeLookup[item.sys.contentType.sys.id]} page for ${
              item.fields.title["en-US"]
            }`
          }}</NuxtLink
        >
      </footer>
    </div>
  </template>
</AisHits>

<AisHits> besides being a descriptor for futuristic robotic waste, serves as a way to define a template to be used for each of your search results. Here you can see how contentTypeLookup and linkLookup() end up being used.

Search page

searchpage

The page this is all rendered on is super simple, just a <Hero> and the <SearchBox /> component we just build. Bada bing!

Last updated: 11/3/2022