Published

- 6 min read

Supercharging Your Next.js Blog with Static Data and Full-Text Search

img of Supercharging Your Next.js Blog with Static Data and Full-Text Search

Introduction

In the world of blogging, speed and ease of navigation are key. One way to dramatically enhance both is by using static data and implementing full-text search functionality.

In this guide, we will walk you through how to implement these features using Next.js, a popular React framework that enables server-side rendering and static site generation.

Our focus will be on how to use Lunr.js for full-text search, and how to leverage next.config.js to generate static data files from your CMS data.

Lunr.js: The Lightweight Search Engine

Firstly, we will need to install Lunr.js, a small, yet powerful search engine designed for use in the browser. Lunr.js provides full-text search in your browser, with no external dependencies, making it an ideal choice for implementing search functionality in your Next.js blog.

Install Lunr.js with the following command:

    pnpm i lunr

What makes Lunr.js stand out is its simplicity, extensibility, and portability:

  • Simplicity: Lunr.js is designed to be small and full-featured, providing a great search experience without the need for external, server-side, search services.

  • Extensibility: With Lunr.js, you can add powerful language processors to deliver more accurate results to user queries, or tweak the built-in processors to better fit your content.

  • Portability: Lunr.js has no external dependencies and works both in your browser and on the server with Node.js.

Leveraging next.config.js for Static Data

Now that we have Lunr.js installed, we need to retrieve post data from our CMS and generate an index from the selected fields. This is where next.config.js comes into play. This configuration file allows us to customize various aspects of how Next.js works. For instance, with the trailingSlash configuration, we can specify whether URLs with trailing slashes are redirected to their counterparts without a trailing slash or vice versa. For further information, check the docs.

Here is the full configuration required to create our index file:

// next.config.js file

const path = require('path')
const fs = require('fs')
const lunr = require('lunr')

const createIndex = async () => {
	const postsResponse = await fetch(
		'https://YOUR-BLOG.com/api/blog-posts?populate=featured_image&populate=categories',
		{
			headers: {
				Authorization: `Bearer ${process.env.STRAPI_TOKEN}`
			}
		}
	)

	const posts = (await postsResponse.json()).data

	const index = lunr(function () {
		// those are the fields that will be indexed
		this.field('id')
		this.field('meta')
		this.field('title', { boost: 10 }) // boost will give more weight to the title

		posts.forEach((post) => {
			const { attributes } = post
			const { meta, title, slug } = attributes
			this.add({
				id: slug,
				meta: meta.toLowerCase(),
				title: title.toLowerCase()
			})
		})
	})

	const indexFile = path.resolve(__dirname, './public/search-index.json')
	const summaryFile = path.resolve(__dirname, './public/search-index-summary.json')

	// we will need the post data to persist statically too, so we can use it on the search page
	const searchSummary = posts.map((post) => {
		const { attributes } = post
		const { slug } = attributes

		const featured_image = attributes?.featured_image?.data?.attributes
		const { name, slug: categorySlug } = attributes?.categories?.data[0]?.attributes

		// remove unused data
		delete attributes.content
		delete attributes.categories
		delete attributes.id

		return {
			...attributes,
			featured_image,
			category: {
				name,
				slug: categorySlug
			},
			id: slug
		}
	})

	fs.writeFileSync(indexFile, JSON.stringify(index))
	fs.writeFileSync(summaryFile, JSON.stringify(searchSummary))
}

const nextConfig = async (phase, { defaultConfig }) => {
	// a little check to not run this on every save while on dev mode
	if (!fs.existsSync('./public/search-index.json')) {
		await createIndex()
	}

	return {
		...defaultConfig
		// ... your existing config goes here
	}
}

module.exports = nextConfig

This configuration fetches blog posts from our CMS, generates a search index using Lunr.js, and saves the index and summary of posts as static files in the public folder. It also cleans up the post data to keep only the necessary fields, reducing the size of the static files and improving the load times.

The createIndex() function is the heart of this configuration. It fetches post data from our CMS, generates a Lunr.js search index based on the id, meta, and title fields of the posts, and saves the index and a summary of the post data as static files.

The nextConfig() function uses the createIndex() function to generate the index and post summary files when they do not exist. This prevents unnecessary index generation during development.

With the static data and search index in place, the final step is to create a search component to allow users to search the blog posts. Here’s how it can be done:

// search.js file

import { useEffect, useState } from 'react'
import lunr from 'lunr'

const Search = () => {
	const [index, setIndex] = useState(null)
	const [filterResult, setFilterResult] = useState([])
	const [posts, setPosts] = useState([])

	useEffect(() => {
		// load the index and posts data from the public folder
		Promise.all([
			fetch('/search-index.json').then((response) => response.json()),
			fetch('/search-index-summary.json').then((response) => response.json())
		]).then(([indexData, postsData]) => {
			// load the index
			const indexFromServer = lunr.Index.load(indexData)

			setIndex(indexFromServer)
			setPosts(postsData)
		})
	}, [])

	const onSearch = (event) => {
		const { value } = event.target

		if (value?.trim() === '') {
			setFilterResult([])
			return
		}

		if (index) {
			// split the search terms and remove any empty strings
			const terms = value.trim().split(' ').filter(Boolean)

			// create the query string, notice "~1" means fuzzy search
			// "+" means "AND" search of each term
			const query = terms.map((term) => `+${term}~1`).join(' ')

			// results are returned in the order of most relevant to least relevant
			let results = index.search(query)

			// map the results to the posts
			const fullResults = results.map((result) => posts.find((item) => item.id === result.ref))

			setFilterResult(fullResults)
		}
	}

	return (
		<div>
			<input onChange={onSearch} />

			{filterResult?.map((result, index) => (
				<div key={index}>
					<h3>{result.title}</h3>
					<p>{result.meta}</p>
				</div>
			))}
		</div>
	)
}

export default Search

This component first fetches the search index and post summaries from the public folder. It then sets up an onSearch function to execute whenever a search query is entered.

This function uses the Lunr.js search index to find the posts that best match the query and display them.


Frequently Asked Questions

What is Lunr.js?

Lunr.js is a small, full-text search library for use in the browser. It indexes JSON documents and provides a simple search interface for retrieving documents that best match text queries. It’s an excellent choice for web applications with all their data already on the client side, allowing the search to be performed locally and without the need for network overhead​1​.

What are the features of Lunr.js?

Lunr.js provides full-text search support for 14 languages, allows for boosting terms at query time or boosting entire documents at index time, scopes searches to specific fields, and supports fuzzy term matching with wildcards or edit distance​1​.

How does next.config.js work in Next.js?

next.config.js is a configuration file for Next.js where you can adjust various settings of your Next.js application. For example, with the trailingSlash configuration, you can specify whether URLs with trailing slashes are redirected to their counterparts without a trailing slash or vice versa​2​.

Conclusion

Implementing full-text search and leveraging static data in your Next.js blog can enhance your user experience, provide faster load times, and improve the overall functionality of your site. The combination of Next.js and Lunr.js makes this process straightforward and efficient, enabling you to supercharge your blog with these powerful features.

Related Posts

There are no related posts yet. 😢