Originally published on www.contentful.com

9 min read

How to add Algolia InstantSearch to your Next.js application

By the time I had written 15 blog articles on my website, it was getting a little tricky to find what I was looking for in a hurry! So I set out to implement search functionality on my blog.

After researching my options, I decided to try out Algolia. Algolia is a flexible hosted search and discovery API that comes with a generous free community plan. It provides up to 10,000 search requests per month, pre-built UI libraries (which we’ll use in this tutorial), natural language processing and many other features. What’s more, the engineers at Algolia are wonderfully helpful! I’d especially like to extend a huge thank you to LukyVJ, who showed up while I was learning about Algolia live on Twitch and helped me navigate the docs for the UI library.

What we’ll do in this tutorial

  1. Set up Algolia to receive data to power search results on a web application

  2. Create a custom script to transform and send the data to Algolia

  3. Build out the search UI in a Next.js application using the Algolia React InstantSearch UI

While the content on my blog site is powered by Contentful, the following concepts apply to any data store or headless CMS out there — even if you store your blog content as markdown with your code. All you need is a Next.js application and some content!

A screenshot of my website showing the new search box in action.

Let’s get started!

Sign up for Algolia

Head on over to Algolia to sign up. You’re invited to a free standard trial for 14 days, after which the plan will be converted to the Community plan automatically.

A screenshot of the Algolia website sign up screen.

Algolia does a really nice job of guiding you through the onboarding process. Follow the instructions until you land on the Get started screen!

A screenshot of the Get started screen after signing up to Algolia.

Create a new index

The first step in your search journey is to create a new index in Algolia. An index stores the data that you want to make searchable in Algolia. I like to think of it as a NoSQL document that stores JSON objects of your content. Read more about this on the Algolia docs.

A screenshot of the first step in setting up Algolia. The CTA is "create a new index" and it provides an input field to name your index. I chose the name "my_awesome_content".

Grab your API keys

Next, you’ll need three API keys from your Algolia account. Navigate to the API Keys area via the sidebar menu.

Find your Application IDSearch-Only API Key and Admin API Key. In your .env file in your Next.js application, add the following environment variables.

markup
NEXT_PUBLIC_ALGOLIA_APP_ID={Application ID}
NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY={Search-Only API Key}
ALGOLIA_SEARCH_ADMIN_KEY={Admin API Key}

To initialize InstantSearch on the front end, we need the Application ID and the Search API key to be publicly available on the client-side. Make sure to preface these two variables with NEXT_PUBLIC_. Just like the Contentful Content Delivery API keys, these keys provide read-only access to your search results, so it’s okay to expose them. 

We’re going to use the Admin API Key on the server-side only as part of the script to send data to the Algolia index. This key provides write-access to your Algolia index. Be sure to keep the Admin API Key a secret and do not expose it to the client with the NEXT_PUBLIC_ prefix.

That’s the setup! It’s done in just three steps! Now it’s time to write some code.

Write a custom script to build your data for your Algolia index

Let’s create a custom script to fetch our data and build up an array of objects to send to our Algolia index. I would recommend working in a script file that’s separate from the Next.js application architecture, which we can call with the postbuild command via the package.json scripts.

Create the script file

Create a directory called scripts and create a new file within it. I named my file build-search.js.

A screenshot of the file explorer in VSCode showing a scripts directory and a file named "build-search.js" inside it.

To your package.json file, add the postbuild command to run the script. This will run node build-search.js in the build pipeline after the build command has completed.

json
// package.json

"scripts": {
  "dev": "next dev",
  "build": "next build",
  "postbuild": "node ./scripts/build-search.js",
  "start": "next start"
},

Install dependencies

Let’s install the following dependencies from npm:

  • algoliasearch — to connect to the Algolia API

  • dotenv — to access environment variables outside of the Next.js application

Run the following command in your terminal at the root of your project:

bash-shell
npm install dotenv algoliasearch 

A note about Contentful Rich Text

The final implementation on my website handles adding a Contentful Rich Text field response to my search index as plain text. To reduce complexity, we won’t cover Rich Text in this post. But if you’re curious, find the code to handle Rich Text on GitHub.

