How to add Algolia search to Next.js website

Updated on: Thu Apr 14 2022

Hey! Here are some notes on how I added algolia search to this blog.

Prerequisites

Blog yaraoncode.me is built powered by next.js, deployed to vercel. Content comes from sanity.io headless cms. It utilises SSG and generates all pages out of content fetched from sanity.io on build time.

Basically process of integration of Algolia search to website consists of 3 stages:

  • setup algolia project
  • populate algolia index on build time
  • build a UI based on algolia instant search react component

Setup algolia project

Go to algolia.com, create an account or if you already have one then add new search application. Algolia will provide a beautiful application setup wizard. Go through it, setup is pretty straightforward there.
Initially it offers you to upload data to index manually - try it at the first place and check you data source via algolia admin panel UI. In my case I downloaded some Posts data from sanity, created posts.json file and uploaded it to algolia index. Here part of it's content:

copied json

[
  {
    "slug": {
      "_type": "slug",
      "current": "useful-bash-tips-and-tricks"
    },
    "title": "Useful bash tips and tricks"
  },
  {
    "slug": {
      "_type": "slug",
      "current": "how-to-create-publish-and-use-cli-util-with-nodejs-commanderjs-and-typescript"
    },
    "title": "How to create, publish and use cli util with nodejs commanderjs and typescript"
  },
  {
    "slug": {
      "_type": "slug",
      "current": "how-to-run-mongo-in-docker-and-restore-dump-there-https-www-notion-so-how-to-run-mongo-in-docker"
    },
    "title": "How to run mongo in docker and restore dump there"
  },
]

Just in case providing here some screenshots from my setup.

Searchable attributes:

Ranking and Sorting:

Populate algolia index on build time

Here we'll utilize next.js's api getStaticProps. I use it to fetch posts from sanity.io and pregenerate all pages at build time and it perfectly suitable for refreshing algolia index. Basically what we want to achieve is upload all needed search data to algolia index every time we build project. And builds are triggered for yaraoncode.me via webhook every time content in sanity.io got changed.
So, long story short, on main page in addition to component itself we have:

copied ts/tsx

export const getStaticProps = async () => {
  ...
  const posts = await sanity.fetch(postsQuery);
  const warmupSearch = await import("@/sanity/utils/warmupSearch");
  await warmupSearch.default();
  return {
    props: {
      posts,
    },
  };
};

Then in @/sanity/utils/warmupSearch we have to fetch needed data from sanity and feed into algolia index. (@ stands for src, setup is made via alias). Content of the file example:

copied ts/tsx

import algoliasearch from "algoliasearch";
import groq from "groq";
import sanity from "@/sanity/client";

const postsForSearchQuery = groq`*[_type == "post" && publishedAt < now() && hidden != true]|order(_updatedAt desc){
_id,
title,
slug,
mainImage,
"plaintextBody": pt::text(body)
}`;

const warmupSearch = async () => {
  const { NEXT_PUBLIC_ALGOLIA_SECRET, NEXT_PUBLIC_ALGOLIA_ID } = process.env;
  const client = algoliasearch(
    NEXT_PUBLIC_ALGOLIA_ID,
    NEXT_PUBLIC_ALGOLIA_SECRET
  );
  const index = client.initIndex("posts-search-index");

  const postsForSearch = await sanity.fetch(postsForSearchQuery);
  postsForSearch.forEach((post: any) => {
    post.objectID = post._id;
  });

  await index.saveObjects(postsForSearch);
};

export default warmupSearch;

Important note: body field in sanity contains structured data and could not be sent to algolia without preparation. Here we use pt::text(body) groq funtion to retrieve it as a raw string.

If posts-search-index which we initialising here is not created yet then algolia will create one for us.

Important note: post.objectID = post._id operation needed to assign special objectID field to algolia item in index. It's by algolia design.

Also we have to not forget to add NEXT_PUBLIC_ALGOLIA_ID, NEXT_PUBLIC_ALGOLIA_SECRET variables to .env.local and in case we deploy to vercel then to environment variables in vercel console as well.

Build a UI based on algolia instant search react component

Algolia offers quite powerful and beautiful UI out of the box but we want for it to be easy and fit our design so we'll utilize algolia's react-instantsearch-dom package and it's customization options.
Our Search component starts with InstantSearch smart component. Without any additional styling whole thing looks like this:

copied ts/tsx

<InstantSearch
  searchClient={searchClient}
  onSearchStateChange={(searchState) => {
    setSearchQuery(searchState.query);
  }}
  indexName="posts-search-index"
  stalledSearchDelay={500}
>
  <SearchBox />
  {searchQuery && <Hits />}
</InstantSearch>;

To define seachClient we'll utilize algoliasearch/lite package.

copied ts/tsx

const NEXT_PUBLIC_ALGOLIA_ID = process.env.NEXT_PUBLIC_ALGOLIA_ID;
const NEXT_PUBLIC_ALGOLIA_SECRET = process.env.NEXT_PUBLIC_ALGOLIA_SECRET;

const getAlgoliaClient = () => {
  return algoliasearch(NEXT_PUBLIC_ALGOLIA_ID, NEXT_PUBLIC_ALGOLIA_SECRET);
};
const searchClient = getAlgoliaClient();

Important note: on client side next.js does not allow object destructuring pattern for process.env so we have to write in old style const NEXT_PUBLIC_ALGOLIA_ID = process.env.NEXT_PUBLIC_ALGOLIA_ID.

SearchBox component here stands for search input itself and Hits is for results.

copied ts/tsx

import { connectHits } from "react-instantsearch-dom";

const Hits = ({ hits }) => (
  <>
    {hits.map((hit) => {
      return <div key={hit.objectID} hit={hit}></div>;
    })}
  </>
);
const CustomHits = connectHits(Hits) as any;

export default CustomHits;

connectHits HOC needed to consume search state from InstantSearch smart component.

Here is a simple implementation of SearchBox. I'll leave some tailwind classes to copy paste them right away later just in case.

copied ts/tsx

import { useRouter } from "next/router";
import { useEffect } from "react";
import { connectSearchBox } from "react-instantsearch-dom";
import Image from "next/image";

const SearchBox = ({ currentRefinement, refine }) => {
  const router = useRouter();
  useEffect(() => {
    refine(null);
  }, [refine, router.asPath]);
  return (
    <form noValidate action="" role="search">
      <div className="flex relative">
        <input
          className="w-full rounded-md bg-stone-200 px-8 py-4 pr-12 outline-none font-light"
          value={currentRefinement}
          placeholder="Search"
          onChange={(event) => refine(event.currentTarget.value)}
        />
        <div className="grid h-full place-items-center absolute right-0 top-0 pr-4">
          {currentRefinement && (
            <button
              className="h-14 flex items-center opacity-50 hover:opacity-90 transition-opacity duration-300"
              onClick={(e) => {
                e.preventDefault();
                refine("");
              }}
            >
              <Image src="/img/icons8-close.svg" height={32} width={32} />
            </button>
          )}
        </div>
      </div>
    </form>
  );
};

const CustomSearchBox = connectSearchBox(SearchBox) as any;

export default CustomSearchBox;


connectHits HOC needed to consume search state from InstantSearch smart component as well.
This part needed to reset search state when navigating between routes.

copied ts/tsx

const router = useRouter();
useEffect(() => {
  refine(null);
}, [refine, router.asPath]);

Demo

Useful links