Article cover image

Integrating Algolia with SSR in Next.js App Router

  • Drayke
    Drayke
    Frontend Developer

To introduce search functionality to the Monogram blog, we prioritized implementing a robust search solution capable of handling our expanding content efficiently. After careful evaluation, we chose Algolia for its speed, relevance, and seamless integration capabilities. By integrating Algolia with Next.js using server-side rendering (SSR), we ensure a fast, responsive search experience with quick load times. Here's a guide on how we achieved this.

Adding our content to Algolia

Step 1: Install and Initialize Algolia Client
First, install the Algolia search client and Initialize it using your Algolia API credentials:

import algoliasearch from 'algoliasearch'

const algoliaClient = algoliasearch(
	process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
	process.env.ALGOLIA_WRITE_API_KEY
)

Step 2: Create a Script to Synchronize Content
To keep your Algolia index up-to-date with your content, create a script for indexing. This involves:

  • Fetching Content: Retrieve your content from your CMS.
  • Preparing Batch Operations: Create batch operations to add or delete records from the index.
  • Executing the Batch Request: Send the batch request to Algolia to update the index.
import algoliasearch from 'algoliasearch'

const algoliaClient = algoliasearch(
  process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
  process.env.ALGOLIA_WRITE_API_KEY
)

async function reIndexBlog() {
  const index = algoliaClient.initIndex(process.env.NEXT_PUBLIC_ALGOLIA_BLOG_INDEX)
  const blogPosts = await getBlogPosts()
  
  const algoliaBatchContent = blogPosts.map(post => ({
    action: 'addObject',
    body: {
      objectID: post.id,
      // Add other fields as necessary
    },
  }))
  
  let algoliaObjectIds = []
  
  await index.browseObjects({
    attributesToRetrieve: ['objectID'],
    batch: batch => {
      algoliaObjectIds = algoliaObjectIds.concat(batch.map(hit => hit.objectID))
    }
  })
  
  const postsToDeleteFromAlgolia = algoliaObjectIds.filter(
    objectID => !blogPosts.some(post => post.id === objectID)
  )
  
  postsToDeleteFromAlgolia.forEach((objectID) => {
		algoliaBatchContent.push({
			action: 'deleteObject',
			body: { objectID },
		})
	})
  
  const { taskID } = await index.batch(algoliaBatchContent)
  await index.waitTask(taskID)
}

In this script:

  • algoliaBatchContent: Prepares the content to be added or updated in the index.
  • algoliaObjectIds: Collects the IDs of the objects currently in the index.
  • postsToDeleteFromAlgolia: Determines which posts need to be removed from the index by comparing IDs in Algolia that are no longer in our CMS.
  • index.batch(): Executes the batch operation to update the index.

Step 3: Set Up Re-Index Webhook with Next.js API Routes
We use Next.js API routes to handle re-indexing via webhooks from our CMS. When a blog post is created or updated a POST request is sent to our API route with the data of the post that was created or updated. Here’s the simplified version:

import algoliasearch from 'algoliasearch'
import { normalizeAlgoliaBlogPost } from '@/src/lib/algolia/normalize'

const algoliaBlogIndex = process.env.NEXT_PUBLIC_ALGOLIA_BLOG_INDEX

const secret = process.env.WEBHOOK_SECRET
const algolia = algoliasearch(
	process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
	process.env.ALGOLIA_WRITE_API_KEY
)
const algoliaClient = algolia.initIndex(algoliaBlogIndex)

export async function POST(req: Request) {
	// Verify webhook secret
	const webhookSecret = req.headers.get('webhook-secret')

	if (webhookSecret !== secret) {
		const message = 'Invalid webhook secret'
		return new Response(message, { status: 401 })
	}

	const post = (await req.json())

	if (!post) {
		const message = 'No post found in request body'
		return new Response(message, { status: 400 })
	}

	try {
		await algoliaClient.saveObject(normalizeAlgoliaBlogPost(post))

		return new Response(
			`✅ "${post.title}" blog post was added/updated in Algolia index: "${algoliaBlogIndex}"`,
			{ status: 200 }
		)
	} catch (error) {
		const message = error instanceof Error ? error.message : String(error)

		return new Response(message, { status: 500 })
	}
}

Integrating Instant Search with Next.js

Step 1: Install Required Libraries
Install react-instantsearch and react-instantsearch-nextjs

npm install react-instantsearch react-instantsearch-nextjs

Step 2: Create a Layout Component for Search Context
Create a layout component that provides the Algolia search context to your components:

'use client'

import algoliasearch from 'algoliasearch/lite'
import { InstantSearchNext } from 'react-instantsearch-nextjs'

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

export default function BlogLayout({ children }) {
  return (
    <InstantSearchNext
      indexName={process.env.NEXT_PUBLIC_ALGOLIA_BLOG_INDEX}
      searchClient={searchClient}
    >
      {children}
    </InstantSearchNext>
  )
}

Step 3: Build Search Components
The SearchBox Component Handles user input and updates the search query.

'use client'

import { useRef, useState } from 'react'
import { useSearchBox } from 'react-instantsearch'

export default function SearchBox() {
  const { query, refine } = useSearchBox()
  const [inputValue, setInputValue] = useState(query)
  const inputRef = useRef<HTMLInputElement>(null)

  function setQuery(newQuery: string) {
    setInputValue(newQuery)
    refine(newQuery)
  }

  return (
    <form role="search" onSubmit={event => event.preventDefault()}>
      <input
        ref={inputRef}
        autoComplete="off"
        autoCorrect="off"
        autoCapitalize="off"
        placeholder="Search our knowledge..."
        spellCheck={false}
        type="search"
        value={inputValue}
        onChange={event => setQuery(event.currentTarget.value)}
      />
    </form>
  )
}

The BlogFeed Component Displays search results retrieved from Algolia.

'use client'

import Link from 'next/link'
import { useHits } from 'react-instantsearch'

export default function BlogFeed() {
  const { results } = useHits()

  if (!results) return null

  return (
    <div>
      {results.hits.map(hit => (
        <Link key={hit.objectID} href={`/blog/${hit.slug}`}>
          <time dateTime={hit.publishedDate}>{hit.publishedDate}</time>
          <h3>{hit.title}</h3>
          <p>{hit.excerpt}</p>
        </Link>
      ))}
    </div>
  )
}

Step 4: Integrate Components into the Blog Home Page
Combine the SearchBox and BlogFeed components into your blog's home page:

import SearchBox from '@/components/blogSearch/SearchBox'
import BlogFeed from '@/components/blogSearch/BlogFeed'

export default function BlogHome() {
  return (
    <>
      <SearchBox />
      <BlogFeed />
    </>
  )
}

By integrating Algolia with Next.js, we have built a powerful search experience for the Monogram blog. The use of server-side rendering (SSR) ensures that users receive a fast and relevant search experience from the start. Client-side components then handle dynamic interactions, ensuring that search results are updated in real-time.