Set up the script with an Immediately Invoked Function Expression

The script should perform several asynchronous operations, including fetching data from Contentful, transforming it and sending it to Algolia. To make the code more readable and to use async/await, we’re going to wrap everything in an async Immediately Invoked Function Expression (IIFE).

javascript
// build-search.js
const dotenv = require("dotenv");

(async function () {
  // initialize environment variables
  dotenv.config();

  console.log("Schnitzel! Let's fetch some data!");

})();

Run your script from the root of the project on the command line to test it out:

bash-shell
node ./scripts/build-search.js

Fetch your data

Fetch your data however you need to. View the full build-search.js file on GitHub to check out how I used the Contentful GraphQL API and node-fetch to grab my data for processing.

javascript
// build-search.js
const dotenv = require("dotenv");

async function getAllBlogPosts() {
  // write your code to fetch your data
}

(async function () {
  // initialize environment variables
  dotenv.config();

  try {
    // fetch your data
    const posts = await getAllBlogPosts();

    }
  } catch (error) {
    console.log(error);
  }
})(); 

Transform your data for Algolia

Transforming your data for Algolia is as simple as creating an array of objects that contains the data you want to be searchable!

Algolia search records are flexible and exist as objects of key-value pairs. Values can be added to the index as strings, booleans, numbers, arrays and objects. Attributes don’t have to respect a schema and can change from one object to another. For example, you could include a large recipe object or a smaller ingredient object in the same index! Read more on the Algolia docs about preparing your data for an index.

Here’s how I transformed my blog post data into an array of objects for Algolia. You can choose whether to provide an ID for each object, or have Algolia auto-generate an ID. Seeing as I had the sys.id from each blog post in Contentful, I chose to insert the posts with the IDs I had to hand.

javascript
// build-search.js
const dotenv = require("dotenv");

async function getAllBlogPosts() {
  // write your code to fetch your data
}

function transformPostsToSearchObjects(posts) {
  const transformed = posts.map((post) => {
    return {
      objectID: post.sys.id,
      title: post.title,
      excerpt: post.excerpt,
      slug: post.slug,
      topicsCollection: { items: post.topicsCollection.items },
      date: post.date,
      readingTime: post.readingTime,
    };
  });

  return transformed;
}

(async function () {
  dotenv.config();

  try {
    const posts = await getAllBlogPosts();
    const transformed = transformPostsToSearchObjects(posts);

    // we have data ready for Algolia!
    console.log(transformed);
  } catch (error) {
    console.log(error);
  }
})();

I also included a little extra data in my search objects, such as readingTimetopics and date to display an already-existing UI component in my search results on the front end (we’ll look at this later). This is the beauty of the flexible schema of the search objects!

Now we have our data records transformed for Algolia, let’s send them to the index!

Import your records programmatically to Algolia

After the content has been transformed, let’s initialize a new algoliasearch client with the environment variables we added earlier. Then, initialize the index with the name of the index you set up when you onboarded to Algolia, and call the saveObjects function with your transformed data. Make sure to import the algoliasearch dependency! Also, let’s log out the objectIDs from the response to make sure everything has gone smoothly.

javascript
// build-search.js
const dotenv = require("dotenv");
const algoliasearch = require("algoliasearch/lite");

async function getAllBlogPosts() {
  // write your code to fetch your data
}

function transformPostsToSearchObjects(posts) {
  // ...
}

(async function () {
  dotenv.config();

  try {
    const posts = await getAllBlogPosts();
    const transformed = transformPostsToSearchObjects(posts);

    // initialize the client with your environment variables
    const client = algoliasearch(
       process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
       process.env.ALGOLIA_SEARCH_ADMIN_KEY,
     );

     // initialize the index with your index name
     const index = client.initIndex("my_awesome_content");

     // save the objects!
     const algoliaResponse = await index.saveObjects(transformed);

     // check the output of the response in the console
     console.log(
       `🎉 Sucessfully added ${algoliaResponse.objectIDs.length} records to Algolia search. Object IDs:\n${algoliaResponse.objectIDs.join(
         "\n",
       )}`,
     );
  } catch (error) {
    console.log(error);
  }
})();

After the script has executed successfully, head on over to your Algolia dashboard, and you’ll see your index populated with your search objects. 🎉 You can also preview the results of the search algorithm — right there in the UI!

A screenshot of the Algolia Indices dashboard, showing a preview of the results as I search for "graphQL tutorial".

Given that you added the postbuild command to your package.json file, you are safe to commit these changes to your project. If your project is live and hosted on a hosting provider like Vercel, check out the build console to confirm the search results are sent to Algolia after your project has been built.

A screenshot of the output of build-search.js in the Vercel logs output UI.

Now our search data records are safe in Algolia, let’s look at how we can use the React InstantSearch UI library to search records in our Next.js application. 

Install InstantSearch dependencies

InstantSearch is Algolia’s front-end library. I always thought it was just a search box — but it’s so much more! It provides a library of pre-built and customizable components to build up a full-page UI on your front end — complete with super-fast filtering. Check out this React InstantSearch demo from Algolia on CodeSandbox.

In this tutorial, we’re going to use the React InstantSearch DOM library to build a simple search box that displays search results when a search term is provided. We’re also going to use some of the provided higher-order components from the library to allow us to build some custom UI components. 

Here’s a breakdown of the components we’ll be using and customizing.

A colour-coded diagram showing how the Algolia InstantSearch, SearchBox and Hits components work together.

Let’s get started by installing the dependencies. We’ll need algoliasearch that we installed earlier and react-instantsearch-dom. Run the following command in your terminal at the root of your project.

bash-shell
npm install react-instantsearch-dom

Using the default InstantSearch components

Create a new component file for the InstantSearch code and import the algoliasearch dependency.

javascript
// ./components/Search/index.js 

// “algoliasearch/lite” is the search-only version of the API client — optimized for size and search
import algoliasearch from "algoliasearch/lite";

export default function Search() {
  return (
    // Our search components will go here!
  )
}

InstantSearch works nicely with server-side rendering so we’re safe to use the new component on Next.js page files out of the box. Import the new component to your existing blog index page.

javascript
// ./pages/blog/index.js

import ContentfulApi from "./lib/ContentfulApi";
import PostList from "./components/PostList";
import Search from "./components/Search";

export default function BlogIndex({ posts }) {
  return (
    <>
        <Search />
        <PostList posts={posts} />
    </>
  );
}

export async function getStaticProps() {
  const posts = await ContentfulApi.getPostSummaries();

  return {
    props: {
      posts,
    },
  };
}

In your new search component, initialise a new algoliasearch client with the public environment variables you set up earlier.

javascript
// .components/Search/index.js

import algoliasearch from "algoliasearch/lite";

const searchClient = algoliasearch(
  process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
  process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY,
);

export default function Search() {
  return (
   // Our search components will go here!
  )
}

Import the InstantSearch, SearchBox and Hits UI components and render them in the component as follows. Pass the searchClient and the indexName you set up with Algolia as props into the InstantSearch component.

javascript
// .components/Search/index.js

import algoliasearch from "algoliasearch/lite";
import { InstantSearch, SearchBox, Hits } from "react-instantsearch-dom";

const searchClient = algoliasearch(
  process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
  process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY,
);

export default function Search() {
  return (
    <>
      <InstantSearch 
        searchClient={searchClient} 
        indexName="my_awesome_content">
        <SearchBox />
        <Hits />
      </InstantSearch>
    </>
  );
}

You’ll now see something like this on your blog index page. Type into the search box to watch your InstantSearch results update — instantly!

A screenshot of the default InstantSearch unstyled UI, available after doing the minimal setup.

That’s InstantSearch connected to our Algolia index, displaying and updating search results in real-time. Now, let’s look at creating some custom components to give us more control over the UI and CSS, and to only render the search results when there’s a search query present in the input field. 

Create your custom components

CustomSearchBox.js

Create a new file inside your Search component folder called CustomSearchBox.js. This will be a new custom form that will perform the search. 

  • Import the connectSearchBox higher-order component from react-instant-search-dom — this is the function that will connect the custom search box to the InstantSearch client. Read more about higher-order components in React.

  • Build your HTML form using the available refine prop to manage the onChange of the input field. I chose to add a label element alongside the input field for accessibility reasons.

  • Export your custom component wrapped with connectSearchBox.

  • You’re free to style the form with standard CSS classes, CSS Modules, Styled Components and so on.

javascript
// .components/Search/CustomSearchBox.js

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

function SearchBox({ refine }) {
  return (
    <form action="" role="search">
      <label htmlFor="algolia_search">Search articles</label>
      <input
        id="algolia_search"
        type="search"
        placeholder="javascript tutorial"
        onChange={(e) => refine(e.currentTarget.value)}
      />
    </form>
  );
}

export default connectSearchBox(SearchBox);

Import and render the CustomSearchBox component as a child of the InstantSearch component, like so.

javascript
// .components/Search/index.js

import algoliasearch from "algoliasearch/lite";
import { InstantSearch, Hits } from "react-instantsearch-dom";
import CustomSearchBox from "./CustomSearchBox";

const searchClient = algoliasearch(...);

export default function Search() {
  return (
    <>
      <InstantSearch searchClient={searchClient} indexName="p4nth3rblog">
        <CustomSearchBox />
        <Hits />
      </InstantSearch>
    </>
  );
}

Next, onto the custom hits component.

CustomHits.js

Create a new file inside your Search component folder called CustomHits.js. This will be the component that processes the logic to only show our search results when a search query is present in the input field.

  • Import the connectStateResults higher-order component from react-instant-search-dom — this is the function that will connect the custom hits to the InstantSearch client.

  • Capture searchState and searchResults as props in the component function declaration.

  • Build your HTML output using the available searchResults prop to manage the onChange of the input field.

  • Export your custom component wrapped with connectStateResults.

  • You’re free to style the form with standard CSS classes, CSS module styles, Styled Components and so on.

  • You’re free to render another custom component to display the searchResults.hits. I used the same component that displays my recent blog posts on my home page!

  • Optional: use searchState.query to process some logic to only render results to the DOM if the length of the search query is greater than or equal to three characters in length. 

javascript
// ./components/Search/CustomHits.js
import { connectStateResults } from "react-instantsearch-dom";

function Hits({ searchState, searchResults }) {
  const validQuery = searchState.query?.length >= 3;

  return (
    <>
      {searchResults?.hits.length === 0 && validQuery && (
        <p>Aw snap! No search results were found.</p>
      )}
      {searchResults?.hits.length > 0 && validQuery && (
        <ol>
          {searchResults.hits.map((hit) => (
            <li key={hit.objectID}>{hit.title}</li>
          ))}
        </ol>
      )}
    </>
  );
}

export default connectStateResults(Hits);

Import and render the CustomHits component as a child of the InstantSearch component.

javascript
// .components/Search/index.js

import algoliasearch from "algoliasearch/lite";
import { InstantSearch } from "react-instantsearch-dom";
import CustomSearchBox from "./CustomSearchBox";
import CustomHits from "./CustomHits";

const searchClient = algoliasearch(
  process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
  process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY,
);

export default function Search() {
  return (
    <>
      <InstantSearch searchClient={searchClient} indexName="p4nth3rblog">
        <CustomSearchBox />
        <CustomHits />
      </InstantSearch>
    </>
  );
}

And there you have it! Now you’ve got InstantSearch hooked up with your custom components, you’re now free to style them up to your heart’s content!

Click here to see the full code example, complete with styles using CSS Modules.

Is there something you’d like to learn more about to get the most out of Contentful? Come and let us know in the Community Slack. We love meeting new developers!

A headshot of Salma wearing black on a red patterned background.

whitep4nth3r | Salma Alam-Naylor

I help developers build stuff, learn things, and love what they do • DevRel @ Contentful • Twitch partner • Microsoft MVP (She/Her)